468 lines
29 KiB
Vue
468 lines
29 KiB
Vue
<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>
|