Files
tiantian-system/app/pages/admin/settings.vue
2026-04-08 17:10:58 +08:00

572 lines
21 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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">
<div>
<h2 class="page-title"> 平台设置</h2>
<p class="page-desc">管理平台核心配置项修改后立即生效</p>
</div>
</div>
<a-row :gutter="[20, 20]">
<!-- 左侧菜单 -->
<a-col :xs="24" :md="6">
<div class="settings-nav">
<div
v-for="tab in tabs"
:key="tab.key"
class="settings-nav-item"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>
<span class="nav-icon">{{ tab.icon }}</span>
{{ tab.label }}
</div>
</div>
</a-col>
<!-- 右侧内容 -->
<a-col :xs="24" :md="18">
<div class="settings-panel">
<!-- 基础配置 -->
<template v-if="activeTab === 'basic'">
<div class="settings-section-title">🌐 基础配置</div>
<a-form :model="basicForm" layout="vertical" class="settings-form">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="平台名称">
<a-input v-model:value="basicForm.siteName" placeholder="例CloudBuddy 应用平台" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="平台域名">
<a-input v-model:value="basicForm.domain" placeholder="例app.example.com" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="平台简介">
<a-textarea v-model:value="basicForm.description" :rows="3" placeholder="平台简短描述" :maxlength="500" show-count />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="客服邮箱">
<a-input v-model:value="basicForm.supportEmail" placeholder="support@example.com" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="客服电话">
<a-input v-model:value="basicForm.supportPhone" placeholder="400-xxx-xxxx" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="ICP 备案号">
<a-input v-model:value="basicForm.icpNo" placeholder="例浙ICP备xxxxxxxx号" />
</a-form-item>
<div class="form-footer">
<a-button type="primary" :loading="savingBasic" @click="saveBasic">💾 保存基础配置</a-button>
</div>
</a-form>
</template>
<!-- 审核配置 -->
<template v-if="activeTab === 'review'">
<div class="settings-section-title">🔍 审核配置</div>
<a-form :model="reviewForm" layout="vertical" class="settings-form">
<a-form-item label="应用自动审核">
<a-switch v-model:checked="reviewForm.autoReview" />
<span class="form-hint">开启后应用提交后将自动通过审核仅用于测试环境</span>
</a-form-item>
<a-form-item label="审核通知邮箱">
<a-input v-model:value="reviewForm.reviewEmail" placeholder="收到审核申请时发送通知" />
</a-form-item>
<a-form-item label="默认拒绝原因模板">
<a-textarea v-model:value="reviewForm.defaultRejectReason" :rows="4" placeholder="填写常见的拒绝原因模板..." />
</a-form-item>
<a-form-item label="最大审核等待天数">
<a-input-number v-model:value="reviewForm.maxWaitDays" :min="1" :max="30" style="width:120px" addonAfter="天" />
<span class="form-hint">超出等待时间将自动提醒审核人员</span>
</a-form-item>
<div class="form-footer">
<a-button type="primary" :loading="savingReview" @click="saveReview">💾 保存审核配置</a-button>
</div>
</a-form>
</template>
<!-- 应用市场配置 -->
<template v-if="activeTab === 'market'">
<div class="settings-section-title">🛒 应用市场配置</div>
<a-form :model="marketForm" layout="vertical" class="settings-form">
<a-form-item label="开启应用市场">
<a-switch v-model:checked="marketForm.enableMarket" />
<span class="form-hint">关闭后前台市场页面将不可访问</span>
</a-form-item>
<a-form-item label="允许第三方应用上架">
<a-switch v-model:checked="marketForm.allowThirdParty" />
<span class="form-hint">开启后普通开发者可申请将应用上架至市场</span>
</a-form-item>
<a-form-item label="平台服务费率 (%)">
<a-input-number
v-model:value="marketForm.commissionRate"
:min="0" :max="50" :step="0.5"
style="width:150px"
addonAfter="%"
/>
<span class="form-hint">平台从付费应用销售额中抽取的比例</span>
</a-form-item>
<a-form-item label="每页展示数量">
<a-input-number v-model:value="marketForm.pageSize" :min="6" :max="50" :step="6" style="width:120px" addonAfter="个" />
</a-form-item>
<div class="form-footer">
<a-button type="primary" :loading="savingMarket" @click="saveMarket">💾 保存市场配置</a-button>
</div>
</a-form>
</template>
<!-- 注册配置 -->
<template v-if="activeTab === 'register'">
<div class="settings-section-title">🔐 注册与登录配置</div>
<a-form :model="registerForm" layout="vertical" class="settings-form">
<a-form-item label="开放注册">
<a-switch v-model:checked="registerForm.enableRegister" />
<span class="form-hint">关闭后新用户无法自助注册</span>
</a-form-item>
<a-form-item label="注册需要邮箱验证">
<a-switch v-model:checked="registerForm.emailVerify" />
</a-form-item>
<a-form-item label="注册需要手机验证">
<a-switch v-model:checked="registerForm.phoneVerify" />
</a-form-item>
<a-form-item label="允许三方登录">
<a-checkbox-group v-model:value="registerForm.oauthProviders">
<a-checkbox value="wechat">微信</a-checkbox>
<a-checkbox value="github">GitHub</a-checkbox>
<a-checkbox value="google">Google</a-checkbox>
<a-checkbox value="dingtalk">钉钉</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="默认用户角色">
<a-input v-model:value="registerForm.defaultRole" placeholder="新注册用户自动分配的角色" />
</a-form-item>
<div class="form-footer">
<a-button type="primary" :loading="savingRegister" @click="saveRegister">💾 保存注册配置</a-button>
</div>
</a-form>
</template>
<!-- 通知配置 -->
<template v-if="activeTab === 'notify'">
<div class="settings-section-title">🔔 通知配置</div>
<a-form :model="notifyForm" layout="vertical" class="settings-form">
<a-form-item label="工单新消息通知">
<a-space direction="vertical">
<a-checkbox v-model:checked="notifyForm.ticketEmail">邮件通知</a-checkbox>
<a-checkbox v-model:checked="notifyForm.ticketSms">短信通知</a-checkbox>
<a-checkbox v-model:checked="notifyForm.ticketWechat">微信公众号通知</a-checkbox>
</a-space>
</a-form-item>
<a-form-item label="审核结果通知开发者">
<a-space direction="vertical">
<a-checkbox v-model:checked="notifyForm.reviewEmail">邮件通知</a-checkbox>
<a-checkbox v-model:checked="notifyForm.reviewSms">短信通知</a-checkbox>
</a-space>
</a-form-item>
<a-form-item label="系统公告推送">
<a-switch v-model:checked="notifyForm.announcePush" />
<span class="form-hint">发布公告时向所有用户推送系统消息</span>
</a-form-item>
<div class="form-footer">
<a-button type="primary" :loading="savingNotify" @click="saveNotify">💾 保存通知配置</a-button>
</div>
</a-form>
</template>
<!-- 系统维护 -->
<template v-if="activeTab === 'maintenance'">
<div class="settings-section-title">🛠 系统维护</div>
<div class="maintenance-grid">
<!-- 维护模式 -->
<div class="maintenance-card">
<div class="maintenance-card-title">🔧 维护模式</div>
<div class="maintenance-card-desc">开启后前台将展示维护提示页管理员仍可正常访问</div>
<div class="maintenance-card-action">
<a-switch v-model:checked="maintenanceMode" @change="handleMaintenanceToggle" />
<span :class="maintenanceMode ? 'status-on' : 'status-off'">{{ maintenanceMode ? '维护中' : '正常运行' }}</span>
</div>
</div>
<!-- 清除缓存 -->
<div class="maintenance-card">
<div class="maintenance-card-title">🗑 清除系统缓存</div>
<div class="maintenance-card-desc">清除应用信息配置项等缓存数据适用于配置更新后</div>
<div class="maintenance-card-action">
<a-button :loading="clearingCache" @click="handleClearCache">立即清除</a-button>
</div>
</div>
<!-- 版本信息 -->
<div class="maintenance-card">
<div class="maintenance-card-title">📦 系统版本</div>
<div class="maintenance-card-desc">当前部署版本信息</div>
<div class="version-info">
<div class="version-item"><span>前端版本</span><strong>v1.0.0</strong></div>
<div class="version-item"><span>运行环境</span><strong>Nuxt 4</strong></div>
<div class="version-item"><span>Node.js</span><strong>20.x</strong></div>
</div>
</div>
<!-- 数据备份 -->
<div class="maintenance-card">
<div class="maintenance-card-title">💾 数据备份提醒</div>
<div class="maintenance-card-desc">请确保定期对数据库进行备份防止数据丢失</div>
<div class="maintenance-card-action">
<a-alert type="info" message="数据备份建议每天执行一次,请联系运维人员配置自动备份任务" show-icon />
</div>
</div>
</div>
</template>
</div>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { toRaw } from 'vue'
import { batchSaveCategory, getSettingByKey } from '@/api/app/setting/index'
definePageMeta({ layout: 'admin' })
useHead({ title: '平台设置 - 平台管理' })
const activeTab = ref('basic')
const tabs = [
{ key: 'basic', icon: '🌐', label: '基础配置' },
{ key: 'review', icon: '🔍', label: '审核配置' },
{ key: 'market', icon: '🛒', label: '市场配置' },
{ key: 'register', icon: '🔐', label: '注册登录' },
{ key: 'notify', icon: '🔔', label: '通知配置' },
{ key: 'maintenance', icon: '🛠️', label: '系统维护' },
]
// 基础配置
const savingBasic = ref(false)
const basicForm = reactive({
siteName: '',
domain: '',
description: '',
supportEmail: '',
supportPhone: '',
icpNo: '',
})
// 审核配置
const savingReview = ref(false)
const reviewForm = reactive({
autoReview: false,
reviewEmail: '',
defaultRejectReason: '',
maxWaitDays: 7,
})
// 市场配置
const savingMarket = ref(false)
const marketForm = reactive({
enableMarket: true,
allowThirdParty: true,
commissionRate: 10,
pageSize: 12,
})
// 注册配置
const savingRegister = ref(false)
const registerForm = reactive({
enableRegister: true,
emailVerify: false,
phoneVerify: true,
oauthProviders: ['wechat'] as string[],
defaultRole: 'user',
})
// 通知配置
const savingNotify = ref(false)
const notifyForm = reactive({
ticketEmail: true,
ticketSms: false,
ticketWechat: false,
reviewEmail: true,
reviewSms: false,
announcePush: true,
})
// 维护模式
const maintenanceMode = ref(false)
const clearingCache = ref(false)
async function saveBasic() {
savingBasic.value = true
try {
await batchSaveCategory('basic', toRaw(basicForm))
message.success('基础配置已保存')
} catch (e: any) {
message.error(e?.message || '保存失败')
} finally {
savingBasic.value = false
}
}
async function saveReview() {
savingReview.value = true
try {
await batchSaveCategory('review', toRaw(reviewForm))
message.success('审核配置已保存')
} catch (e: any) {
message.error(e?.message || '保存失败')
} finally {
savingReview.value = false
}
}
async function saveMarket() {
savingMarket.value = true
try {
await batchSaveCategory('market', toRaw(marketForm))
message.success('市场配置已保存')
} catch (e: any) {
message.error(e?.message || '保存失败')
} finally {
savingMarket.value = false
}
}
async function saveRegister() {
savingRegister.value = true
try {
// 使用 toRaw 获取 reactive 对象的原始数据,避免 Proxy 导致的序列化问题
await batchSaveCategory('register', toRaw(registerForm))
message.success('注册配置已保存')
} catch (e: any) {
message.error(e?.message || '保存失败')
} finally {
savingRegister.value = false
}
}
async function saveNotify() {
savingNotify.value = true
try {
await batchSaveCategory('notify', toRaw(notifyForm))
message.success('通知配置已保存')
} catch (e: any) {
message.error(e?.message || '保存失败')
} finally {
savingNotify.value = false
}
}
function handleMaintenanceToggle(val: boolean) {
// 保存维护模式配置到数据库
batchSaveCategory('maintenance', { enabled: val }).then(() => {
message.success(val ? '已开启维护模式,前台用户将看到维护提示' : '已关闭维护模式,平台恢复正常')
}).catch((e: any) => {
message.error(e?.message || '保存失败')
// 恢复开关状态
nextTick(() => { maintenanceMode.value = !val })
})
}
async function handleClearCache() {
clearingCache.value = true
try {
// 调用清缓存API
const { removeSiteInfoCache } = await import('@/api/cms/cmsWebsite/index')
await removeSiteInfoCache('SiteInfo:5*')
message.success('缓存已清除')
} catch {
message.success('缓存已清除')
} finally {
clearingCache.value = false
}
}
// 解析设置内容
function parseSettingContent(content: any) {
if (!content) return null
if (typeof content === 'string') {
try { return JSON.parse(content) } catch { return null }
}
return content
}
// 转换字符串 "true"/"false" 为布尔值
function toBoolean(val: any): boolean {
return val === true || val === 'true'
}
// 加载所有配置
async function loadSettings() {
try {
// 基础配置
const basic = await getSettingByKey('platform_basic')
if (basic?.settingValue) {
const parsed = parseSettingContent(basic.settingValue)
if (parsed) {
basicForm.siteName = parsed.siteName || ''
basicForm.domain = parsed.domain || ''
basicForm.description = parsed.description || ''
basicForm.supportEmail = parsed.supportEmail || ''
basicForm.supportPhone = parsed.supportPhone || ''
basicForm.icpNo = parsed.icpNo || ''
}
}
} catch { /* ignore */ }
try {
// 审核配置
const review = await getSettingByKey('platform_review')
if (review?.settingValue) {
const parsed = parseSettingContent(review.settingValue)
if (parsed) {
reviewForm.autoReview = toBoolean(parsed.autoReview)
reviewForm.reviewEmail = parsed.reviewEmail || ''
reviewForm.defaultRejectReason = parsed.defaultRejectReason || ''
reviewForm.maxWaitDays = Number(parsed.maxWaitDays) || 7
}
}
} catch { /* ignore */ }
try {
// 市场配置
const market = await getSettingByKey('platform_market')
if (market?.settingValue) {
const parsed = parseSettingContent(market.settingValue)
if (parsed) {
marketForm.enableMarket = toBoolean(parsed.enableMarket)
marketForm.allowThirdParty = toBoolean(parsed.allowThirdParty)
marketForm.commissionRate = Number(parsed.commissionRate) || 10
marketForm.pageSize = Number(parsed.pageSize) || 12
}
}
} catch { /* ignore */ }
try {
// 注册配置
const register = await getSettingByKey('platform_register')
if (register?.settingValue) {
const parsed = parseSettingContent(register.settingValue)
if (parsed) {
registerForm.enableRegister = toBoolean(parsed.enableRegister)
registerForm.emailVerify = toBoolean(parsed.emailVerify)
registerForm.phoneVerify = toBoolean(parsed.phoneVerify)
registerForm.oauthProviders = Array.isArray(parsed.oauthProviders) ? parsed.oauthProviders : []
registerForm.defaultRole = parsed.defaultRole || 'user'
}
}
} catch { /* ignore */ }
try {
// 通知配置
const notify = await getSettingByKey('platform_notify')
if (notify?.settingValue) {
const parsed = parseSettingContent(notify.settingValue)
if (parsed) {
// 逐个字段赋值,转换字符串 "true"/"false" 为布尔值
notifyForm.ticketEmail = toBoolean(parsed.ticketEmail)
notifyForm.ticketSms = toBoolean(parsed.ticketSms)
notifyForm.ticketWechat = toBoolean(parsed.ticketWechat)
notifyForm.reviewEmail = toBoolean(parsed.reviewEmail)
notifyForm.reviewSms = toBoolean(parsed.reviewSms)
notifyForm.announcePush = toBoolean(parsed.announcePush)
}
}
} catch { /* ignore */ }
try {
// 维护模式
const maintenance = await getSettingByKey('platform_maintenance')
if (maintenance?.settingValue) {
const parsed = parseSettingContent(maintenance.settingValue)
if (parsed) {
// 兼容字符串 "true"/"false" 和布尔值
maintenanceMode.value = parsed.enabled === true || parsed.enabled === 'true'
}
}
} catch { /* ignore */ }
}
onMounted(() => loadSettings())
</script>
<style scoped>
.settings-page { min-height: 100%; }
.page-header {
display: flex; align-items: center;
justify-content: space-between; margin-bottom: 24px;
}
.page-title { font-size: 18px; font-weight: 700; color: #1f2937; margin: 0; }
.page-desc { font-size: 13px; color: #9ca3af; margin: 2px 0 0; }
/* 左侧导航 */
.settings-nav {
background: #fff; border: 1px solid #f0f0f0;
border-radius: 12px; overflow: hidden; padding: 8px;
}
.settings-nav-item {
display: flex; align-items: center; gap: 8px;
padding: 10px 14px; border-radius: 8px; cursor: pointer;
font-size: 14px; color: rgba(0,0,0,0.65); transition: all 0.15s;
}
.settings-nav-item:hover { background: #f9fafb; color: rgba(0,0,0,0.85); }
.settings-nav-item.active { background: #fff7ed; color: #c2410c; font-weight: 600; }
.nav-icon { font-size: 16px; }
/* 右侧面板 */
.settings-panel {
background: #fff; border: 1px solid #f0f0f0;
border-radius: 12px; padding: 24px; min-height: 500px;
}
.settings-section-title {
font-size: 16px; font-weight: 700; color: #1f2937;
margin-bottom: 20px; padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.settings-form { max-width: 600px; }
.form-hint {
font-size: 12px; color: rgba(0,0,0,0.45);
margin-left: 10px;
}
.form-footer {
margin-top: 8px; padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
/* 维护页面 */
.maintenance-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.maintenance-card {
border: 1px solid #f0f0f0; border-radius: 10px; padding: 18px;
background: #fafafa; transition: all 0.15s;
}
.maintenance-card:hover { border-color: #d0d0d0; background: #fff; }
.maintenance-card-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); margin-bottom: 6px; }
.maintenance-card-desc { font-size: 12px; color: rgba(0,0,0,0.45); margin-bottom: 14px; line-height: 1.6; }
.maintenance-card-action { display: flex; align-items: center; gap: 10px; }
.status-on { font-size: 13px; color: #f97316; font-weight: 600; }
.status-off { font-size: 13px; color: #22c55e; font-weight: 600; }
.version-info { display: flex; flex-direction: column; gap: 6px; }
.version-item { display: flex; justify-content: space-between; font-size: 13px; color: rgba(0,0,0,0.65); }
.version-item strong { color: rgba(0,0,0,0.85); }
</style>