2025-11-02 01:01:37 +08:00

520 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="settings-page">
<div class="page-header">
<n-space justify="space-between" align="center">
<div>
<h1 class="page-title">{{ t('Settings') }}</h1>
</div>
<n-space v-if="isAdmin">
<n-button type="default" @click="() => loadEnvironmentConfig()" :loading="envConfigLoading">
<template #icon>
<n-icon><Icon icon="tabler:refresh" /></n-icon>
</template>
</n-button>
<n-button type="primary" class="save-btn-brand" :loading="envConfigSaving" @click="saveEnvironmentConfig">
<template #icon>
<n-icon><Icon icon="tabler:check" /></n-icon>
</template>
{{ t('Save') }}
</n-button>
</n-space>
<n-space v-else>
<n-button type="primary" class="save-btn-brand" @click="saveSystemSettings">
<template #icon>
<n-icon><Icon icon="tabler:check" /></n-icon>
</template>
{{ t('Save') }}
</n-button>
</n-space>
</n-space>
</div>
<n-grid :cols="2" :x-gap="24" :y-gap="24">
<!-- 左栏系统设置 -->
<n-grid-item>
<n-card :title="t('System Settings')">
<n-form :model="systemSettings" label-placement="left" label-width="120px">
<n-form-item :label="t('App Name')">
<n-input v-model:value="systemSettings.appName" :placeholder="t('Enter app name')" />
</n-form-item>
<n-form-item label="Jingrow API URL">
<n-input v-model:value="systemSettings.jingrowApiUrl" />
</n-form-item>
<n-form-item :label="t('API Key')">
<n-input v-model:value="systemSettings.apiKey" type="password" show-password-on="click" />
</n-form-item>
<n-form-item :label="t('API Secret')">
<n-input v-model:value="systemSettings.apiSecret" type="password" show-password-on="click" />
</n-form-item>
<n-form-item :label="t('Timeout')">
<n-input-number v-model:value="systemSettings.timeout" :min="5" :max="300" />
</n-form-item>
<n-form-item :label="t('Interface Language')">
<n-select
v-model:value="systemSettings.language"
:options="languageOptions"
style="width: 200px"
@update:value="changeLanguage"
/>
</n-form-item>
<n-form-item :label="t('Items Per Page')">
<n-select
v-model:value="systemSettings.itemsPerPage"
:options="pageSizeOptions"
style="width: 120px"
/>
</n-form-item>
<n-form-item :label="t('Timezone')">
<n-select
v-model:value="systemSettings.timezone"
:options="timezoneOptions"
style="width: 250px"
filterable
:placeholder="t('Select timezone')"
/>
</n-form-item>
</n-form>
</n-card>
</n-grid-item>
<!-- 右栏环境配置仅系统管理员可见 -->
<n-grid-item v-if="isAdmin">
<n-card :title="t('Environment Configuration')">
<n-alert type="warning" style="margin-bottom: 16px">
{{ t('Only system administrators can view and edit environment configuration') }}
</n-alert>
<n-form
:model="envConfig"
label-placement="left"
label-width="180px"
:loading="envConfigLoading"
>
<n-collapse>
<n-collapse-item name="jingrow" :title="t('Jingrow API Configuration')">
<n-form-item :label="t('Jingrow Server URL')">
<n-input v-model:value="envConfig.jingrow_server_url" placeholder="https://example.jingrow.com" />
</n-form-item>
<n-form-item :label="t('Jingrow API Key')">
<n-input v-model:value="envConfig.jingrow_api_key" type="password" show-password-on="click" />
</n-form-item>
<n-form-item :label="t('Jingrow API Secret')">
<n-input v-model:value="envConfig.jingrow_api_secret" type="password" show-password-on="click" />
</n-form-item>
</n-collapse-item>
<n-collapse-item name="cloud" :title="t('Jingrow Cloud Configuration')">
<n-form-item :label="t('Cloud URL')">
<n-input v-model:value="envConfig.jingrow_cloud_url" />
</n-form-item>
<n-form-item :label="t('Cloud API URL')">
<n-input v-model:value="envConfig.jingrow_cloud_api_url" />
</n-form-item>
<n-form-item :label="t('Cloud API Key')">
<n-input v-model:value="envConfig.jingrow_cloud_api_key" type="password" show-password-on="click" />
</n-form-item>
<n-form-item :label="t('Cloud API Secret')">
<n-input v-model:value="envConfig.jingrow_cloud_api_secret" type="password" show-password-on="click" />
</n-form-item>
</n-collapse-item>
<n-collapse-item v-if="isLocalMode" name="database" :title="t('Database Configuration')">
<n-form-item :label="t('DB Host')">
<n-input v-model:value="envConfig.jingrow_db_host" />
</n-form-item>
<n-form-item :label="t('DB Port')">
<n-input v-model:value="envConfig.jingrow_db_port" />
</n-form-item>
<n-form-item :label="t('DB Name')">
<n-input v-model:value="envConfig.jingrow_db_name" />
</n-form-item>
<n-form-item :label="t('DB User')">
<n-input v-model:value="envConfig.jingrow_db_user" />
</n-form-item>
<n-form-item :label="t('DB Password')">
<n-input v-model:value="envConfig.jingrow_db_password" type="password" show-password-on="click" />
</n-form-item>
<n-form-item :label="t('DB Type')">
<n-select v-model:value="envConfig.jingrow_db_type" :options="dbTypeOptions" style="width: 200px" />
</n-form-item>
</n-collapse-item>
<n-collapse-item name="backend" :title="t('Backend Configuration')">
<n-form-item :label="t('Backend Host')">
<n-input v-model:value="envConfig.backend_host" />
</n-form-item>
<n-form-item :label="t('Backend Port')">
<n-input-number v-model:value="envConfig.backend_port" :min="1" :max="65535" />
</n-form-item>
<n-form-item :label="t('Backend Reload')">
<n-switch v-model:value="envConfig.backend_reload" />
</n-form-item>
</n-collapse-item>
<n-collapse-item name="dramatiq" :title="t('Dramatiq')">
<n-form-item :label="t('Worker Processes')">
<n-input-number v-model:value="envConfig.worker_processes" :min="1" :max="32" />
</n-form-item>
<n-form-item :label="t('Worker Threads')">
<n-input-number v-model:value="envConfig.worker_threads" :min="1" :max="32" />
</n-form-item>
<n-form-item :label="t('Watch')">
<n-switch v-model:value="envConfig.watch" />
</n-form-item>
</n-collapse-item>
<n-collapse-item name="qdrant" :title="t('Qdrant Configuration')">
<n-form-item :label="t('Qdrant Host')">
<n-input v-model:value="envConfig.qdrant_host" />
</n-form-item>
<n-form-item :label="t('Qdrant Port')">
<n-input-number v-model:value="envConfig.qdrant_port" :min="1" :max="65535" />
</n-form-item>
</n-collapse-item>
<n-collapse-item name="runtime" :title="t('Other')">
<n-form-item :label="t('Run Mode')">
<n-select v-model:value="envConfig.run_mode" :options="runModeOptions" style="width: 200px" />
</n-form-item>
<n-form-item :label="t('Environment')">
<n-select v-model:value="envConfig.environment" :options="environmentOptions" style="width: 200px" />
</n-form-item>
<n-form-item :label="t('Log Level')">
<n-select v-model:value="envConfig.log_level" :options="logLevelOptions" style="width: 200px" />
</n-form-item>
</n-collapse-item>
</n-collapse>
</n-form>
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted, computed, ref } from 'vue'
import {
NGrid,
NGridItem,
NCard,
NForm,
NFormItem,
NInput,
NButton,
NInputNumber,
NSelect,
NSwitch,
NAlert,
NCollapse,
NCollapseItem,
NSpace,
NIcon,
useMessage
} from 'naive-ui'
import { Icon } from '@iconify/vue'
import { getCurrentLocale, setLocale, locales, initLocale, t } from '../shared/i18n'
import { useAuthStore } from '../shared/stores/auth'
import { getEnvironmentConfig, updateEnvironmentConfig, type EnvironmentConfig } from '../shared/api/system'
const message = useMessage()
const authStore = useAuthStore()
// 检查是否为系统管理员
const isAdmin = computed(() => {
const user = authStore.user
return user?.username === 'Administrator' || user?.id === 'Administrator'
})
// 检查是否为 local 运行模式
const isLocalMode = computed(() => {
return envConfig.run_mode === 'local'
})
// 环境配置
const envConfig = reactive<Partial<EnvironmentConfig>>({})
const envConfigLoading = ref(false)
const envConfigSaving = ref(false)
const systemSettings = reactive({
appName: localStorage.getItem('appName') || 'Jingrow',
jingrowApiUrl: (import.meta as any).env.VITE_JINGROW_SERVER_URL || '',
apiKey: (import.meta as any).env.VITE_JINGROW_API_KEY || '',
apiSecret: (import.meta as any).env.VITE_JINGROW_API_SECRET || '',
timeout: 30,
language: getCurrentLocale(),
itemsPerPage: parseInt(localStorage.getItem('itemsPerPage') || '10'),
timezone: localStorage.getItem('timezone') || 'Asia/Shanghai'
})
// 语言选项
const languageOptions = locales.map(locale => ({
label: `${locale.flag} ${locale.name}`,
value: locale.code
}))
// 每页数量选项
const pageSizeOptions = [
{ label: '10', value: 10 },
{ label: '20', value: 20 },
{ label: '50', value: 50 },
{ label: '100', value: 100 }
]
// 时区选项
const timezoneOptions = [
{ label: 'Asia/Shanghai (中国标准时间)', value: 'Asia/Shanghai' },
{ label: 'Asia/Tokyo (日本标准时间)', value: 'Asia/Tokyo' },
{ label: 'Asia/Seoul (韩国标准时间)', value: 'Asia/Seoul' },
{ label: 'Asia/Hong_Kong (香港时间)', value: 'Asia/Hong_Kong' },
{ label: 'Asia/Taipei (台北时间)', value: 'Asia/Taipei' },
{ label: 'Asia/Singapore (新加坡时间)', value: 'Asia/Singapore' },
{ label: 'UTC (协调世界时)', value: 'UTC' },
{ label: 'America/New_York (美国东部时间)', value: 'America/New_York' },
{ label: 'America/Los_Angeles (美国西部时间)', value: 'America/Los_Angeles' },
{ label: 'Europe/London (英国时间)', value: 'Europe/London' },
{ label: 'Europe/Paris (法国时间)', value: 'Europe/Paris' },
{ label: 'Europe/Berlin (德国时间)', value: 'Europe/Berlin' },
{ label: 'Australia/Sydney (澳大利亚东部时间)', value: 'Australia/Sydney' },
{ label: 'Pacific/Auckland (新西兰时间)', value: 'Pacific/Auckland' }
]
// 数据库类型选项
const dbTypeOptions = [
{ label: 'MariaDB', value: 'mariadb' },
{ label: 'MySQL', value: 'mysql' },
{ label: 'PostgreSQL', value: 'postgresql' }
]
// 运行模式选项
const runModeOptions = [
{ label: 'API', value: 'api' },
{ label: 'Local', value: 'local' }
]
// 环境选项
const environmentOptions = [
{ label: 'Development', value: 'development' },
{ label: 'Production', value: 'production' }
]
// 日志级别选项
const logLevelOptions = [
{ label: 'DEBUG', value: 'DEBUG' },
{ label: 'INFO', value: 'INFO' },
{ label: 'WARNING', value: 'WARNING' },
{ label: 'ERROR', value: 'ERROR' },
{ label: 'CRITICAL', value: 'CRITICAL' }
]
const changeLanguage = (locale: string) => {
setLocale(locale)
message.success(t('Language updated'))
}
const saveSystemSettings = () => {
// 保存应用名称
localStorage.setItem('appName', systemSettings.appName)
// 保存每页数量设置
localStorage.setItem('itemsPerPage', systemSettings.itemsPerPage.toString())
// 保存时区设置
localStorage.setItem('timezone', systemSettings.timezone)
message.success(t('System settings saved'))
// 保存成功后自动刷新页面,让新设置生效
setTimeout(() => {
window.location.reload()
}, 1000)
}
// 加载环境配置
const loadEnvironmentConfig = async (showMessage = true) => {
if (!isAdmin.value) {
return
}
envConfigLoading.value = true
try {
const result = await getEnvironmentConfig()
if (result.success && result.data) {
Object.assign(envConfig, result.data)
if (showMessage) {
message.success(t('Environment configuration loaded'))
}
} else {
if (showMessage) {
message.error(result.message || t('Failed to load environment configuration'))
}
}
} catch (error: any) {
if (showMessage) {
message.error(error.message || t('Failed to load environment configuration'))
}
} finally {
envConfigLoading.value = false
}
}
// 保存环境配置
const saveEnvironmentConfig = async () => {
if (!isAdmin.value) {
message.error(t('Only system administrators can edit environment configuration'))
return
}
envConfigSaving.value = true
try {
const result = await updateEnvironmentConfig(envConfig)
if (result.success) {
message.success(result.message || t('Environment configuration saved'))
// 重新加载配置以获取最新值(静默加载,不显示消息)
await loadEnvironmentConfig(false)
} else {
message.error(result.message || t('Failed to save environment configuration'))
}
} catch (error: any) {
message.error(error.message || t('Failed to save environment configuration'))
} finally {
envConfigSaving.value = false
}
}
onMounted(async () => {
initLocale()
systemSettings.language = getCurrentLocale()
// 如果是系统管理员,加载环境配置
if (isAdmin.value) {
await loadEnvironmentConfig()
}
})
</script>
<style scoped>
.settings-page {
width: 100%;
padding: 0 16px;
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-size: 28px;
font-weight: 700;
color: #1f2937;
margin: 0;
}
/* 保存按钮 - 使用柔和的品牌色系,与 pagetype 详情页保存按钮一致 */
.save-btn-brand {
background: #e6f8f0 !important;
border: 1px solid #1fc76f !important;
color: #0d684b !important;
}
.save-btn-brand :deep(.n-button__border) {
border: none !important;
border-color: transparent !important;
}
.save-btn-brand :deep(.n-button__state-border) {
border: none !important;
border-color: transparent !important;
}
.save-btn-brand:hover {
background: #dcfce7 !important;
border-color: #1fc76f !important;
border: 1px solid #1fc76f !important;
color: #166534 !important;
box-shadow: 0 2px 8px rgba(31, 199, 111, 0.15) !important;
}
.save-btn-brand:hover :deep(.n-button__border),
.save-btn-brand:hover :deep(.n-button__state-border) {
border: none !important;
border-color: transparent !important;
}
.save-btn-brand:focus {
background: #dcfce7 !important;
border-color: #1fc76f !important;
border: 1px solid #1fc76f !important;
color: #166534 !important;
box-shadow: 0 0 0 2px rgba(31, 199, 111, 0.2) !important;
}
.save-btn-brand:focus :deep(.n-button__border),
.save-btn-brand:focus :deep(.n-button__state-border) {
border: none !important;
border-color: transparent !important;
}
.save-btn-brand:active {
background: #1fc76f !important;
border-color: #1fc76f !important;
border: 1px solid #1fc76f !important;
color: white !important;
box-shadow: 0 1px 4px rgba(31, 199, 111, 0.2) !important;
}
.save-btn-brand:active :deep(.n-button__border),
.save-btn-brand:active :deep(.n-button__state-border) {
border-color: transparent !important;
}
.save-btn-brand:disabled {
background: #f1f5f9 !important;
border: 1px solid #e2e8f0 !important;
border-color: #e2e8f0 !important;
color: #94a3b8 !important;
opacity: 0.6 !important;
cursor: not-allowed !important;
}
.save-btn-brand:disabled :deep(.n-button__border),
.save-btn-brand:disabled :deep(.n-button__state-border) {
border: none !important;
border-color: transparent !important;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.settings-page :deep(.n-grid) {
grid-template-columns: 1fr !important;
}
}
@media (max-width: 768px) {
.settings-page {
padding: 0 12px;
}
.page-header {
margin-bottom: 16px;
}
.page-title {
font-size: 24px;
}
.page-description {
font-size: 14px;
}
}
@media (max-width: 480px) {
.settings-page {
padding: 0 8px;
}
.page-title {
font-size: 20px;
}
.page-description {
font-size: 13px;
}
}
</style>