feat(theme): CMS 站点主题弱联动 + campus-green 色值对齐(T3)——新增 utils/cmsTheme 映射适配器(SKIN_MAP/PARAM_ALIASE/colorScheme 预设,未映射一律 null 回落);App.vue 皮肤三层优先级 stale-while-revalidate(缓存先画、后台刷新、失败静默);svc.ts 加 hallApi.site;campus-green 四色与 CMS campus-glow 同源对齐并升 1.1.0;theme-base.css 克制补丁 view-ui 实际使用的 Checkbox/Radio/Page/Modal.confirm/dyn-file-btn 交互态

This commit is contained in:
chenxing
2026-10-03 13:52:56 +08:00
parent f636075a5e
commit 41b79566c1
6 changed files with 136 additions and 15 deletions
+42 -4
View File
@@ -5,23 +5,31 @@
* 与后台前端(front/)刻意不同:不渲染平台模块菜单(app_module 是管理端契约),
* 只做两个入口——办事大厅、我的办件(详情/办理页从列表进入)。
*
* 皮肤:启动时按运行时配置(env-config 的 theme/themeParams)应用皮肤包;
* 未配置 → default;未注册 → default(整页回退,见 utils/theme.ts)。
* 皮肤三层优先级(T3 弱联动,2026-10-03):
* CMS 站点推荐(/api/hall/site 的 theme/themeParams,经 utils/cmsTheme 映射;
* stale-while-revalidate——localStorage 缓存先画、后台刷新,失败静默)
* > env-config 部署级皮肤 > default。
* 推荐值、不保证生效:任一层缺失/非法都回落下一层(utils/theme.ts)。
*/
import { onMounted, ref } from "vue";
import { baseApi } from "@/api/module/base";
import config from "@/assets/config/host.config";
import { applyTheme, resolveTheme, type ThemeManifest } from "@/utils/theme";
import { toHallSkin } from "@/utils/cmsTheme";
import { useSite } from "@/utils/site";
import { hallApi } from "@/api/module/svc";
const userName = ref("");
const theme = ref<ThemeManifest>(resolveTheme(null));
// CMS 指南页来路(T4 回跳闭环):仅 /#/apply 深链带合法 from 参数时非空
const { portalFrom } = useSite();
const { portalFrom, currentSiteCode } = useSite();
onMounted(async () => {
// 皮肤先应用(首屏观感),再拉用户信息——用户信息失败不影响办理页面使用
// ① 基线:env-config 部署级皮肤(现状语义不变,先应用保证首屏观感)
theme.value = applyTheme(config.theme, config.themeParams);
// ② 站点推荐:CMS 站点主题(弱联动)——缓存先画,后台刷新
applySiteThemeRecommend();
// 用户信息失败不影响办理页面使用
try {
const user: any = await baseApi.getUserInfo().send();
userName.value = user?.name || "";
@@ -32,6 +40,36 @@ onMounted(async () => {
}
}
});
/**
* 站点主题推荐应用(stale-while-revalidate):
* 未登录时该请求会 302 跳 CAS(与 items 同行为,页面本来就要登录);
* 任何失败静默保留现皮肤——推荐、不保证生效,绝不阻塞首屏。
*/
async function applySiteThemeRecommend() {
const code = currentSiteCode.value; // useSite() 模块级 ref,当前视图 onMounted 已 resolve
if (!code) return;
const cacheKey = `svchall.public.siteTheme.${code}`;
// ① stale:同步读缓存立即应用(二次访问无闪色)
try {
const cached = JSON.parse(localStorage.getItem(cacheKey) || "null");
const stale = toHallSkin(cached);
if (stale) theme.value = applyTheme(stale.name, stale.params);
} catch {
// 坏缓存当没有
}
// ② revalidate:后台刷新(失败静默保留现皮肤——推荐、不保证生效)
try {
const info: any = await hallApi.site(code).send();
const pick = toHallSkin({ theme: info?.theme, themeParams: info?.themeParams });
localStorage.setItem(cacheKey, JSON.stringify({
theme: info?.theme || null, themeParams: info?.themeParams || null,
}));
if (pick) theme.value = applyTheme(pick.name, pick.params);
} catch (e) {
console.warn("[public-hall] 站点主题获取失败,沿用当前皮肤", e);
}
}
</script>
<template>
+4
View File
@@ -63,6 +63,10 @@ class HallApi {
filters(siteCode: string) {
return get<HallFilters>(`${HALL}/filters`, { siteCode });
}
/** 站点信息(含 CMS 主题 raw;主题弱联动数据源,失败静默——见 App.vue 应用链,T3) */
site(siteCode: string) {
return get(`${HALL}/site`, { siteCode });
}
}
class ApplyApi {
+20
View File
@@ -160,3 +160,23 @@ body {
background: var(--sh-primary-dark);
border-color: var(--sh-primary-dark);
}
/* view-ui 主色跟随皮肤(克制补丁:仅 hall 实际出现的交互态,2026-10-03 T3 联动配套;
未用到的 Tabs/Switch/ivu-btn-text 不加,不做全组件库 retheme) */
.dyn-file-btn:hover .dyn-file-btn-inner {
color: var(--sh-primary);
}
.ivu-checkbox-checked .ivu-checkbox-inner,
.ivu-radio-checked .ivu-radio-inner {
border-color: var(--sh-primary);
}
.ivu-checkbox-checked .ivu-checkbox-inner:after,
.ivu-radio-checked .ivu-radio-inner:after {
background: var(--sh-primary);
}
.ivu-page-item-active a {
color: var(--sh-primary);
}
.ivu-modal-confirm-head-icon-confirm .ivu-icon-ios-alert {
color: var(--sh-accent);
}
+6 -5
View File
@@ -1,9 +1,10 @@
/* 校园青绿——第二套皮肤:换令牌 + 换圆角 + 换列表布局(结构变体走 data-sh-* 钩子) */
/* 校园青绿——第二套皮肤:换令牌 + 换圆角 + 换列表布局(结构变体走 data-sh-* 钩子)
色值与 CMS campus-glow 同源(T3 弱联动对齐,2026-10-03),与 theme.json default 一致 */
:root[data-theme="campus-green"] {
--sh-primary: #0e8a6e;
--sh-primary-dark: #0a6a55;
--sh-accent: #d97706;
--sh-page-bg: #f2f8f5;
--sh-primary: #0e8a5f;
--sh-primary-dark: #0a6244;
--sh-accent: #ff7a1a;
--sh-page-bg: #f4f7f5;
--sh-radius: 12px;
}
+6 -6
View File
@@ -2,13 +2,13 @@
"specVersion": 1,
"name": "campus-green",
"title": "校园青绿",
"version": "1.0.0",
"description": "第二套可切换皮肤:青绿主色 + 更宽松的圆角,用于验证皮肤包机制(新增皮肤=新增目录,宿主零改动)。",
"version": "1.1.0",
"description": "第二套可切换皮肤:青绿主色 + 更宽松的圆角(色值与 CMS campus-glow 同源,T3 弱联动对齐 2026-10-03)。",
"params": [
{ "key": "primary", "cssVar": "--sh-primary", "label": "主色", "type": "color", "default": "#0e8a6e" },
{ "key": "primaryDark", "cssVar": "--sh-primary-dark", "label": "主色加深", "type": "color", "default": "#0a6a55" },
{ "key": "accent", "cssVar": "--sh-accent", "label": "强调色", "type": "color", "default": "#d97706" },
{ "key": "pageBg", "cssVar": "--sh-page-bg", "label": "页面底色", "type": "color", "default": "#f2f8f5" },
{ "key": "primary", "cssVar": "--sh-primary", "label": "主色", "type": "color", "default": "#0e8a5f" },
{ "key": "primaryDark", "cssVar": "--sh-primary-dark", "label": "主色加深", "type": "color", "default": "#0a6244" },
{ "key": "accent", "cssVar": "--sh-accent", "label": "强调色", "type": "color", "default": "#ff7a1a" },
{ "key": "pageBg", "cssVar": "--sh-page-bg", "label": "页面底色", "type": "color", "default": "#f4f7f5" },
{
"key": "listLayout",
"label": "事项列表布局",
+58
View File
@@ -0,0 +1,58 @@
/**
* CMS 门户主题 → hall 皮肤的映射适配器(色值级弱联动,2026-10-03 方案 T3)。
* 原则:CMS 主题是"推荐值"——未映射/字段缺失一律返回 null,调用方回落 env-config 皮肤;
* 联动只对齐品牌色 token,不复刻 CMS 模板结构。
* 契约来源:CMS /oapi/site/list raw 透传(SiteOapiController),键:theme、themeParams。
*/
export interface CmsSiteThemeInfo {
theme: string | null;
themeParams: Record<string, string> | null;
}
export interface HallSkinChoice {
/** hall 皮肤名(一定已注册,未映射返回 null 整体回落) */
name: string;
/** 覆盖值(键必须命中目标皮肤的 params 声明,applyTheme 会再过滤校验) */
params: Record<string, string>;
}
/** CMS 皮肤包名 → hall 皮肤名(新增 CMS 主题时在此登记;不认识=回落) */
const SKIN_MAP: Record<string, string> = {
"default": "default",
"campus-glow": "campus-green",
"scholar-blue": "default",
};
/** CMS default 主题 colorScheme 预设 → hall 参数(值译自 CMS default/theme.json presets 段) */
const COLOR_SCHEMES: Record<string, Record<string, string>> = {
"blue-white": { primary: "#2f6bd8", primaryDark: "#1d4fae", pageBg: "#f7f9fc" },
"classic-red": { primary: "#b01a2e", primaryDark: "#7f1224", pageBg: "#f7f5f2" },
"fresh-mint": { primary: "#17a2a2", primaryDark: "#0f7c7c", pageBg: "#f5fbfa" },
"warm-kinder": { primary: "#e8722a", primaryDark: "#c25a1a", pageBg: "#fff8f0" },
};
/** CMS themeParams 键 → hall 皮肤参数键(同名直传,bg 特译;accent 同名但仅部分 CMS 主题声明) */
const PARAM_ALIASE: Record<string, string> = {
primary: "primary",
primaryDark: "primaryDark",
accent: "accent",
bg: "pageBg",
};
export function toHallSkin(cms: CmsSiteThemeInfo | null | undefined): HallSkinChoice | null {
if (!cms || !cms.theme) return null;
const skin = SKIN_MAP[cms.theme];
if (!skin) return null;
const params: Record<string, string> = {};
// ① colorScheme 预设打底(custom/未配=无)
const scheme = cms.themeParams?.colorScheme;
if (scheme && COLOR_SCHEMES[scheme]) Object.assign(params, COLOR_SCHEMES[scheme]);
// ② 站点显式色值覆盖预设(同键后者赢)
const tp = cms.themeParams || {};
for (const [cmsKey, hallKey] of Object.entries(PARAM_ALIASE)) {
const v = tp[cmsKey];
if (typeof v === "string" && /^#[0-9a-fA-F]{3,8}$/.test(v)) params[hallKey] = v;
}
return { name: skin, params };
}