542 lines
18 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">
<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>