feat: custom server config

This commit is contained in:
overtrue
2025-07-07 21:34:50 +08:00
parent e95e16de21
commit f3bb968cd8
14 changed files with 455 additions and 46 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" viewBox="0 0 1422 800"><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="oooscillate-grad"><stop stop-color="hsl(206, 75%, 49%)" stop-opacity="1" offset="0%"></stop><stop stop-color="hsl(331, 90%, 56%)" stop-opacity="1" offset="100%"></stop></linearGradient></defs><g stroke-width="6" stroke="hsl(205, 69%, 50%)" fill="none" stroke-linecap="round"><path d="M 0 630 Q 355.5 -100 711 400 Q 1066.5 900 1422 630" opacity="0.56"></path><path d="M 0 588 Q 355.5 -100 711 400 Q 1066.5 900 1422 588" opacity="0.38"></path><path d="M 0 546 Q 355.5 -100 711 400 Q 1066.5 900 1422 546" opacity="0.42"></path><path d="M 0 504 Q 355.5 -100 711 400 Q 1066.5 900 1422 504" opacity="0.84"></path><path d="M 0 462 Q 355.5 -100 711 400 Q 1066.5 900 1422 462" opacity="0.35"></path><path d="M 0 420 Q 355.5 -100 711 400 Q 1066.5 900 1422 420" opacity="0.84"></path><path d="M 0 378 Q 355.5 -100 711 400 Q 1066.5 900 1422 378" opacity="0.98"></path><path d="M 0 336 Q 355.5 -100 711 400 Q 1066.5 900 1422 336" opacity="0.53"></path><path d="M 0 294 Q 355.5 -100 711 400 Q 1066.5 900 1422 294" opacity="0.64"></path><path d="M 0 252 Q 355.5 -100 711 400 Q 1066.5 900 1422 252" opacity="0.40"></path><path d="M 0 210 Q 355.5 -100 711 400 Q 1066.5 900 1422 210" opacity="0.78"></path><path d="M 0 168 Q 355.5 -100 711 400 Q 1066.5 900 1422 168" opacity="0.19"></path><path d="M 0 126 Q 355.5 -100 711 400 Q 1066.5 900 1422 126" opacity="0.55"></path><path d="M 0 84 Q 355.5 -100 711 400 Q 1066.5 900 1422 84" opacity="0.26"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

+1
View File
@@ -40,6 +40,7 @@ declare module 'vue' {
NGrid: typeof import('naive-ui')['NGrid']
NH3: typeof import('naive-ui')['NH3']
NH4: typeof import('naive-ui')['NH4']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
+29
View File
@@ -0,0 +1,29 @@
<template>
<div class="w-full bg-gray-50 dark:bg-black h-full p-16 flex flex-col justify-center gap-8 overflow-hidden relative border-r dark:border-none">
<div class="max-w-7xl flex flex-col z-10">
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<div class="text-4xl my-6 font-semibold !text-primary px-0">
<span :class="{ 'pr-1': locale !== 'zh' }">{{ t('Rust-based') }} </span>
<FlipWords :words="[t('High Performance'), t('Infinite Scaling'), t('Secure & Reliable'), t('Multi-Cloud Storage'), t('S3 Compatible')]" :duration="3000"
class="text-4xl font-semibold !text-primary px-0" />
<div class="text-muted-foreground mt-2">
{{ t('Reliable distributed file system') }}
</div>
</div>
</div>
<a href="https://www.rustfs.com" class="z-10 text-primary-500 inline-flex w-max items-center gap-2 leading-none p-2 px-5 border rounded-full border-blue-500">
<span>{{ t('Visit website') }}</span>
<Icon name="ri:arrow-right-long-fill" class="mr-2" />
</a>
<div class="h-full inset-0 absolute z-0">
<img src="~/assets/backgrounds/ttten.svg" class="absolute h-full w-full inset-0 z-0 opacity-45 object-cover" alt="" />
</div>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import FlipWords from '~/components/ui/flip-words/FlipWords.vue'
const { t, locale } = useI18n()
</script>
+5
View File
@@ -73,6 +73,11 @@ export default [
key: "divider-2",
type: "divider",
},
{
label: "Settings",
to: "/settings",
icon: "ri:settings-3-line",
},
{
label: "License",
// to: 'https://github.com/rustfs/rustfs?tab=Apache-2.0-1-ov-file#readme',
+18 -1
View File
@@ -133,6 +133,20 @@
"Welcome Back": "Welcome Back",
"Login Problems": "Login Problems",
"Bucket Not Empty": "Bucket Not Empty",
"Server Configuration": "Server Configuration",
"Server URL": "Server URL",
"Enter complete server URL (e.g., http://localhost:9000)": "Enter complete server URL (e.g., http://localhost:9000)",
"Server URL is required": "Server URL is required",
"Only HTTP and HTTPS protocols are supported": "Only HTTP and HTTPS protocols are supported",
"Invalid hostname": "Invalid hostname",
"Invalid URL format": "Invalid URL format",
"Protocol": "Protocol",
"Host": "Host",
"Port": "Port",
"Server configuration saved": "Server configuration saved",
"Save Configuration": "Save Configuration",
"Clear Configuration": "Clear Configuration",
"Generated Configuration": "Generated Configuration",
"Upload File": "Upload File",
"Close": "Close",
"Select File": "Select File",
@@ -343,5 +357,8 @@
"Days After": "Days After",
"Expire": "Expire",
"On": "On",
"Off": "Off"
"Off": "Off",
"No valid server configuration detected, please check server configuration": "No valid server configuration detected, please check server configuration",
"Login failed, please check server configuration": "Login failed, please check server configuration",
"Click to modify": "Click to modify"
}
+18 -1
View File
@@ -154,6 +154,20 @@
"Login Failed": "登录失败",
"Login Problems": "登录遇到问题?",
"Get Help": "获取帮助",
"Server Configuration": "服务器配置",
"Server URL": "服务器地址",
"Enter complete server URL (e.g., http://localhost:9000)": "请输入完整的服务器地址(例如:http://localhost:9000)",
"Server URL is required": "服务器地址不能为空",
"Only HTTP and HTTPS protocols are supported": "仅支持 HTTP 和 HTTPS 协议",
"Invalid hostname": "无效的主机名",
"Invalid URL format": "无效的URL格式",
"Protocol": "协议",
"Host": "主机",
"Port": "端口",
"Server configuration saved": "服务器配置已保存",
"Save Configuration": "保存配置",
"Clear Configuration": "清除配置",
"Generated Configuration": "生成的配置",
"Create Bucket": "创建存储桶",
"Creation Date": "创建时间",
"Bucket Not Empty": "当前桶不为空,请先删除桶内容",
@@ -534,5 +548,8 @@
"Please enter valid days": "请输入天数",
"Expire": "过期",
"On": "开启",
"Off": "关闭"
"Off": "关闭",
"No valid server configuration detected, please check server configuration": "未检测到有效服务器配置,请检查服务器配置",
"Login failed, please check server configuration": "登录失败,请检查服务器配置",
"Click to modify": "点击修改"
}
+152 -31
View File
@@ -1,14 +1,14 @@
<script lang="ts" setup>
await setPageLayout('plain')
import { Motion } from "motion-v"
import { ref } from 'vue'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import AuroraBackground from '~/components/ui/aurora-background/AuroraBackground.vue'
import { useRoute, useRouter } from 'vue-router'
import { configManager } from '~/utils/config'
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const method = ref('accessKeyAndSecretKey')
const accessKeyAndSecretKey = ref({
@@ -22,54 +22,123 @@ const sts = ref({
sessionToken: '',
})
// 服务端配置
const serverConfig = ref({
protocol: 'http',
host: 'localhost',
port: '9000',
region: 'us-east-1'
})
const message = useMessage()
const auth = useAuth()
// 控制服务器配置折叠面板展开
const expandedNames = ref<string[]>([])
// 警告提示内容
const configAlert = ref('')
// 检查是否需要默认展开服务器配置
onMounted(() => {
if (route.query.showConfig === '1') {
expandedNames.value = ['server-config']
configAlert.value = t('No valid server configuration detected, please check server configuration')
}
serverUrl.value = `${serverConfig.value.protocol}://${serverConfig.value.host}:${serverConfig.value.port}`
})
const handleLogin = async () => {
// 验证并解析URL
validateAndParseUrl()
if (urlValidationStatus.value === 'error') {
expandedNames.value = ['server-config']
return
}
const credentials = method.value === 'accessKeyAndSecretKey' ? accessKeyAndSecretKey.value : sts.value
try {
// 保存配置到 localStorage
configManager.saveConfig({
protocol: serverConfig.value.protocol,
host: serverConfig.value.host,
port: serverConfig.value.port,
region: serverConfig.value.region
})
message.success(t('Server configuration saved'))
await auth.login(credentials)
message.success(t('Login Success'))
window.location.href = '/'
} catch (error) {
expandedNames.value = ['server-config']
configAlert.value = t('Login failed, please check server configuration')
message.error(t('Login Failed'))
}
}
const serverUrl = ref('')
const urlValidationStatus = ref<'success' | 'warning' | 'error' | undefined>(undefined)
const urlError = ref('')
const validateAndParseUrl = () => {
const url = serverUrl.value.trim()
if (!url) {
urlValidationStatus.value = 'error'
urlError.value = t('Server URL is required')
return
}
try {
const urlObj = new URL(url)
// 验证协议
if (!['http:', 'https:'].includes(urlObj.protocol)) {
urlValidationStatus.value = 'error'
urlError.value = t('Only HTTP and HTTPS protocols are supported')
return
}
// 验证主机
if (!urlObj.hostname) {
urlValidationStatus.value = 'error'
urlError.value = t('Invalid hostname')
return
}
// 解析并更新配置
serverConfig.value.protocol = urlObj.protocol.replace(':', '')
serverConfig.value.host = urlObj.hostname
serverConfig.value.port = urlObj.port || (urlObj.protocol === 'https:' ? '443' : '80')
urlValidationStatus.value = 'success'
urlError.value = ''
} catch (error) {
urlValidationStatus.value = 'error'
urlError.value = t('Invalid URL format')
}
}
const onUrlInput = () => {
// 清除之前的错误状态
if (urlValidationStatus.value === 'error') {
urlValidationStatus.value = undefined
urlError.value = ''
}
}
</script>
<style>
.bg {
background-color: #191919;
opacity: 1;
background-image: linear-gradient(#090909 1.6px, transparent 1.6px),
linear-gradient(to right, #090909 1.6px, #191919 1.6px);
background-size: 32px 32px;
}
</style>
<template>
<div class="lg:p-20 flex flex-col items-center justify-center min-h-screen bg-gray-100 dark:bg-neutral-700">
<AuroraBackground class="absolute inset-0 z-0">
<Motion as="div" :initial="{ opacity: 0, y: 40, filter: 'blur(10px)' }" :in-view="{
opacity: 1,
y: 0,
filter: 'blur(0px)',
}" :transition="{
delay: 0.3,
duration: 0.8,
ease: 'easeInOut',
}" class="relative flex flex-col items-center justify-center gap-4 px-4">
</Motion>
</AuroraBackground>
<div class="flex-1 flex w-full z-10 max-w-7xl lg:max-h-[75vh] shadow-lg rounded-lg overflow-hidden mx-auto dark:bg-neutral-800 dark:border-neutral-700">
<div class="lg:p-20 flex flex-col items-center justify-center min-h-screen bg-gray-100 dark:bg-neutral-800">
<img src="~/assets/backgrounds/scillate.svg" class="absolute inset-0 z-0 opacity-45" alt="" />
<div class="flex-1 flex w-full z-10 max-w-7xl lg:max-h-[85vh] shadow-lg rounded-lg overflow-hidden mx-auto dark:bg-neutral-800 dark:border-neutral-700">
<div class="hidden lg:block w-1/2">
<auth-heros-ripple></auth-heros-ripple>
<auth-heros-static></auth-heros-static>
</div>
<div class="w-full lg:w-1/2 flex flex-col justify-center items-center bg-white dark:bg-neutral-900 dark:border-neutral-700 relative">
<div class="max-w-sm w-full p-4 sm:p-7">
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<img src="https://rustfs.com/rustfs.logo.svg" class="max-w-28" alt="" />
<div class="py-6">
<h1 class="block text-2xl font-bold text-gray-800 dark:text-white">{{ t('Login to RustFS Console') }}</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
@@ -79,6 +148,58 @@ const handleLogin = async () => {
</div>
<div class="mt-5 space-y-4">
<!-- 服务器配置折叠面板 -->
<div class="mb-2">
<div class="transition-all duration-300 ease-in-out">
<!-- 折叠时的卡片 -->
<div v-if="!expandedNames.includes('server-config')" @click="expandedNames = ['server-config']" class="w-full cursor-pointer">
<div
class="rounded border border-gray-200 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 px-4 py-2 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-neutral-700 transition-colors">
<div class="truncate">
<span class="font-mono text-sm">{{ serverConfig.protocol }}://{{ serverConfig.host }}:{{ serverConfig.port }}</span>
<span class="ml-2 text-xs text-gray-500">{{ serverConfig.region }}</span>
</div>
<Icon :size="18" name="ri:arrow-right-s-fill" class="text-gray-600 dark:text-gray-300 hover:text-blue-500 transition-all duration-200" />
</div>
</div>
<!-- 展开时的内容 -->
<div v-else class="border border-gray-200 dark:border-neutral-700 rounded-lg overflow-hidden animate-in fade-in slide-in-from-top-2 duration-300">
<!-- 标题栏 -->
<div @click="expandedNames = []"
class="w-full cursor-pointer flex items-center justify-between px-4 py-3 bg-gray-50 dark:bg-neutral-800 hover:bg-gray-100 dark:hover:bg-neutral-700 transition-colors border-b border-gray-200 dark:border-neutral-700">
<span class="flex items-center text-sm font-medium">
<Icon :size="16" name="ri:settings-3-line" class="mr-2 text-gray-500" />
{{ t('Server Configuration') }}
</span>
<Icon :size="18" name="ri:arrow-down-s-fill" class="text-gray-600 dark:text-gray-300 hover:text-blue-500 transition-all duration-200 transform" />
</div>
<!-- 配置表单 -->
<div class="p-4 bg-white dark:bg-neutral-900">
<div v-if="configAlert"
class="mb-4 px-3 py-2 rounded bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border border-red-200 dark:border-red-800 text-sm">
{{ configAlert }}
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">{{ t('Server URL') }}</label>
<n-input v-model:value="serverUrl" size="medium" type="text" :placeholder="'http://localhost:9000'" :status="urlValidationStatus"
@blur="validateAndParseUrl" @input="onUrlInput" />
<div v-if="urlError" class="mt-1 text-xs text-red-500">{{ urlError }}</div>
</div>
<div>
<label class="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">{{ t('Region') }}</label>
<n-input v-model:value="serverConfig.region" size="medium" type="text" :placeholder="'us-east-1'" />
</div>
</div>
</div>
</div>
</div>
</div>
<n-tabs type="segment" animated size="small" v-model:value="method">
<n-tab-pane name="accessKeyAndSecretKey" :label="t('Key Login')" />
<n-tab-pane name="sts" :label="t('STS Login')" />
+115
View File
@@ -0,0 +1,115 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { configManager } from '~/utils/config'
const { t } = useI18n()
const message = useMessage()
// 服务端配置
const serverConfig = ref({
protocol: 'http',
host: 'localhost',
port: '9000',
region: 'us-east-1'
})
// 加载当前配置
const loadCurrentConfig = () => {
const currentConfig = configManager.loadConfig()
if (currentConfig) {
// 从 baseURL 解析配置
const url = new URL(currentConfig.api.baseURL)
serverConfig.value = {
protocol: url.protocol.replace(':', ''),
host: url.hostname,
port: url.port || (url.protocol === 'https:' ? '443' : '80'),
region: currentConfig.s3.region
}
}
}
// 保存配置
const saveConfig = () => {
configManager.saveConfig(serverConfig.value)
message.success(t('Server configuration saved'))
}
// 清除配置
const clearConfig = () => {
configManager.clearConfig()
message.success('Configuration cleared')
loadCurrentConfig()
}
// 页面加载时读取当前配置
onMounted(() => {
loadCurrentConfig()
})
</script>
<template>
<div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto">
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-lg p-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white mb-6">
{{ t('Server Configuration') }}
</h1>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Protocol') }}
</label>
<n-select v-model:value="serverConfig.protocol" :options="[
{ label: 'HTTP', value: 'http' },
{ label: 'HTTPS', value: 'https' }
]" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Port') }}
</label>
<n-input v-model:value="serverConfig.port" type="text" :placeholder="'9000'" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Host') }}
</label>
<n-input v-model:value="serverConfig.host" type="text" :placeholder="'localhost'" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Region') }}
</label>
<n-input v-model:value="serverConfig.region" type="text" :placeholder="'us-east-1'" />
</div>
<div class="bg-gray-50 dark:bg-neutral-700 p-4 rounded-lg">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Generated Configuration') }}
</h3>
<div class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<div><strong>API URL:</strong> {{ serverConfig.protocol }}://{{ serverConfig.host }}:{{ serverConfig.port }}/rustfs/admin/v3</div>
<div><strong>S3 Endpoint:</strong> {{ serverConfig.protocol }}://{{ serverConfig.host }}:{{ serverConfig.port }}</div>
<div><strong>Region:</strong> {{ serverConfig.region }}</div>
</div>
</div>
<div class="flex gap-4 pt-4">
<n-button type="primary" @click="saveConfig">
{{ t('Save Configuration') }}
</n-button>
<n-button @click="clearConfig">
{{ t('Clear Configuration') }}
</n-button>
</div>
</div>
</div>
</div>
</div>
</template>
+50 -13
View File
@@ -1,29 +1,66 @@
import { configManager } from '~/utils/config'
export default defineNuxtPlugin({
name: 'load-site-config',
async setup(nuxtApp) {
let finalConfig: any
// 客户端:尝试从 public/config.json 动态加载配置
// 优先级:localStorage > public/config.json > runtimeConfig
try {
const response = await fetch('/config.json')
const remoteConfig = await response.json()
// 获取license
// 判断是否是开发环境
if (process.env.NODE_ENV !== 'development') {
const licenseResponse = await fetch('/license')
const licenseConfig = await licenseResponse.json()
remoteConfig.license = licenseConfig
// 1. 首先尝试从 localStorage 读取配置
const localStorageConfig = configManager.loadConfig()
if (localStorageConfig) {
finalConfig = { ...useRuntimeConfig().public, ...localStorageConfig }
console.log('Configuration loaded from localStorage')
}
// 2. 如果 localStorage 没有配置,尝试从 public/config.json 加载
if (!finalConfig) {
const response = await fetch('/config.json')
const remoteConfig = await response.json()
// 获取license
// 判断是否是开发环境
if (process.env.NODE_ENV !== 'development') {
const licenseResponse = await fetch('/license')
const licenseConfig = await licenseResponse.json()
remoteConfig.license = licenseConfig
}
// 合并 public/runtimeConfig 与 remote 配置,remote 的优先
finalConfig = { ...useRuntimeConfig().public, ...remoteConfig }
console.log('Configuration loaded from config.json')
}
// 3. 如果都没有,使用 runtimeConfig
if (!finalConfig) {
finalConfig = useRuntimeConfig().public
console.log('Configuration loaded from runtimeConfig')
}
// 合并 public/runtimeConfig 与 remote 配置,remote 的优先
finalConfig = { ...useRuntimeConfig().public, ...remoteConfig }
} catch (error) {
console.error('Failed to load configuration:', error)
// 如果配置加载失败,重定向到登录页而不是抛出错误
if (process.client) {
const currentRoute = useRoute()
if (currentRoute.path !== '/auth/login') {
await navigateTo('/auth/login')
}
}
// 使用默认配置,避免应用崩溃
finalConfig = useRuntimeConfig().public
}
if (!finalConfig.api.baseURL) {
throw new Error('API URL is not defined in the configuration.')
// 如果仍然没有 baseURL,重定向到登录页
if (process.client) {
const currentRoute = useRoute()
if (currentRoute.path !== '/auth/login') {
await navigateTo('/auth/login?showConfig=1')
}
}
}
return {
+65
View File
@@ -0,0 +1,65 @@
export interface ServerConfig {
protocol: string
host: string
port: string
region: string
}
export interface RustFSConfig {
api: {
baseURL: string
}
s3: {
endpoint: string
region: string
}
}
const CONFIG_KEY = 'rustfs-config'
export const configManager = {
// 保存配置到 localStorage
saveConfig(serverConfig: ServerConfig): void {
if (process.client) {
const config: RustFSConfig = {
api: {
baseURL: `${serverConfig.protocol}://${serverConfig.host}:${serverConfig.port}/rustfs/admin/v3`
},
s3: {
endpoint: `${serverConfig.protocol}://${serverConfig.host}:${serverConfig.port}`,
region: serverConfig.region
}
}
localStorage.setItem(CONFIG_KEY, JSON.stringify(config))
}
},
// 从 localStorage 读取配置
loadConfig(): RustFSConfig | null {
if (process.client) {
try {
const configStr = localStorage.getItem(CONFIG_KEY)
if (configStr) {
return JSON.parse(configStr)
}
} catch (error) {
console.warn('Failed to parse localStorage config:', error)
}
}
return null
},
// 清除配置
clearConfig(): void {
if (process.client) {
localStorage.removeItem(CONFIG_KEY)
}
},
// 检查是否有有效配置
hasValidConfig(): boolean {
const config = this.loadConfig()
return !!(config?.api?.baseURL)
}
}