fix(API基址): VITE_SERVICE_HOST /cms→/yschcms 对齐projman部署上下文;upload校验/菜单剥离兼容新旧前缀
build / build (push) Successful in 2m16s

This commit is contained in:
chenxing
2026-09-07 09:35:11 +08:00
parent 82eb067b19
commit 506fae0097
9 changed files with 24 additions and 19 deletions
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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 读取。
+5 -4
View File
@@ -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 {
+3 -3
View File
@@ -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/{站点编码})。",
},
];
+5 -2
View File
@@ -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);
+3 -2
View File
@@ -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":
+1 -1
View File
@@ -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(),