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
+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>