feat(theme-system): F006 后台主题配置页
- src/views/theme/ThemeConfigView.vue: 4 套主题色卡 + preview + 一键保存 - router: /sys/theme-config 路由 - menu-labels: 系统设置下加主题配置菜单项 (Brush icon) - 后端依赖: GET /sys/openapi/theme/current + POST /sys/openapi/theme/update?theme=X
This commit is contained in:
parent
9837d9fb41
commit
474a4eabfc
@ -145,6 +145,7 @@ export const MENU_LABELS: MenuLabel[] = [
|
|||||||
{ url: 'sys/dict-type', label: '字典类型', icon: 'Collection', path: '/sys/dict', sort: 412 },
|
{ url: 'sys/dict-type', label: '字典类型', icon: 'Collection', path: '/sys/dict', sort: 412 },
|
||||||
{ url: 'sys/notice', label: '通知公告', icon: 'Bell', path: '/sys/notice', sort: 413 },
|
{ url: 'sys/notice', label: '通知公告', icon: 'Bell', path: '/sys/notice', sort: 413 },
|
||||||
{ url: 'sys/params', label: '参数配置', icon: 'Setting', path: '/sys/params', sort: 414 },
|
{ url: 'sys/params', label: '参数配置', icon: 'Setting', path: '/sys/params', sort: 414 },
|
||||||
|
{ url: 'sys/theme-config', label: '主题配置', icon: 'Brush', path: '/sys/theme-config', sort: 415 },
|
||||||
{ url: 'message/mail-template', label: '邮件模板', icon: 'Document', path: '/message/mail-template', sort: 803 },
|
{ url: 'message/mail-template', label: '邮件模板', icon: 'Document', path: '/message/mail-template', sort: 803 },
|
||||||
{ url: 'message/mail-log', label: '邮件日志', icon: 'List', path: '/message/mail-log', sort: 804 },
|
{ url: 'message/mail-log', label: '邮件日志', icon: 'List', path: '/message/mail-log', sort: 804 },
|
||||||
{ url: 'message/sms-log', label: '短信日志', icon: 'List', path: '/message/sms-log', sort: 805 },
|
{ url: 'message/sms-log', label: '短信日志', icon: 'List', path: '/message/sms-log', sort: 805 },
|
||||||
|
|||||||
@ -250,6 +250,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{ path: '/sys/log/error', component: () => import('@/modules/sys/LogErrorView.vue'), meta: { title: '异常日志' } },
|
{ path: '/sys/log/error', component: () => import('@/modules/sys/LogErrorView.vue'), meta: { title: '异常日志' } },
|
||||||
{ path: '/sys/notice', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-notice' }), meta: { title: '通知公告' } },
|
{ path: '/sys/notice', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-notice' }), meta: { title: '通知公告' } },
|
||||||
{ path: '/sys/params', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-params' }), meta: { title: '参数配置' } },
|
{ path: '/sys/params', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-params' }), meta: { title: '参数配置' } },
|
||||||
|
{ path: '/sys/theme-config', component: () => import('@/views/theme/ThemeConfigView.vue'), meta: { title: '主题配置' } },
|
||||||
{ path: '/sys/notice-user', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-notice-user' }), meta: { title: '用户通知' } },
|
{ path: '/sys/notice-user', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-notice-user' }), meta: { title: '用户通知' } },
|
||||||
{ path: '/sys/userManagement', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-userManagement' }), meta: { title: '全国运营中心' } },
|
{ path: '/sys/userManagement', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-userManagement' }), meta: { title: '全国运营中心' } },
|
||||||
// user 9 个
|
// user 9 个
|
||||||
|
|||||||
178
src/views/theme/ThemeConfigView.vue
Normal file
178
src/views/theme/ThemeConfigView.vue
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<!--
|
||||||
|
* F006 主题配置 (管理后台)
|
||||||
|
* - 选 4 套预设主题之一,应用到 pc-前端-v2 全站
|
||||||
|
* - 后端接口:
|
||||||
|
* GET /sys/openapi/theme/current → { theme: "industrial-blue", available: [...] }
|
||||||
|
* POST /sys/openapi/theme/update?theme=xxx 切换主题
|
||||||
|
* - 切换后 pc-前端-v2 下次刷新或主页轮询时会拿到最新主题
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import { httpGet, httpPost } from '@/utils/request'
|
||||||
|
|
||||||
|
interface Theme {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
description: string
|
||||||
|
primary: string
|
||||||
|
'primary-container': string
|
||||||
|
secondary: string
|
||||||
|
surface: string
|
||||||
|
'text-primary': string
|
||||||
|
border: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const themes = ref<Theme[]>([
|
||||||
|
{
|
||||||
|
key: 'industrial-blue',
|
||||||
|
label: '工业蓝',
|
||||||
|
description: '默认主题,适用于工业品 B2B 大宗交易场景',
|
||||||
|
primary: '#0B5394', 'primary-container': '#E8F0FA',
|
||||||
|
secondary: '#E07B00', surface: '#FFFFFF',
|
||||||
|
'text-primary': '#1A1F2C', border: '#E2E6EE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'steel-red',
|
||||||
|
label: '钢红',
|
||||||
|
description: '国企/政策导向客户首选,庄重正式',
|
||||||
|
primary: '#A8202E', 'primary-container': '#FAE8EB',
|
||||||
|
secondary: '#C99B30', surface: '#FFFFFF',
|
||||||
|
'text-primary': '#1A1F2C', border: '#EFE2E4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'forest-green',
|
||||||
|
label: '森林绿',
|
||||||
|
description: '能源/碳中和/化工场景,传递可持续发展',
|
||||||
|
primary: '#1F6E4D', 'primary-container': '#E5F2EC',
|
||||||
|
secondary: '#D4A017', surface: '#FFFFFF',
|
||||||
|
'text-primary': '#1A1F2C', border: '#E2EAE5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'midnight-slate',
|
||||||
|
label: '暗夜深灰',
|
||||||
|
description: '数据型暗模式,适合长时间盯盘的运营人员',
|
||||||
|
primary: '#3B82F6', 'primary-container': '#1E293B',
|
||||||
|
secondary: '#60A5FA', surface: '#0F172A',
|
||||||
|
'text-primary': '#F1F5F9', border: '#334155',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const current = ref<string>('')
|
||||||
|
const preview = ref<string>('') // 用户选中但未保存
|
||||||
|
const saving = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const updateTime = ref('')
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await httpGet('/sys/openapi/theme/current')
|
||||||
|
if (res.code === 0) {
|
||||||
|
current.value = res.data.theme
|
||||||
|
preview.value = res.data.theme
|
||||||
|
updateTime.value = new Date().toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('加载当前主题失败: ' + (e as Error).message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!preview.value) return
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await httpPost(`/sys/openapi/theme/update?theme=${preview.value}`)
|
||||||
|
if (res.code === 0) {
|
||||||
|
current.value = preview.value
|
||||||
|
updateTime.value = new Date().toLocaleString('zh-CN')
|
||||||
|
ElMessage.success(`主题已切换为「${findByKey(preview.value)?.label}」,pc-前端-v2 下次访问将自动应用`)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '保存失败')
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error('保存失败: ' + e.message)
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findByKey(key: string) {
|
||||||
|
return themes.value.find(t => t.key === key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirty = computed(() => preview.value !== current.value)
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageHeader title="主题配置" :sub="`当前生效主题 · ${findByKey(current)?.label || '加载中'} · 更新于 ${updateTime}`">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<el-tag v-if="dirty" type="warning" effect="plain">有未保存的更改</el-tag>
|
||||||
|
<el-button type="primary" :disabled="!dirty || saving" :loading="saving" @click="save">
|
||||||
|
保存并应用
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="grid grid-cols-1 lg:grid-cols-2 gap-5 mt-5">
|
||||||
|
<div
|
||||||
|
v-for="theme in themes"
|
||||||
|
:key="theme.key"
|
||||||
|
class="rounded-2xl border-2 transition-all cursor-pointer overflow-hidden hover:-translate-y-1 hover:shadow-xl"
|
||||||
|
:class="preview === theme.key
|
||||||
|
? 'border-primary shadow-lg shadow-primary/20'
|
||||||
|
: 'border-border'"
|
||||||
|
@click="preview = theme.key"
|
||||||
|
>
|
||||||
|
<!-- 预览头 -->
|
||||||
|
<div
|
||||||
|
class="h-24 flex items-center justify-between px-5 text-white"
|
||||||
|
:style="{ background: `linear-gradient(135deg, ${theme.primary}, ${theme.secondary})` }"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="text-lg font-bold">{{ theme.label }}</div>
|
||||||
|
<div class="text-xs opacity-80 mt-1">{{ theme.key }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-icon v-if="preview === theme.key" :size="22"><Check /></el-icon>
|
||||||
|
<el-tag v-if="current === theme.key" type="success" effect="dark" size="small">当前生效</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview mock -->
|
||||||
|
<div class="p-5 space-y-3" :style="{ backgroundColor: theme.surface, color: theme['text-primary'] }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 rounded-lg flex items-center justify-center text-white font-bold"
|
||||||
|
:style="{ backgroundColor: theme.primary }">M</div>
|
||||||
|
<div class="font-bold text-sm">LOGO 主品牌色</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs opacity-70">{{ theme.description }}</div>
|
||||||
|
<div class="flex gap-2 mt-3">
|
||||||
|
<button class="px-4 py-1.5 rounded-lg text-xs font-bold text-white"
|
||||||
|
:style="{ backgroundColor: theme.primary }">主按钮</button>
|
||||||
|
<button class="px-4 py-1.5 rounded-lg text-xs font-bold border-2"
|
||||||
|
:style="{ borderColor: theme.primary, color: theme.primary }">描边按钮</button>
|
||||||
|
<span class="px-3 py-1 rounded text-xs font-bold"
|
||||||
|
:style="{ backgroundColor: theme['primary-container'], color: theme.primary }">标签</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] opacity-50 pt-2 border-t" :style="{ borderColor: theme.border }">
|
||||||
|
HEX: {{ theme.primary }} · {{ theme.secondary }} · {{ theme['text-primary'] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
class="mt-6"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
title="关于主题切换"
|
||||||
|
description="点击主题卡片预览,保存后 pc-前端-v2 的所有页面(包括其他用户正在浏览)将在下次请求 / 重载时生效。无需重启服务。"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
Loading…
Reference in New Issue
Block a user