kra-new/web/src/view/systemTools/system/system.vue

468 lines
29 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="kra-table-box config-layout">
<div class="flex items-center justify-between mb-5">
<div>
<h2 class="text-xl font-semibold">系统配置</h2>
<p class="text-sm text-gray-500 mt-1">管理 Kra 后台运行参数和邮件服务</p>
</div>
<div class="flex gap-2">
<el-button @click="reload">重载服务</el-button>
<el-button type="primary" :loading="saving" @click="update">保存配置</el-button>
</div>
</div>
<el-tabs v-model="activeKey">
<el-tab-pane label="基础设置" name="basic">
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
<el-form-item label="路由前缀">
<el-input v-model.trim="config.admin.routerPrefix" placeholder="例如 /api留空表示无前缀" />
</el-form-item>
<el-form-item label="服务端口">
<el-input-number v-model="config.admin.system.addr" :min="1" :max="65535" class="!w-full" />
</el-form-item>
<el-form-item label="IP 限流次数">
<el-input-number v-model="config.admin.system.iplimitCount" :min="0" class="!w-full" />
</el-form-item>
<el-form-item label="IP 限流窗口(秒)">
<el-input-number v-model="config.admin.system.iplimitTime" :min="0" class="!w-full" />
</el-form-item>
<el-form-item label="JWT 签发者">
<el-input v-model.trim="config.admin.jwt.issuer" />
</el-form-item>
<el-form-item label="JWT 签名密钥">
<el-input v-model="config.admin.jwt.signingKey" show-password>
<template #append>
<el-button @click="generateSigningKey">生成</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="JWT 有效期">
<el-input v-model.trim="config.admin.jwt.expiresTime" placeholder="例如 168h" />
</el-form-item>
<el-form-item label="JWT 缓冲期">
<el-input v-model.trim="config.admin.jwt.bufferTime" placeholder="例如 24h" />
</el-form-item>
<el-form-item label="验证码长度">
<el-input-number v-model="config.admin.captcha.keyLong" :min="1" :max="12" class="!w-full" />
</el-form-item>
<el-form-item label="验证码宽度">
<el-input-number v-model="config.admin.captcha.imgWidth" :min="40" class="!w-full" />
</el-form-item>
<el-form-item label="验证码高度">
<el-input-number v-model="config.admin.captcha.imgHeight" :min="20" class="!w-full" />
</el-form-item>
<el-form-item label="验证码缓存时间">
<el-input v-model.trim="config.admin.captcha.storeExpiration" placeholder="例如 3m" />
</el-form-item>
<el-form-item label="本地文件目录">
<el-input v-model.trim="config.admin.local.storePath" />
</el-form-item>
<el-form-item label="文件访问前缀">
<el-input v-model.trim="config.admin.local.pathPrefix" />
</el-form-item>
<el-form-item label="大文件上传会话有效期(小时)">
<el-input-number v-model="config.admin.media.sessionTtl" :min="1" :max="720" class="!w-full" />
</el-form-item>
<el-form-item label="单文件最大字节数0 为不限)">
<el-input-number v-model="config.admin.media.maxFileSize" :min="0" class="!w-full" />
</el-form-item>
<el-form-item label="分片目录">
<el-input v-model.trim="config.admin.media.chunkDir" />
</el-form-item>
<el-form-item label="运行开关" class="md:col-span-2">
<div class="flex flex-wrap gap-5">
<el-switch v-model="config.admin.system.useRedis" active-text="启用 Redis" />
<el-switch v-model="config.admin.system.useMultipoint" active-text="限制多点登录" />
<el-switch v-model="config.admin.system.useStrictAuth" active-text="严格角色树" />
<el-switch v-model="config.admin.system.disableAutoMigrate" active-text="禁用自动迁移" />
</div>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="数据库" name="database">
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
<el-form-item label="数据库类型">
<el-select v-model="config.data.database.driver" class="!w-full">
<el-option label="MySQL" value="mysql" /><el-option label="PostgreSQL" value="pgsql" />
<el-option label="SQL Server" value="mssql" /><el-option label="SQLite" value="sqlite" />
<el-option label="Oracle" value="oracle" />
</el-select>
</el-form-item>
<el-form-item label="主机"><el-input v-model.trim="config.data.database.host" /></el-form-item>
<el-form-item label="端口"><el-input v-model.trim="config.data.database.port" /></el-form-item>
<el-form-item label="用户名"><el-input v-model.trim="config.data.database.user" /></el-form-item>
<el-form-item label="密码"><el-input v-model="config.data.database.password" show-password /></el-form-item>
<el-form-item label="数据库名"><el-input v-model.trim="config.data.database.name" /></el-form-item>
<el-form-item label="SQLite 目录"><el-input v-model.trim="config.data.database.path" /></el-form-item>
<el-form-item label="连接参数"><el-input v-model.trim="config.data.database.config" /></el-form-item>
<el-form-item label="表前缀"><el-input v-model.trim="config.data.database.prefix" /></el-form-item>
<el-form-item label="存储引擎"><el-input v-model.trim="config.data.database.engine" placeholder="InnoDB" /></el-form-item>
<el-form-item label="GORM 日志级别"><el-select v-model="config.data.database.log_mode" class="!w-full"><el-option label="Silent" value="silent" /><el-option label="Error" value="error" /><el-option label="Warn" value="warn" /><el-option label="Info" value="info" /></el-select></el-form-item>
<el-form-item label="最大空闲连接"><el-input-number v-model="config.data.database.max_idle_conns" :min="0" class="!w-full" /></el-form-item>
<el-form-item label="最大打开连接"><el-input-number v-model="config.data.database.max_open_conns" :min="0" class="!w-full" /></el-form-item>
<el-form-item label="连接最长复用(秒)"><el-input-number v-model="config.data.database.conn_max_lifetime" :min="0" class="!w-full" /></el-form-item>
<el-form-item label="表名选项"><el-switch v-model="config.data.database.singular" active-text="使用单数表名" /></el-form-item>
<p class="md:col-span-2 text-sm text-gray-500">保存后点击“重载服务”,数据库连接会按新配置重建。</p>
</el-form>
</el-tab-pane>
<el-tab-pane label="Redis" name="redis">
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
<el-form-item label="网络"><el-input v-model.trim="config.data.redis.network" placeholder="tcp" /></el-form-item>
<el-form-item label="地址"><el-input v-model.trim="config.data.redis.addr" placeholder="127.0.0.1:6379留空关闭 Redis" /></el-form-item>
<el-form-item label="实例名"><el-input v-model.trim="config.data.redis.name" /></el-form-item>
<el-form-item label="密码"><el-input v-model="config.data.redis.password" show-password /></el-form-item>
<el-form-item label="数据库"><el-input-number v-model="config.data.redis.db" :min="0" class="!w-full" /></el-form-item>
<el-form-item label="集群模式"><el-switch v-model="config.data.redis.use_cluster" /></el-form-item>
<el-form-item v-if="config.data.redis.use_cluster" label="集群节点(每行一个)" class="md:col-span-2"><el-input v-model="redisClusterText" type="textarea" :rows="4" /></el-form-item>
<el-form-item label="读取超时"><el-input v-model.trim="config.data.redis.read_timeout" placeholder="例如 200ms" /></el-form-item>
<el-form-item label="写入超时"><el-input v-model.trim="config.data.redis.write_timeout" placeholder="例如 200ms" /></el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="MongoDB / 多数据库" name="advancedDatabase">
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
<el-form-item label="MongoDB 开关"><el-switch v-model="config.admin.system.useMongo" active-text="启用 MongoDB" /></el-form-item>
<el-form-item label="数据库名"><el-input v-model.trim="config.data.mongo.database" /></el-form-item>
<el-form-item label="默认集合"><el-input v-model.trim="config.data.mongo.coll" /></el-form-item>
<el-form-item label="连接选项"><el-input v-model.trim="config.data.mongo.options" /></el-form-item>
<el-form-item label="用户名"><el-input v-model.trim="config.data.mongo.username" /></el-form-item>
<el-form-item label="密码"><el-input v-model="config.data.mongo.password" show-password /></el-form-item>
<el-form-item label="认证库"><el-input v-model.trim="config.data.mongo.auth_source" /></el-form-item>
<el-form-item label="最小连接池"><el-input-number v-model="config.data.mongo.min_pool_size" :min="0" class="!w-full" /></el-form-item>
<el-form-item label="最大连接池"><el-input-number v-model="config.data.mongo.max_pool_size" :min="0" class="!w-full" /></el-form-item>
<el-form-item label="连接超时(毫秒)"><el-input-number v-model="config.data.mongo.connect_timeout_ms" :min="0" class="!w-full" /></el-form-item>
<el-form-item label="Socket 超时(毫秒)"><el-input-number v-model="config.data.mongo.socket_timeout_ms" :min="0" class="!w-full" /></el-form-item>
<el-form-item label="使用独立 Mongo 日志"><el-switch v-model="config.data.mongo.is_zap" /></el-form-item>
<el-form-item label="MongoDB 节点 JSON" class="md:col-span-2"><el-input v-model="mongoHostsText" type="textarea" :rows="4" placeholder='[{"host":"127.0.0.1","port":"27017"}]' /></el-form-item>
<el-form-item label="多数据库配置 JSON" class="md:col-span-2"><el-input v-model="databaseListText" type="textarea" :rows="6" /></el-form-item>
<el-form-item label="多 Redis 配置 JSON" class="md:col-span-2"><el-input v-model="redisListText" type="textarea" :rows="6" /></el-form-item>
<el-form-item label="磁盘挂载点 JSON" class="md:col-span-2"><el-input v-model="diskListText" type="textarea" :rows="3" placeholder='[{"mount_point":"/"}]' /></el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="日志" name="logging">
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
<el-form-item label="日志级别"><el-select v-model="config.admin.zap.level" class="!w-full"><el-option label="Debug" value="debug" /><el-option label="Info" value="info" /><el-option label="Warn" value="warn" /><el-option label="Error" value="error" /></el-select></el-form-item>
<el-form-item label="格式"><el-select v-model="config.admin.zap.format" class="!w-full"><el-option label="JSON" value="json" /><el-option label="Console" value="console" /></el-select></el-form-item>
<el-form-item label="日志目录"><el-input v-model.trim="config.admin.zap.director" /></el-form-item>
<el-form-item label="日志前缀"><el-input v-model="config.admin.zap.prefix" /></el-form-item>
<el-form-item label="级别编码器"><el-select v-model="config.admin.zap.encode_level" class="!w-full"><el-option label="lowercase" value="LowercaseLevelEncoder" /><el-option label="lowercase color" value="LowercaseColorLevelEncoder" /><el-option label="capital" value="CapitalLevelEncoder" /><el-option label="capital color" value="CapitalColorLevelEncoder" /></el-select></el-form-item>
<el-form-item label="堆栈字段"><el-input v-model.trim="config.admin.zap.stacktrace_key" /></el-form-item>
<el-form-item label="保留天数"><el-input-number v-model="config.admin.zap.retention_day" :min="0" class="!w-full" /></el-form-item>
<el-form-item label="访问日志最大字节"><el-input-number v-model="config.admin.zap.access_log_max_bytes" :min="1024" class="!w-full" /></el-form-item>
<el-form-item label="仅写文件的模块(每行一个)"><el-input v-model="fileOnlyModulesText" type="textarea" :rows="3" /></el-form-item>
<el-form-item label="输出选项" class="md:col-span-2"><div class="flex flex-wrap gap-5"><el-switch v-model="config.admin.zap.log_in_console" active-text="控制台" /><el-switch v-model="config.admin.zap.show_line" active-text="调用行" /><el-switch v-model="config.admin.zap.access_req_body" active-text="请求体" /><el-switch v-model="config.admin.zap.access_resp_data" active-text="响应体" /><el-switch v-model="config.admin.zap.access_req_headers" active-text="请求头" /></div></el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="应用 / CORS" name="application">
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
<el-form-item label="节点标识"><el-input v-model.trim="config.admin.app.node" /></el-form-item>
<el-form-item label="应用 ID"><el-input v-model.trim="config.admin.app.app_id" /></el-form-item>
<el-form-item label="运行环境"><el-input v-model.trim="config.admin.app.env" /></el-form-item>
<el-form-item label="CORS 模式"><el-select v-model="config.admin.cors.mode" class="!w-full"><el-option label="白名单" value="whitelist" /><el-option label="严格白名单" value="strict-whitelist" /><el-option label="全部放行" value="allow-all" /></el-select></el-form-item>
<el-form-item label="CORS 白名单 JSON" class="md:col-span-2"><el-input v-model="corsRulesText" type="textarea" :rows="7" /></el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="对象存储" name="storage">
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
<el-form-item label="存储类型" class="md:col-span-2">
<el-select v-model="config.admin.storage.type" class="!w-full">
<el-option v-for="item in storageTypes" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<template v-if="config.admin.storage.type === 'qiniu'">
<el-form-item label="区域"><el-input v-model.trim="config.admin.storage.qiniu.zone" placeholder="ZoneHuadong" /></el-form-item>
<el-form-item label="Bucket"><el-input v-model.trim="config.admin.storage.qiniu.bucket" /></el-form-item>
<el-form-item label="访问域名"><el-input v-model.trim="config.admin.storage.qiniu.base_url" /></el-form-item>
<el-form-item label="Access Key"><el-input v-model.trim="config.admin.storage.qiniu.access_key" /></el-form-item>
<el-form-item label="Secret Key"><el-input v-model="config.admin.storage.qiniu.secret_key" show-password /></el-form-item>
<el-form-item label="连接选项"><div class="flex gap-5"><el-switch v-model="config.admin.storage.qiniu.use_https" active-text="HTTPS" /><el-switch v-model="config.admin.storage.qiniu.use_cdn_domains" active-text="CDN 域名" /></div></el-form-item>
</template>
<template v-else-if="currentObjectStorage">
<el-form-item label="Endpoint"><el-input v-model.trim="currentObjectStorage.endpoint" placeholder="可留空使用提供商默认地址" /></el-form-item>
<el-form-item label="Region"><el-input v-model.trim="currentObjectStorage.region" /></el-form-item>
<el-form-item label="Bucket"><el-input v-model.trim="currentObjectStorage.bucket" /></el-form-item>
<el-form-item label="访问域名"><el-input v-model.trim="currentObjectStorage.base_url" /></el-form-item>
<el-form-item label="Access Key"><el-input v-model.trim="currentObjectStorage.access_key" /></el-form-item>
<el-form-item label="Secret Key"><el-input v-model="currentObjectStorage.secret_key" show-password /></el-form-item>
<el-form-item label="对象前缀"><el-input v-model.trim="currentObjectStorage.path_prefix" /></el-form-item>
<el-form-item v-if="config.admin.storage.type === 'cloudflare-r2'" label="Account ID"><el-input v-model.trim="currentObjectStorage.account_id" /></el-form-item>
<el-form-item label="连接选项"><div class="flex gap-5"><el-switch v-model="currentObjectStorage.use_ssl" active-text="HTTPS" /><el-switch v-model="currentObjectStorage.force_path_style" active-text="Path Style" /></div></el-form-item>
</template>
<div class="md:col-span-2 flex justify-end">
<el-button type="primary" :loading="storageSaving" @click="saveStorage">保存对象存储</el-button>
</div>
</el-form>
</el-tab-pane>
<el-tab-pane label="邮件设置" name="email">
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
<el-form-item label="默认收件人">
<el-input v-model.trim="config.email.to" placeholder="多个地址用英文逗号分隔" />
</el-form-item>
<el-form-item label="发件人">
<el-input v-model.trim="config.email.from" />
</el-form-item>
<el-form-item label="SMTP 主机">
<el-input v-model.trim="config.email.host" />
</el-form-item>
<el-form-item label="SMTP 端口">
<el-input-number v-model="config.email.port" :min="1" :max="65535" class="!w-full" />
</el-form-item>
<el-form-item label="SMTP 密钥">
<el-input v-model="config.email.secret" show-password />
</el-form-item>
<el-form-item label="发件人昵称">
<el-input v-model.trim="config.email.nickname" />
</el-form-item>
<el-form-item label="连接方式">
<div class="flex gap-5">
<el-switch v-model="config.email['is-ssl']" active-text="SSL" />
<el-switch v-model="config.email['is-loginauth']" active-text="LOGIN 认证" />
</div>
</el-form-item>
<el-form-item label="配置操作" class="md:col-span-2">
<div class="flex gap-2">
<el-button type="primary" :loading="emailSaving" @click="saveEmail">保存邮件配置</el-button>
<el-button :loading="testing" @click="testEmail">保存并发送测试邮件</el-button>
</div>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getSystemConfig, reloadSystem, setEmailConfig, setStorageConfig, setSystemConfig } from '@/api/system'
import { emailTest } from '@/api/email'
import { CreateUUID } from '@/utils/format'
defineOptions({ name: 'Config' })
const activeKey = ref('basic')
const saving = ref(false)
const storageSaving = ref(false)
const emailSaving = ref(false)
const testing = ref(false)
const config = ref({
admin: {
routerPrefix: '',
jwt: { signingKey: '******', expiresTime: '168h', bufferTime: '24h', issuer: 'kra' },
captcha: { keyLong: 6, imgWidth: 240, imgHeight: 80, storeExpiration: '3m' },
local: { storePath: 'uploads/file', pathPrefix: 'uploads/file' }, media: { sessionTtl: 24, maxFileSize: 0, chunkDir: 'uploads/chunks' },
zap: { level: 'info', prefix: '[kra] ', format: 'json', director: 'logs', encode_level: 'LowercaseLevelEncoder', stacktrace_key: 'stacktrace', show_line: true, log_in_console: true, retention_day: 7, access_req_body: true, access_resp_data: true, access_req_headers: true, access_log_max_bytes: 1024, file_only_modules: ['http', 'sql'] },
cors: { mode: 'whitelist', whitelist: [] }, app: { node: '', app_id: 'kra', env: 'development' },
system: { useRedis: false, useMultipoint: false, useStrictAuth: false, disableAutoMigrate: false, useMongo: false, addr: 8000, iplimitCount: 0, iplimitTime: 0 },
storage: { type: 'local', qiniu: {}, aliyun_oss: {}, huawei_obs: {}, tencent_cos: {}, aws_s3: {}, cloudflare_r2: {}, minio: {} }
},
email: {
to: '', from: '', host: '', secret: '', nickname: '', port: 465,
'is-ssl': true, 'is-loginauth': false
},
data: {
database: { driver: 'mysql', source: '', host: '127.0.0.1', port: '3306', user: '', password: '******', name: '', config: '', path: '', prefix: '', engine: 'InnoDB', log_mode: 'info', max_idle_conns: 10, max_open_conns: 100, conn_max_lifetime: 3600, singular: false },
redis: { network: 'tcp', addr: '', name: 'default', password: '******', db: 0, use_cluster: false, cluster_addrs: [], read_timeout: '200ms', write_timeout: '200ms' },
database_list: [], redis_list: [], mongo: { coll: '', options: '', database: '', username: '', password: '******', auth_source: '', min_pool_size: 0, max_pool_size: 0, connect_timeout_ms: 0, socket_timeout_ms: 0, is_zap: false, hosts: [] }
}
})
const storageTypes = [
{ label: '本地存储', value: 'local' }, { label: '七牛云', value: 'qiniu' },
{ label: '阿里云 OSS', value: 'aliyun-oss' }, { label: '华为云 OBS', value: 'huawei-obs' },
{ label: '腾讯云 COS', value: 'tencent-cos' }, { label: 'AWS S3', value: 'aws-s3' },
{ label: 'Cloudflare R2', value: 'cloudflare-r2' }, { label: 'MinIO', value: 'minio' }
]
const storageKeys = { 'aliyun-oss': 'aliyun_oss', 'huawei-obs': 'huawei_obs', 'tencent-cos': 'tencent_cos', 'aws-s3': 'aws_s3', 'cloudflare-r2': 'cloudflare_r2', minio: 'minio' }
const currentObjectStorage = computed(() => config.value.admin.storage[storageKeys[config.value.admin.storage.type]])
const redisClusterText = ref('')
const mongoHostsText = ref('[]')
const databaseListText = ref('[]')
const redisListText = ref('[]')
const corsRulesText = ref('[]')
const diskListText = ref('[]')
const fileOnlyModulesText = ref('')
const initForm = async () => {
const res = await getSystemConfig()
if (res.code === 0) {
config.value = {
admin: { ...config.value.admin, ...res.data.config.admin,
jwt: { ...config.value.admin.jwt, ...res.data.config.admin?.jwt },
captcha: { ...config.value.admin.captcha, ...res.data.config.admin?.captcha },
local: { ...config.value.admin.local, ...res.data.config.admin?.local }
, media: { ...config.value.admin.media, ...res.data.config.admin?.media }
, system: { ...config.value.admin.system, ...res.data.config.admin?.system }
, zap: { ...config.value.admin.zap, ...res.data.config.admin?.zap }
, cors: { ...config.value.admin.cors, ...res.data.config.admin?.cors }
, app: { ...config.value.admin.app, ...res.data.config.admin?.app }
, storage: { ...config.value.admin.storage, ...res.data.config.admin?.storage,
qiniu: { ...config.value.admin.storage.qiniu, ...res.data.config.admin?.storage?.qiniu },
aliyun_oss: { ...config.value.admin.storage.aliyun_oss, ...res.data.config.admin?.storage?.aliyun_oss },
huawei_obs: { ...config.value.admin.storage.huawei_obs, ...res.data.config.admin?.storage?.huawei_obs },
tencent_cos: { ...config.value.admin.storage.tencent_cos, ...res.data.config.admin?.storage?.tencent_cos },
aws_s3: { ...config.value.admin.storage.aws_s3, ...res.data.config.admin?.storage?.aws_s3 },
cloudflare_r2: { ...config.value.admin.storage.cloudflare_r2, ...res.data.config.admin?.storage?.cloudflare_r2 },
minio: { ...config.value.admin.storage.minio, ...res.data.config.admin?.storage?.minio }
}
},
email: { ...config.value.email, ...res.data.config.email },
data: {
database: { ...config.value.data.database, ...res.data.config.data?.database },
redis: { ...config.value.data.redis, ...res.data.config.data?.redis },
database_list: res.data.config.data?.database_list || [],
redis_list: res.data.config.data?.redis_list || [],
mongo: { ...config.value.data.mongo, ...res.data.config.data?.mongo }
}
}
redisClusterText.value = (config.value.data.redis.cluster_addrs || []).join('\n')
mongoHostsText.value = JSON.stringify(config.value.data.mongo.hosts || [], null, 2)
databaseListText.value = JSON.stringify(config.value.data.database_list || [], null, 2)
redisListText.value = JSON.stringify(config.value.data.redis_list || [], null, 2)
corsRulesText.value = JSON.stringify(config.value.admin.cors.whitelist || [], null, 2)
diskListText.value = JSON.stringify(config.value.admin.disk_list || [], null, 2)
fileOnlyModulesText.value = (config.value.admin.zap.file_only_modules || []).join('\n')
}
}
const update = async () => {
saving.value = true
try {
try {
config.value.data.redis.cluster_addrs = redisClusterText.value.split('\n').map(item => item.trim()).filter(Boolean)
config.value.data.mongo.hosts = JSON.parse(mongoHostsText.value || '[]')
config.value.data.database_list = JSON.parse(databaseListText.value || '[]')
config.value.data.redis_list = JSON.parse(redisListText.value || '[]')
config.value.admin.cors.whitelist = JSON.parse(corsRulesText.value || '[]')
config.value.admin.disk_list = JSON.parse(diskListText.value || '[]')
config.value.admin.zap.file_only_modules = fileOnlyModulesText.value.split('\n').map(item => item.trim()).filter(Boolean)
} catch {
ElMessage.error('JSON 配置格式不正确')
return
}
const res = await setSystemConfig({ config: config.value })
if (res.code === 0) {
ElMessage.success('配置保存成功')
await initForm()
}
} finally {
saving.value = false
}
}
const generateSigningKey = () => {
config.value.admin.jwt.signingKey = CreateUUID()
}
const storageFieldLabels = {
endpoint: 'Endpoint', region: 'Region', bucket: 'Bucket',
access_key: 'Access Key', secret_key: 'Secret Key', account_id: 'Account ID'
}
const validateStorage = () => {
const storage = config.value.admin.storage
if (storage.type === 'local') return true
const target = storage.type === 'qiniu' ? storage.qiniu : currentObjectStorage.value
const required = {
qiniu: ['bucket', 'access_key', 'secret_key'],
'aliyun-oss': ['endpoint', 'bucket', 'access_key', 'secret_key'],
'huawei-obs': ['endpoint', 'bucket', 'access_key', 'secret_key'],
'tencent-cos': ['region', 'bucket', 'access_key', 'secret_key'],
'aws-s3': ['region', 'bucket', 'access_key', 'secret_key'],
'cloudflare-r2': ['bucket', 'access_key', 'secret_key'],
minio: ['endpoint', 'bucket', 'access_key', 'secret_key']
}[storage.type] || []
const missing = required.filter(key => !String(target?.[key] || '').trim())
if (storage.type === 'cloudflare-r2' && !String(target?.endpoint || target?.account_id || '').trim()) {
missing.push('account_id')
}
if (missing.length) {
ElMessage.warning(`请填写:${missing.map(key => storageFieldLabels[key]).join('、')}`)
return false
}
return true
}
const saveStorage = async () => {
if (!validateStorage()) return
storageSaving.value = true
try {
const res = await setStorageConfig(config.value.admin.storage)
if (res.code === 0) {
ElMessage.success('对象存储配置已保存并生效')
await initForm()
}
} finally {
storageSaving.value = false
}
}
const reload = async () => {
try {
await ElMessageBox.confirm('确定要重载服务配置吗?', '提示')
} catch {
ElMessage.info('取消重载')
return
}
const res = await reloadSystem()
if (res.code === 0) ElMessage.success('配置已重载')
}
const validateEmail = () => {
const email = config.value.email
const fields = [
['to', '默认收件人'], ['from', '发件人'], ['host', 'SMTP 主机'], ['secret', 'SMTP 密钥']
]
const missing = fields.filter(([key]) => !String(email[key] || '').trim()).map(([, label]) => label)
if (!email.port) missing.push('SMTP 端口')
if (missing.length) {
ElMessage.warning(`请填写:${missing.join('、')}`)
return false
}
return true
}
const saveEmail = async () => {
if (!validateEmail()) return false
emailSaving.value = true
try {
const res = await setEmailConfig(config.value.email)
if (res.code !== 0) return false
ElMessage.success('邮件配置已保存并生效')
await initForm()
return true
} finally {
emailSaving.value = false
}
}
const testEmail = async () => {
if (!validateEmail()) return
testing.value = true
try {
const saved = await setEmailConfig(config.value.email)
if (saved.code !== 0) return
const res = await emailTest()
if (res.code === 0) {
ElMessage.success('测试邮件发送成功')
await initForm()
} else {
ElMessage.error('测试邮件发送失败')
}
} finally {
testing.value = false
}
}
initForm()
</script>
<style scoped>
.config-layout { min-height: 520px; }
</style>