542 lines
18 KiB
Vue
542 lines
18 KiB
Vue
<template>
|
||
<div class="settings-page">
|
||
<div class="page-header">
|
||
<h1 class="page-title">{{ t('Settings') }}</h1>
|
||
</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="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>
|
||
<template #footer>
|
||
<n-space justify="start">
|
||
<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>
|
||
</template>
|
||
</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>
|
||
<template #footer>
|
||
<n-space justify="start">
|
||
<n-button type="default" @click="() => loadEnvironmentConfig()" :loading="envConfigLoading">
|
||
<template #icon>
|
||
<n-icon><Icon icon="tabler:refresh" /></n-icon>
|
||
</template>
|
||
{{ t('Refresh') }}
|
||
</n-button>
|
||
<n-button type="warning" :loading="envConfigRestarting" @click="handleRestartEnvironment">
|
||
<template #icon>
|
||
<n-icon><Icon icon="ix:restart" /></n-icon>
|
||
</template>
|
||
{{ t('Restart Environment') }}
|
||
</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>
|
||
</template>
|
||
</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,
|
||
useDialog
|
||
} 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, restartEnvironment, type EnvironmentConfig } from '../../shared/api/system'
|
||
|
||
const message = useMessage()
|
||
const dialog = useDialog()
|
||
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 envConfigRestarting = ref(false)
|
||
|
||
const systemSettings = reactive({
|
||
appName: localStorage.getItem('appName') || 'Jingrow',
|
||
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/Hong_Kong (中国香港时间)', value: 'Asia/Hong_Kong' },
|
||
{ 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
|
||
}
|
||
}
|
||
|
||
// 重启环境
|
||
const handleRestartEnvironment = () => {
|
||
if (!isAdmin.value) {
|
||
message.error(t('Only system administrators can restart environment'))
|
||
return
|
||
}
|
||
|
||
// 确认对话框
|
||
dialog.warning({
|
||
title: t('Restart Environment'),
|
||
content: t('Are you sure you want to restart the environment? This operation may cause service interruption.'),
|
||
positiveText: t('Restart'),
|
||
negativeText: t('Cancel'),
|
||
onPositiveClick: async () => {
|
||
envConfigRestarting.value = true
|
||
try {
|
||
const result = await restartEnvironment()
|
||
if (result.success) {
|
||
message.success(result.message || t('Environment restart request submitted. The system will restart shortly.'))
|
||
} else {
|
||
message.error(result.message || t('Failed to restart environment'))
|
||
}
|
||
} catch (error: any) {
|
||
message.error(error.message || t('Failed to restart environment'))
|
||
} finally {
|
||
envConfigRestarting.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
onMounted(async () => {
|
||
initLocale()
|
||
systemSettings.language = getCurrentLocale()
|
||
|
||
// 如果是系统管理员,加载环境配置(静默加载,不显示消息)
|
||
if (isAdmin.value) {
|
||
await loadEnvironmentConfig(false)
|
||
}
|
||
})
|
||
</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>
|