From f636075a5e20a87125eb0db9decb6ee07351cab0 Mon Sep 17 00:00:00 2001 From: chenxing Date: Sat, 3 Oct 2026 13:29:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(apply):=20=E6=B7=B1=E9=93=BE=20from=20?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E8=A7=A3=E6=9E=90=20+=20=E9=A1=B6=E6=A0=8F?= =?UTF-8?q?=E3=80=8C=E8=BF=94=E5=9B=9E=E9=97=A8=E6=88=B7=E3=80=8D=EF=BC=88?= =?UTF-8?q?T4=EF=BC=89=E2=80=94=E2=80=94site.ts=20=E6=96=B0=E5=A2=9E=20res?= =?UTF-8?q?olveFrom=EF=BC=88=E4=BB=85=20http/https=20=E4=B8=94=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E5=B1=9E=E6=9C=AC=E9=83=A8=E7=BD=B2=E5=8F=AF=E4=BF=A1?= =?UTF-8?q?=E5=9F=9F=EF=BC=8C=E9=98=B2=E4=BC=AA=E5=8D=8F=E8=AE=AE/?= =?UTF-8?q?=E5=A4=96=E5=9F=9F=E6=B3=A8=E5=85=A5=EF=BC=9Bsession=20?= =?UTF-8?q?=E7=BA=A7=E5=86=85=E5=AD=98=E4=B8=8D=E8=90=BD=20localStorage?= =?UTF-8?q?=EF=BC=89=EF=BC=8CApply.load=20=E5=9C=A8=20resolveSite=20?= =?UTF-8?q?=E5=90=8E=E8=B0=83=E7=94=A8=EF=BC=8CApp.vue=20nav=20=E6=8C=89?= =?UTF-8?q?=E9=9C=80=E6=B8=B2=E6=9F=93=E8=BF=94=E5=9B=9E=E9=97=A8=E6=88=B7?= =?UTF-8?q?=E9=93=BE=E6=8E=A5=EF=BC=88nav=20=E4=B8=BA=E4=B8=BB=E8=89=B2?= =?UTF-8?q?=E5=BA=95=20header=EF=BC=8C=E7=99=BD=E5=AD=97+=E5=88=86?= =?UTF-8?q?=E9=9A=94=E7=BA=BF=E4=B8=8E=E6=97=A2=E6=9C=89=E9=A3=8E=E6=A0=BC?= =?UTF-8?q?=E4=B8=80=E8=87=B4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 4 ++++ src/assets/styles/theme-base.css | 6 ++++++ src/utils/site.ts | 26 +++++++++++++++++++++++++- src/views/Apply.vue | 4 +++- 4 files changed, 38 insertions(+), 2 deletions(-) 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 参数";