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