diff --git a/.env.base b/.env.base index 68361e1..d9dc213 100644 --- a/.env.base +++ b/.env.base @@ -1,8 +1,8 @@ # 正式环境 - base VITE_BASE_HOST=http://base.yunhaixingtong.com VITE_SSO_HOST=http://sso.yunhaixingtong.com -# API 基址用相对 "/cms"(同源 + context-path):SPA 由 cms-app 自带(/cms/admin), -# base 域/Caddy 与学校域 vhost 都按路径反代 /cms → 同源恒成立; +# API 基址用相对 "/yschcms"(同源 + context-path):projman 部署的服务上下文为 /yschcms, +# base 域网关按路径反代 /yschcms → 同源恒成立; # 写死绝对域名会导致 localhost 直连、换域名等场景全部"加载中"卡死(day2 修复) -VITE_SERVICE_HOST=/cms +VITE_SERVICE_HOST=/yschcms VITE_IS_MOCK=false diff --git a/.env.prod b/.env.prod index 9c1c8e0..a13f549 100644 --- a/.env.prod +++ b/.env.prod @@ -1,6 +1,6 @@ # 生产环境(与 base 同源域名) VITE_BASE_HOST=http://base.yunhaixingtong.com VITE_SSO_HOST=http://sso.yunhaixingtong.com -# 相对 "/cms"(同源 + context-path),理由见 .env.base 注释 -VITE_SERVICE_HOST=/cms +# 相对 "/yschcms"(同源 + context-path,projman 部署注入),理由见 .env.base 注释 +VITE_SERVICE_HOST=/yschcms VITE_IS_MOCK=false diff --git a/src/api/module/cms.ts b/src/api/module/cms.ts index 3caacf0..7abd0d1 100644 --- a/src/api/module/cms.ts +++ b/src/api/module/cms.ts @@ -22,7 +22,7 @@ import { /** * CMS 管理端 API(手写 class 风格,同 Notice front 约定)。 - * baseURL = conf.serviceHost(dev 为空串走 vite 代理;生产 = 源 + /cms 上下文)。 + * baseURL = conf.serviceHost(dev 为空串走 vite 代理;生产 = 源 + /yschcms 上下文)。 */ const AUTH = "/api/auth"; diff --git a/src/router/routes.ts b/src/router/routes.ts index 28a5235..e4faad4 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -2,7 +2,7 @@ import { RouteRecordRaw } from "vue-router"; import { useCmsStore } from "@/store"; /** - * CMS 管理端路由(hash 模式,部署于 /cms/admin 子路径)。 + * CMS 管理端路由(hash 模式,部署于 /yschcms/admin 子路径)。 * 学校化管理双层壳(docs/09 §3.3): * 一级 /workbench 学校工作台(无壳全屏);二级 /sch/:schCode/* 学校专属管理壳(SchoolLayout)。 * 站点/学校上下文经 localStorage 持久化,各页面从 store 读取。 diff --git a/src/utils/menuRoute.ts b/src/utils/menuRoute.ts index 63222ff..39c5650 100644 --- a/src/utils/menuRoute.ts +++ b/src/utils/menuRoute.ts @@ -111,8 +111,9 @@ function matchRoute(candidate: string): string | null { // 1. 绝对路径精确匹配 const exact = ABSOLUTE_PATHS.find(p => p.toLowerCase() === lower); if (exact) return exact; - // 2. 学校壳内子路径匹配(/article → /sch/{code}/article),兼容旧 /cms 前缀 - const stripped = lower.startsWith("/cms/") ? lower.slice("/cms".length) : lower; + // 2. 学校壳内子路径匹配(/article → /sch/{code}/article),兼容新旧上下文前缀(历史 /cms、现网 /yschcms) + const stripped = lower.startsWith("/cms/") ? lower.slice("/cms".length) + : lower.startsWith("/yschcms/") ? lower.slice("/yschcms".length) : lower; const entry = ROUTE_ENTRIES.find(e => !e.absolute && e.sub.toLowerCase() === stripped); if (entry) { return toConcrete(entry); @@ -137,10 +138,10 @@ function matchByName(name?: string | null): RouteEntry | null { return hits.length === 1 ? hits[0] : null; } -/** 是否为本应用入口类地址(/、/cms、…/index.html):只表示"打开应用", +/** 是否为本应用入口类地址(/、/cms、/yschcms、…/index.html):只表示"打开应用", * 不携带页面信息,不能当作有效跳转目标 */ function isAppEntryCandidate(candidate: string): boolean { - return !candidate || candidate === "/" || candidate === "/cms"; + return !candidate || candidate === "/" || candidate === "/cms" || candidate === "/yschcms"; } export interface ResolvedMenuPage { diff --git a/src/utils/siteConfigForm.ts b/src/utils/siteConfigForm.ts index 2ad20c2..990fc45 100644 --- a/src/utils/siteConfigForm.ts +++ b/src/utils/siteConfigForm.ts @@ -23,12 +23,12 @@ export interface SiteConfigFieldDef { export const SITE_CONFIG_FIELD_DEFS: SiteConfigFieldDef[] = [ { key: "logo", label: "站点 Logo", group: "display", kind: "asset", maxLength: 500, - placeholder: "如 /cms/upload/2026/06/logo.png 或 https://…", + placeholder: "如 /yschcms/upload/2026/06/logo.png 或 https://…", tip: "门户顶栏与页脚品牌位展示;建议先在「附件管理」上传,复制路径填到这里。", }, { key: "favicon", label: "浏览器图标 favicon", group: "display", kind: "asset", maxLength: 500, - placeholder: "如 /cms/upload/…/favicon.png(留空用主题内置图标)", + placeholder: "如 /yschcms/upload/…/favicon.png(留空用主题内置图标)", }, { key: "slogan", label: "首页标语", group: "display", kind: "text", maxLength: 200, @@ -63,7 +63,7 @@ export const SITE_CONFIG_FIELD_DEFS: SiteConfigFieldDef[] = [ { key: "public_base_url", label: "对外基地址", group: "domain", kind: "url", maxLength: 500, placeholder: "如 https://www.xxx.edu.cn", - tip: "绑定独立域名后必填;未填时全站链接走 base 域子路径形态(/cms/portal/{站点编码})。", + tip: "绑定独立域名后必填;未填时全站链接走 base 域子路径形态(/yschcms/portal/{站点编码})。", }, ]; diff --git a/src/utils/themeParamForm.spec.ts b/src/utils/themeParamForm.spec.ts index ac76eae..6aaff0f 100644 --- a/src/utils/themeParamForm.spec.ts +++ b/src/utils/themeParamForm.spec.ts @@ -62,16 +62,19 @@ describe("themeParamForm(方案 §6.3/§8.4)", () => { expect(COLOR_RE.test("javascript:alert(1)")).toBe(false); }); - it("T29: select 枚举外拒绝;image 非 /cms/upload/** 拒绝;空值一律不合法", () => { + it("T29: select 枚举外拒绝;image 仅放行 /cms|/yschcms + /upload/** 前缀;空值一律不合法", () => { const t = theme(); const select = t.params.find(p => p.key === "headerStyle")!; const image = t.params.find(p => p.key === "logoImage")!; const color = t.params.find(p => p.key === "primary")!; expect(isValidParamValue(select, "plain")).toBe(true); expect(isValidParamValue(select, "hacker")).toBe(false); - // B-7:与应用真实路径对齐(context-path=/cms),旧 /upload/** 前缀不再放行 + // B-7:与应用真实路径对齐,旧 /upload/** 前缀不再放行; + // 兼容历史 /cms/upload/**(本地/旧数据)与现网 /yschcms/upload/**(projman 部署 context-path) expect(isValidParamValue(image, "/cms/upload/cms/logo.png")).toBe(true); + expect(isValidParamValue(image, "/yschcms/upload/cms/logo.png")).toBe(true); expect(isValidParamValue(image, "/upload/cms/logo.png")).toBe(false); + expect(isValidParamValue(image, "/yschcmsupload/x.png")).toBe(false); expect(isValidParamValue(image, "https://evil/x.png")).toBe(false); expect(isValidParamValue(color, "")).toBe(false); expect(isValidParamValue(color, undefined)).toBe(false); diff --git a/src/utils/themeParamForm.ts b/src/utils/themeParamForm.ts index 32a8458..39677dd 100644 --- a/src/utils/themeParamForm.ts +++ b/src/utils/themeParamForm.ts @@ -24,8 +24,9 @@ export function isValidParamValue(param: ThemeParamVo, value: string | undefined case "color": return COLOR_RE.test(value); case "image": - // B-7:与后端对齐——附件 webUrl 相对形态含 context-path(/cms/upload/**) - return value.startsWith("/cms/upload/"); + // B-7:与后端对齐——附件 webUrl 相对形态含 context-path; + // 兼容历史 /cms/upload/** 与现网 /yschcms/upload/**(projman 部署后 context-path 变更) + return value.startsWith("/cms/upload/") || value.startsWith("/yschcms/upload/"); case "select": return !!param.options?.some(o => o.value === value); case "text": diff --git a/vite.config.ts b/vite.config.ts index bb8c124..c4cca51 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,7 +15,7 @@ export default defineConfig(({ command, mode }) => { // VITE_IS_MOCK=true 时启用本地 mock 中间件(拦截 /api、/portal 等,无需后端) const mockEnabled = loadEnv(mode, process.cwd(), "VITE_").VITE_IS_MOCK === "true"; return { - base: "./", // 相对路径 + hash 路由:支持部署到 /cms/admin 任意子路径 + base: "./", // 相对路径 + hash 路由:支持部署到 /yschcms/admin 任意子路径 plugins: [ ...(mockEnabled ? [cmsMockPlugin()] : []), vue(),