diff --git a/src/App.vue b/src/App.vue index d60ea30..d64fb01 100644 --- a/src/App.vue +++ b/src/App.vue @@ -12,9 +12,12 @@ 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 { useSite } from "@/utils/site"; const userName = ref(""); const theme = ref(resolveTheme(null)); +// CMS 指南页来路(T4 回跳闭环):仅 /#/apply 深链带合法 from 参数时非空 +const { portalFrom } = useSite(); onMounted(async () => { // 皮肤先应用(首屏观感),再拉用户信息——用户信息失败不影响办理页面使用 @@ -41,6 +44,7 @@ onMounted(async () => { {{ userName }} diff --git a/src/assets/styles/theme-base.css b/src/assets/styles/theme-base.css index aa1d262..969d745 100644 --- a/src/assets/styles/theme-base.css +++ b/src/assets/styles/theme-base.css @@ -80,6 +80,12 @@ body { background: rgba(255, 255, 255, 0.22); 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 { margin-left: 16px; font-size: 13px; diff --git a/src/utils/site.ts b/src/utils/site.ts index 876ed2c..b6d4c27 100644 --- a/src/utils/site.ts +++ b/src/utils/site.ts @@ -13,6 +13,8 @@ import config from "@/assets/config/host.config"; */ const currentSiteCode = ref(localStorage.getItem("svchall.public.site") || ""); const lastItemCode = ref(localStorage.getItem("svchall.public.item") || ""); +/** CMS 指南页来路(session 级内存,不落 localStorage——回跳是"这一次来的路",不是偏好) */ +const portalFrom = ref(""); export function useSite() { /** 从 query / 运行时配置 / 本地记忆依次解析站点,命中即记住 */ @@ -29,6 +31,28 @@ export function useSite() { 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) { currentSiteCode.value = siteCode; if (siteCode) { @@ -44,5 +68,5 @@ export function useSite() { } } - return { currentSiteCode, lastItemCode, resolveSite, setSite, rememberItem }; + return { currentSiteCode, lastItemCode, portalFrom, resolveSite, resolveFrom, setSite, rememberItem }; } diff --git a/src/views/Apply.vue b/src/views/Apply.vue index ad02838..4f7662d 100644 --- a/src/views/Apply.vue +++ b/src/views/Apply.vue @@ -19,7 +19,7 @@ import { useSite } from "@/utils/site"; import DynamicForm from "@/components/DynamicForm.vue"; const route = useRoute(); const router = useRouter(); -const { resolveSite, rememberItem } = useSite(); +const { resolveSite, resolveFrom, rememberItem } = useSite(); /** 轮询参数:代发起 + 回调一般在 1s 内完成,给到 ~9s 足够,避免用户盯着"待提交"发懵 */ const POLL_TIMES = 12; @@ -37,6 +37,8 @@ const schemaJson = computed(() => (item.value?.schemaJson as string) || ""); async function load() { siteCode.value = resolveSite(route.query.siteCode); + // CMS 指南页来路(T4):校验通过即记入 session,App.vue 顶栏渲染"返回门户" + resolveFrom(route.query.from); const itemCode = String(route.query.itemCode || ""); if (!siteCode.value || !itemCode) { loadError.value = "缺少 siteCode/itemCode 参数";