Compare commits

..
5 Commits
Author SHA1 Message Date
chenxing f5b39b96c6 chore(cleanup): 移除未使用的 loading.gif 资产与 .table-spin-load 样式
build / build (push) Successful in 2m14s
删除 public/image/loading.gif、src/assets/image/loading.gif 及 main.scss 中
唯一引用该图的 .table-spin-load 类;全仓 grep 确认三者均零引用(归属存疑已收口,
见 docs/svc-hall-P3与ModeA-实施台账-20261019.md)
2026-10-19 23:24:21 +08:00
chenxing f508924ae8 chore(dev): .env.dev 本地站点标识 yhms→demo(T6 走查发现:本地 bsch_cms 演示站点为 demo,yhms 无对应站点致大厅页空态报「站点不存在」;仅影响 vite dev,产物不读 .env) 2026-10-03 15:54:22 +08:00
chenxing a4f954d3b8 chore(cleanup): 注释口径统一/死文件清理/越轨色值令牌化(T6)——注释 target=hall(index.html/host.config/.env.dev)与 routes 部署路径、契约注释补 &from=;README 失效文档引用改 ../docs/用户面(hall);删 .env.mock+dev:mock+viteMockServe 配置+mockjs/vite-plugin-mock 依赖(mockPath 指向不存在目录)、删 _varible.scss(唯一 import 方式为不带下划线的 "./varible",方案 grep 口径漏检,_utils.scss 就地内联 4 色值产物不变)、删 config/index.js(basedebug 调试残留零引用);#2d8cf0/rgba(26,92,176,.15)/#1f9254/#d03050 令牌化(--sh-success/--sh-danger 新增) 2026-10-03 14:09:09 +08:00
chenxing 41b79566c1 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 交互态 2026-10-03 13:52:56 +08:00
chenxing f636075a5e feat(apply): 深链 from 参数解析 + 顶栏「返回门户」(T4)——site.ts 新增 resolveFrom(仅 http/https 且域名属本部署可信域,防伪协议/外域注入;session 级内存不落 localStorage),Apply.load 在 resolveSite 后调用,App.vue nav 按需渲染返回门户链接(nav 为主色底 header,白字+分隔线与既有风格一致) 2026-10-03 13:29:46 +08:00
25 changed files with 287 additions and 600 deletions
+3 -2
View File
@@ -1,5 +1,5 @@
# 开发环境 (dev):仅 vite dev --mode dev 加载;build 产物不读任何 .env.* # 开发环境 (dev):仅 vite dev --mode dev 加载;build 产物不读任何 .env.*
# (.env.base/.env.ld/.env.prod 已删,线上由 /api/env-config?target=front 运行时下发)。 # (.env.base/.env.ld/.env.prod/.env.mock 已删,线上由 /api/env-config?target=hall 运行时下发)。
# ⚠️ VITE_* 仅本地开发生效。 # ⚠️ VITE_* 仅本地开发生效。
# API 走 /api 同源相对路径(vite proxy 转发到本地后端 8086), # API 走 /api 同源相对路径(vite proxy 转发到本地后端 8086),
# 规避后端 CAS 过滤器不回 CORS 头导致的跨域拦截;SSO/基础平台用真实环境 # 规避后端 CAS 过滤器不回 CORS 头导致的跨域拦截;SSO/基础平台用真实环境
@@ -11,5 +11,6 @@ VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
VITE_IS_MOCK=false VITE_IS_MOCK=false
# 本地开发:站点标识与皮肤包(线上由 env-config 下发,build 产物不读 .env) # 本地开发:站点标识与皮肤包(线上由 env-config 下发,build 产物不读 .env)
VITE_SITE_CODE=yhms # siteCode=demo:本地 bsch_cms 的演示站点(与 CMS 本地种子数据对应)
VITE_SITE_CODE=demo
VITE_THEME=default VITE_THEME=default
-9
View File
@@ -1,9 +0,0 @@
# 本地纯前端调试模式:仅 vite dev --mode mock 加载(build 产物不读任何 .env.*,
# 线上由 /api/env-config?target=front 运行时下发)。后端 SSO/base 不可达时启用内置 mock
# 启动:npm run dev:mock
VITE_BASE_HOST=
VITE_SSO_HOST=
VITE_SERVICE_HOST=http://localhost:8101
VITE_ZIYUANKU_HOST=
VITE_ZHUANXIANG_HOST=
VITE_IS_MOCK=true
+1 -1
View File
@@ -89,7 +89,7 @@ push 到 `main` → gitea CI(`.gitea/workflows/build.yml`):`npm run build`
**首次上线前**需在 projman 应用 `svc-hall` 下新增前端目标 `hall`(路径片段 `hall`), **首次上线前**需在 projman 应用 `svc-hall` 下新增前端目标 `hall`(路径片段 `hall`),
并在配置管理里补 `env-config-hall.json`(`siteCode`、`theme`、`baseHost`、`ssoHost`), 并在配置管理里补 `env-config-hall.json`(`siteCode`、`theme`、`baseHost`、`ssoHost`),
再把 CMS 站点的 `svc_hall_base` 指向新前端(`https://<域名>/svc-hall/hall`)。详见 再把 CMS 站点的 `svc_hall_base` 指向新前端(`https://<域名>/svc-hall/hall`)。详见
`docs/公共端上线与projman接入说明.md`。 `../docs/用户面(hall)上线与projman接入说明.md`。
## 契约纪律 ## 契约纪律
-6
View File
@@ -1,6 +0,0 @@
//铃铛debug
window.conf = {
baseHost: "http://basedebug.bestsch.com",
ssoHost: "http://ssodebug.bestsch.com",
serviceHost: "http://basedebug.bestsch.com/QualityEvaluate",
};
+4 -4
View File
@@ -8,15 +8,15 @@
<!-- 运行时配置引导:在任何应用模块执行前同步拉取 /<contextPath>/api/env-config <!-- 运行时配置引导:在任何应用模块执行前同步拉取 /<contextPath>/api/env-config
(后端免登录端点,projman 配置管理在线维护),替代生产构建环境变量。 (后端免登录端点,projman 配置管理在线维护),替代生产构建环境变量。
必须同步:host.config.ts 在模块求值期就读 window.__APP_CONFIG__;失败/本地开发 必须同步:host.config.ts 在模块求值期就读 window.__APP_CONFIG__;失败/本地开发
(根路径,段数不足 2)则不设置:本地走 .env.dev/.env.mock(VITE_*),线上走同域推导兜底。 (根路径,段数不足 2)则不设置:本地走 .env.dev(VITE_*),线上走同域推导兜底。
target = URL 第 2 段目录名:本应用产物部署于 /{contextPath}/public/,故为 public, target = URL 第 2 段目录名:本应用产物部署于 /{contextPath}/hall/,故为 hall,
对应 projman 前端目标名 public(文件 /app/config/env-config-public.json)。 对应 projman 前端目标名 hall(文件 /app/config/env-config-hall.json)。
注意:同步 XHR 不支持 timeout 属性(赋值会抛 InvalidAccessError),依赖内网快速失败 --> 注意:同步 XHR 不支持 timeout 属性(赋值会抛 InvalidAccessError),依赖内网快速失败 -->
<script> <script>
(function () { (function () {
try { try {
var seg = location.pathname.split("/").filter(Boolean); var seg = location.pathname.split("/").filter(Boolean);
if (seg.length < 2) return; // 本地开发(根路径):走 .env.dev / .env.mock if (seg.length < 2) return; // 本地开发(根路径):走 .env.dev
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open("GET", "/" + seg[0] + "/api/env-config?target=" + encodeURIComponent(seg[1]), false); xhr.open("GET", "/" + seg[0] + "/api/env-config?target=" + encodeURIComponent(seg[1]), false);
xhr.send(null); xhr.send(null);
+77 -493
View File
File diff suppressed because it is too large Load Diff
-5
View File
@@ -6,11 +6,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --mode dev", "dev": "vite --mode dev",
"dev:mock": "vite --mode mock",
"build": "vite build --mode base", "build": "vite build --mode base",
"build:base": "vite build --mode base", "build:base": "vite build --mode base",
"build:ld": "vite build --mode ld",
"build:prod": "vite build --mode prod",
"preview": "vite preview", "preview": "vite preview",
"api:gen": "alova gen -f" "api:gen": "alova gen -f"
}, },
@@ -24,12 +21,10 @@
"axios": "^0.24.0", "axios": "^0.24.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"mockjs": "^1.1.0",
"pdfjs-dist": "^4.10.38", "pdfjs-dist": "^4.10.38",
"pinia": "^2.1.4", "pinia": "^2.1.4",
"sass": "^1.64.1", "sass": "^1.64.1",
"view-ui-plus": "^1.3.16", "view-ui-plus": "^1.3.16",
"vite-plugin-mock": "2.9.6",
"vue": "^3.4.19", "vue": "^3.4.19",
"vue-router": "4", "vue-router": "4",
"vuedraggable": "^4.1.0" "vuedraggable": "^4.1.0"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

+45 -3
View File
@@ -5,20 +5,31 @@
* 与后台前端(front/)刻意不同:不渲染平台模块菜单(app_module 是管理端契约), * 与后台前端(front/)刻意不同:不渲染平台模块菜单(app_module 是管理端契约),
* 只做两个入口——办事大厅、我的办件(详情/办理页从列表进入)。 * 只做两个入口——办事大厅、我的办件(详情/办理页从列表进入)。
* *
* 皮肤:启动时按运行时配置(env-config 的 theme/themeParams)应用皮肤包; * 皮肤三层优先级(T3 弱联动,2026-10-03):
* 未配置 → default;未注册 → default(整页回退,见 utils/theme.ts)。 * CMS 站点推荐(/api/hall/site 的 theme/themeParams,经 utils/cmsTheme 映射;
* stale-while-revalidate——localStorage 缓存先画、后台刷新,失败静默)
* > env-config 部署级皮肤 > default。
* 推荐值、不保证生效:任一层缺失/非法都回落下一层(utils/theme.ts)。
*/ */
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import { baseApi } from "@/api/module/base"; import { baseApi } from "@/api/module/base";
import config from "@/assets/config/host.config"; import config from "@/assets/config/host.config";
import { applyTheme, resolveTheme, type ThemeManifest } from "@/utils/theme"; 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 userName = ref("");
const theme = ref<ThemeManifest>(resolveTheme(null)); const theme = ref<ThemeManifest>(resolveTheme(null));
// CMS 指南页来路(T4 回跳闭环):仅 /#/apply 深链带合法 from 参数时非空
const { portalFrom, currentSiteCode } = useSite();
onMounted(async () => { onMounted(async () => {
// 皮肤先应用(首屏观感),再拉用户信息——用户信息失败不影响办理页面使用 // ① 基线:env-config 部署级皮肤(现状语义不变,先应用保证首屏观感)
theme.value = applyTheme(config.theme, config.themeParams); theme.value = applyTheme(config.theme, config.themeParams);
// ② 站点推荐:CMS 站点主题(弱联动)——缓存先画,后台刷新
applySiteThemeRecommend();
// 用户信息失败不影响办理页面使用
try { try {
const user: any = await baseApi.getUserInfo().send(); const user: any = await baseApi.getUserInfo().send();
userName.value = user?.name || ""; userName.value = user?.name || "";
@@ -29,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> </script>
<template> <template>
@@ -41,6 +82,7 @@ onMounted(async () => {
<nav class="sh-nav"> <nav class="sh-nav">
<router-link to="/hall">办事大厅</router-link> <router-link to="/hall">办事大厅</router-link>
<router-link to="/my">我的办件</router-link> <router-link to="/my">我的办件</router-link>
<a v-if="portalFrom" :href="portalFrom" class="sh-back-portal">返回门户</a>
</nav> </nav>
<span class="sh-user" v-if="userName">{{ userName }}</span> <span class="sh-user" v-if="userName">{{ userName }}</span>
</header> </header>
+4
View File
@@ -63,6 +63,10 @@ class HallApi {
filters(siteCode: string) { filters(siteCode: string) {
return get<HallFilters>(`${HALL}/filters`, { siteCode }); return get<HallFilters>(`${HALL}/filters`, { siteCode });
} }
/** 站点信息(含 CMS 主题 raw;主题弱联动数据源,失败静默——见 App.vue 应用链,T3) */
site(siteCode: string) {
return get(`${HALL}/site`, { siteCode });
}
} }
class ApplyApi { class ApplyApi {
+4 -4
View File
@@ -3,10 +3,10 @@ import { BaseConf } from "@/types/Basics";
/** /**
* 配置优先级(高 → 低): * 配置优先级(高 → 低):
* ① window.__APP_CONFIG__:线上运行时配置,index.html 引导脚本在应用模块加载前同步拉取 * ① window.__APP_CONFIG__:线上运行时配置,index.html 引导脚本在应用模块加载前同步拉取
* /<contextPath>/api/env-config?target=<部署目录名>(本应用部署于 /{contextPath}/front/, * /<contextPath>/api/env-config?target=<部署目录名>(本应用部署于 /{contextPath}/hall/,
* 故为 front,对应后端键 yh.env-config.source.front,projman 配置管理维护)写入 * 故为 hall,对应 projman 前端目标 hall / 文件 /app/config/env-config-hall.json)写入
* ② import.meta.env.VITE_*:仅本地开发/演示(vite dev --mode dev 读 .env.dev、--mode mock 读 .env.mock); * ② import.meta.env.VITE_*:仅本地开发/演示(vite dev --mode dev 读 .env.dev);
* build 产物不读任何 .env.*(.env.base/.env.ld/.env.prod 已删),VITE_* 恒为 undefined, * build 产物不读任何 .env.*(.env.base/.env.ld/.env.prod/.env.mock 已删),VITE_* 恒为 undefined,
* 本层自动跳过,产物不含任何环境值 * 本层自动跳过,产物不含任何环境值
* ③ 同域推导兜底:env-config 未配置/拉取失败时框架至少能渲染并调通同源 API; * ③ 同域推导兜底:env-config 未配置/拉取失败时框架至少能渲染并调通同源 API;
* ssoHost 为空 → 登出跳转不可用——与后端未就绪状态一致,可接受。 * ssoHost 为空 → 登出跳转不可用——与后端未就绪状态一致,可接受。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

+1 -12
View File
@@ -98,18 +98,7 @@ h4.subtitle {
} }
} }
.table-spin-load {
width: 60px;
height: 90px;
padding-top: 60px;
background-image: url(../image/loading.gif);
background-size: 60px 60px;
background-position: top center;
background-repeat: no-repeat;
border: none;
color: #577afb;
margin: 0 auto;
}
.ivu-table-cell-tree { .ivu-table-cell-tree {
margin-right: 4px; margin-right: 4px;
+33 -4
View File
@@ -13,6 +13,9 @@
--sh-text: #24292f; --sh-text: #24292f;
--sh-text-weak: #6b7280; --sh-text-weak: #6b7280;
--sh-border: #e6e9ef; --sh-border: #e6e9ef;
/* 语义色令牌(T6 令牌化:此前 ApplyDetail/tag 各写死一份) */
--sh-success: #1f9254;
--sh-danger: #d03050;
} }
html, html,
@@ -80,6 +83,12 @@ body {
background: rgba(255, 255, 255, 0.22); background: rgba(255, 255, 255, 0.22);
font-weight: 600; font-weight: 600;
} }
/* 返回门户(T4 回跳闭环):外跳文字链,与 nav 同风格白字,加分隔线区分站内导航。
方案原文写 var(--sh-primary),但 nav 位于主色底 header 上会隐形,故取 nav 现有风格。 */
.sh-header .sh-nav a.sh-back-portal {
border-left: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 0;
}
.sh-user { .sh-user {
margin-left: 16px; margin-left: 16px;
font-size: 13px; font-size: 13px;
@@ -130,13 +139,13 @@ body {
background: rgba(26, 92, 176, 0.06); background: rgba(26, 92, 176, 0.06);
} }
.sh-tag-success { .sh-tag-success {
color: #1f9254; color: var(--sh-success);
border-color: #1f9254; border-color: var(--sh-success);
background: rgba(31, 146, 84, 0.07); background: rgba(31, 146, 84, 0.07);
} }
.sh-tag-error { .sh-tag-error {
color: #d03050; color: var(--sh-danger);
border-color: #d03050; border-color: var(--sh-danger);
background: rgba(208, 48, 80, 0.07); background: rgba(208, 48, 80, 0.07);
} }
.sh-tag-warning { .sh-tag-warning {
@@ -154,3 +163,23 @@ body {
background: var(--sh-primary-dark); background: var(--sh-primary-dark);
border-color: 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 -1
View File
@@ -1,6 +1,11 @@
// utils 工具// 颜色 // utils 工具// 颜色
// (2026-10-03 T6:_varible.scss 已删——本文件仅剩 4 个色值依赖它,就地内联,
// 产物 CSS 与删除前逐字节一致;原文件唯一 import 形式为不带下划线的 "./varible")
$color-error: #e8684a;
$color-success: #45b972;
$color-warning: #e9b954;
$color-primary: #5979ea;
@import "./varible";
.bg-error { .bg-error {
background-color: $color-error !important; background-color: $color-error !important;
} }
-22
View File
@@ -1,22 +0,0 @@
/*scss变量*/
// 主题色
$color-error: #e8684a;
$color-success: #45b972;
$color-warning: #e9b954;
$color-primary: #5979ea;
// 文字色
$color-black: #404042;
$color-dark: #333;
$color-deep: #555;
$color-pl: #999999;
$color-weak: #b3b3b3;
$color-white: #fff;
// 背景色
$bg-bar: #f9f9f9;
$bg-page: #f3f3f3;
$bg-page-light: #f9f9f9;
$border-color: #e9e9e9;
+2 -2
View File
@@ -399,8 +399,8 @@ watch(
transition: color 0.2s, border-color 0.2s; transition: color 0.2s, border-color 0.2s;
} }
.dyn-file-btn:hover .dyn-file-btn-inner { .dyn-file-btn:hover .dyn-file-btn-inner {
color: #2d8cf0; color: var(--sh-primary);
border-color: #2d8cf0; border-color: var(--sh-primary);
} }
</style> </style>
+4 -2
View File
@@ -3,9 +3,11 @@ import type { RouteRecordRaw } from "vue-router";
/** /**
* 公共办事大厅路由(用户面;后台管理路由在 svc-hall/front,两仓互不引用)。 * 公共办事大厅路由(用户面;后台管理路由在 svc-hall/front,两仓互不引用)。
* 四个页面:事项大厅(入口)/ 在线办理 / 我的办件 / 办件详情。 * 四个页面:事项大厅(入口)/ 在线办理 / 我的办件 / 办件详情。
* 全部走 hash 路由(部署于 /{contextPath}/public/,hash 免服务端 rewrite)。 * 全部走 hash 路由(部署于 /{contextPath}/hall/,hash 免服务端 rewrite)。
* 参数页用 query 传参(与 CMS 指南页"我要办理"外链形态一致: * 参数页用 query 传参(与 CMS 指南页"我要办理"外链形态一致:
* {svc_hall_base}/#/apply?siteCode=&itemCode=)——拼成路径参数会 no-match 空白页。 * {svc_hall_base}/#/apply?siteCode=&itemCode=&from=<指南页绝对URL>;
* from=T4 回跳闭环参数,hall 侧 resolveFrom 校验可信域后显示"返回门户")
* ——拼成路径参数会 no-match 空白页。
*/ */
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
+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"] { :root[data-theme="campus-green"] {
--sh-primary: #0e8a6e; --sh-primary: #0e8a5f;
--sh-primary-dark: #0a6a55; --sh-primary-dark: #0a6244;
--sh-accent: #d97706; --sh-accent: #ff7a1a;
--sh-page-bg: #f2f8f5; --sh-page-bg: #f4f7f5;
--sh-radius: 12px; --sh-radius: 12px;
} }
+6 -6
View File
@@ -2,13 +2,13 @@
"specVersion": 1, "specVersion": 1,
"name": "campus-green", "name": "campus-green",
"title": "校园青绿", "title": "校园青绿",
"version": "1.0.0", "version": "1.1.0",
"description": "第二套可切换皮肤:青绿主色 + 更宽松的圆角,用于验证皮肤包机制(新增皮肤=新增目录,宿主零改动)。", "description": "第二套可切换皮肤:青绿主色 + 更宽松的圆角(色值与 CMS campus-glow 同源,T3 弱联动对齐 2026-10-03)。",
"params": [ "params": [
{ "key": "primary", "cssVar": "--sh-primary", "label": "主色", "type": "color", "default": "#0e8a6e" }, { "key": "primary", "cssVar": "--sh-primary", "label": "主色", "type": "color", "default": "#0e8a5f" },
{ "key": "primaryDark", "cssVar": "--sh-primary-dark", "label": "主色加深", "type": "color", "default": "#0a6a55" }, { "key": "primaryDark", "cssVar": "--sh-primary-dark", "label": "主色加深", "type": "color", "default": "#0a6244" },
{ "key": "accent", "cssVar": "--sh-accent", "label": "强调色", "type": "color", "default": "#d97706" }, { "key": "accent", "cssVar": "--sh-accent", "label": "强调色", "type": "color", "default": "#ff7a1a" },
{ "key": "pageBg", "cssVar": "--sh-page-bg", "label": "页面底色", "type": "color", "default": "#f2f8f5" }, { "key": "pageBg", "cssVar": "--sh-page-bg", "label": "页面底色", "type": "color", "default": "#f4f7f5" },
{ {
"key": "listLayout", "key": "listLayout",
"label": "事项列表布局", "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 };
}
+25 -1
View File
@@ -13,6 +13,8 @@ import config from "@/assets/config/host.config";
*/ */
const currentSiteCode = ref<string>(localStorage.getItem("svchall.public.site") || ""); const currentSiteCode = ref<string>(localStorage.getItem("svchall.public.site") || "");
const lastItemCode = ref<string>(localStorage.getItem("svchall.public.item") || ""); const lastItemCode = ref<string>(localStorage.getItem("svchall.public.item") || "");
/** CMS 指南页来路(session 级内存,不落 localStorage——回跳是"这一次来的路",不是偏好) */
const portalFrom = ref<string>("");
export function useSite() { export function useSite() {
/** 从 query / 运行时配置 / 本地记忆依次解析站点,命中即记住 */ /** 从 query / 运行时配置 / 本地记忆依次解析站点,命中即记住 */
@@ -29,6 +31,28 @@ export function useSite() {
return hit; return hit;
} }
/**
* 解析并校验 from 参数(CMS 指南页深链带回,T4 回跳闭环):
* 仅接受 http/https 且 hostname 属于本部署可信域(当前页域名或 baseHost)的 URL,否则忽略——
* 防 javascript: 伪协议与外域注入。
*/
function resolveFrom(queryFrom?: unknown): string {
const raw = typeof queryFrom === "string" ? queryFrom.trim() : "";
if (!raw) return "";
try {
const u = new URL(raw);
if (u.protocol !== "http:" && u.protocol !== "https:") return "";
const mine = location.hostname;
const base = String(config.baseHost || "");
const baseHost = base ? new URL(base).hostname : "";
if (u.hostname !== mine && (!baseHost || u.hostname !== baseHost)) return "";
portalFrom.value = raw;
} catch {
// 非法 URL 当没有
}
return portalFrom.value;
}
function setSite(siteCode: string) { function setSite(siteCode: string) {
currentSiteCode.value = siteCode; currentSiteCode.value = siteCode;
if (siteCode) { if (siteCode) {
@@ -44,5 +68,5 @@ export function useSite() {
} }
} }
return { currentSiteCode, lastItemCode, resolveSite, setSite, rememberItem }; return { currentSiteCode, lastItemCode, portalFrom, resolveSite, resolveFrom, setSite, rememberItem };
} }
+3 -1
View File
@@ -19,7 +19,7 @@ import { useSite } from "@/utils/site";
import DynamicForm from "@/components/DynamicForm.vue"; import DynamicForm from "@/components/DynamicForm.vue";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { resolveSite, rememberItem } = useSite(); const { resolveSite, resolveFrom, rememberItem } = useSite();
/** 轮询参数:代发起 + 回调一般在 1s 内完成,给到 ~9s 足够,避免用户盯着"待提交"发懵 */ /** 轮询参数:代发起 + 回调一般在 1s 内完成,给到 ~9s 足够,避免用户盯着"待提交"发懵 */
const POLL_TIMES = 12; const POLL_TIMES = 12;
@@ -37,6 +37,8 @@ const schemaJson = computed(() => (item.value?.schemaJson as string) || "");
async function load() { async function load() {
siteCode.value = resolveSite(route.query.siteCode); siteCode.value = resolveSite(route.query.siteCode);
// CMS 指南页来路(T4):校验通过即记入 session,App.vue 顶栏渲染"返回门户"
resolveFrom(route.query.from);
const itemCode = String(route.query.itemCode || ""); const itemCode = String(route.query.itemCode || "");
if (!siteCode.value || !itemCode) { if (!siteCode.value || !itemCode) {
loadError.value = "缺少 siteCode/itemCode 参数"; loadError.value = "缺少 siteCode/itemCode 参数";
+3 -3
View File
@@ -325,7 +325,7 @@ onMounted(load);
} }
.step-dot.active { .step-dot.active {
background: var(--sh-primary); background: var(--sh-primary);
box-shadow: 0 0 0 3px rgba(26, 92, 176, 0.15); box-shadow: 0 0 0 3px color-mix(in srgb, var(--sh-primary) 15%, transparent);
} }
.step-body { .step-body {
flex: 1; flex: 1;
@@ -354,10 +354,10 @@ onMounted(load);
font-weight: 500; font-weight: 500;
} }
.step-done .assignee-name { .step-done .assignee-name {
color: #1f9254; color: var(--sh-success);
} }
.step-reject .assignee-name { .step-reject .assignee-name {
color: #d03050; color: var(--sh-danger);
} }
.step-invalid { .step-invalid {
opacity: 0.55; opacity: 0.55;
+2 -14
View File
@@ -3,7 +3,6 @@ import { createHtmlPlugin } from "vite-plugin-html";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import path, { resolve } from "path"; import path, { resolve } from "path";
import { createSvgIconsPlugin } from "vite-plugin-svg-icons"; import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
import { viteMockServe } from "vite-plugin-mock";
import ViteAutoImport from "unplugin-auto-import/vite"; import ViteAutoImport from "unplugin-auto-import/vite";
import UnoCSS from "@unocss/vite"; import UnoCSS from "@unocss/vite";
@@ -27,11 +26,8 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
inject: "body-last", // 插入的位置 inject: "body-last", // 插入的位置
customDomId: "__svg__icons__dom__", // svg的id customDomId: "__svg__icons__dom__", // svg的id
}), }),
viteMockServe({ // vite-plugin-mock 已裁(2026-10-03 T6):mockPath 指向的 ./src/assets/mock 目录
mockPath: "./src/assets/mock", // 并不存在,dev:mock/.env.mock 一并移除——dev/联调/正式均走真实后端
// 仅 --mode mock 时启用 mock 中间件;dev/联调/正式均走真实后端
localEnabled: mode === "mock",
}),
ViteAutoImport({ ViteAutoImport({
imports: [ imports: [
"vue", "vue",
@@ -108,14 +104,6 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
}, },
}, },
}, },
// css预处理器
css: {
// preprocessorOptions: {
// scss: {
// additionalData: '@import "./src/assets/styles/utils/_varible.scss";',
// },
// },
},
base: "./", base: "./",
build: { build: {
target: "modules", //设置最终构建的浏览器兼容目标 //es2015(编译成es5) | modules target: "modules", //设置最终构建的浏览器兼容目标 //es2015(编译成es5) | modules