From c77be8b5d8d50fc86970a07e6ef4169eb019e04d Mon Sep 17 00:00:00 2001 From: chenxing Date: Mon, 7 Sep 2026 18:59:27 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=89=8D=E7=BD=AE=E5=BF=AB=E7=85=A7-a?= =?UTF-8?q?ssetUrl=E5=89=8D=E7=BC=80=E4=B8=8E=E5=AD=A6=E6=A0=A1=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E6=94=B6=E6=95=9B=E4=BC=9A=E8=AF=9D=E9=81=97=E7=95=99?= =?UTF-8?q?(=E6=89=A7=E8=A1=8C=E6=96=B9=E6=A1=88B1~B5=E5=89=8D=E6=97=A2?= =?UTF-8?q?=E6=9C=89=E6=9C=AA=E6=8F=90=E4=BA=A4=E5=8F=98=E6=9B=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/module/cms.spec.ts | 101 +++++++++++++++++++++ src/api/module/cms.ts | 27 +++--- src/components/SchoolLayout.vue | 126 ++++++--------------------- src/components/theme/ThemePicker.vue | 4 +- src/views/cms/SchoolWorkbench.vue | 64 +++++++++----- src/views/cms/SiteManage.vue | 47 +++++++++- 6 files changed, 232 insertions(+), 137 deletions(-) create mode 100644 src/api/module/cms.spec.ts diff --git a/src/api/module/cms.spec.ts b/src/api/module/cms.spec.ts new file mode 100644 index 0000000..a06e243 --- /dev/null +++ b/src/api/module/cms.spec.ts @@ -0,0 +1,101 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +/** + * 路径参数型 POST 接口的 siteId 透传回归(publish 曾因 postJSON 丢第三参, + * 请求裸发 POST /api/article/publish/{id},后端报「缺少必填参数:siteId」)。 + * alova 实例与 host.config(顶层 document 依赖)均 mock,node 环境纯断言。 + * host.config 用 vi.hoisted 可变对象:assetUrl 用例需要切换 serviceHost 模拟生产 /yschcms。 + */ +const confState = vi.hoisted(() => ({ + conf: { baseHost: "", ssoHost: "", serviceHost: "", isMock: false, isDebug: false }, +})); + +vi.mock("@/assets/config/host.config", () => ({ + default: confState.conf, +})); + +vi.mock("@/utils/alova/alova-iview", () => ({ + alovaInstance: { + Get: vi.fn(), + Post: vi.fn(() => ({})), + }, +})); + +import { articleApi, assetUrl } from "./cms"; +import { alovaInstance } from "@/utils/alova/alova-iview"; + +const JSON_HEADERS = { "Content-Type": "application/json;charset=UTF-8" }; + +describe("cms.ts 路径参数型 POST 的 query 透传", () => { + beforeEach(() => { + vi.mocked(alovaInstance.Post).mockClear(); + }); + + it("publish: siteId 以 query 参数随 Post config 发出", () => { + articleApi.publish(5, 2); + expect(alovaInstance.Post).toHaveBeenCalledWith("/api/article/publish/2", undefined, { + headers: JSON_HEADERS, + params: { siteId: 5 }, + }); + }); + + it("offline: 同款透传", () => { + articleApi.offline(5, 3); + expect(alovaInstance.Post).toHaveBeenCalledWith("/api/article/offline/3", undefined, { + headers: JSON_HEADERS, + params: { siteId: 5 }, + }); + }); + + it("topmost: siteId 与 enable 一并透传", () => { + articleApi.topmost(5, 3, true); + expect(alovaInstance.Post).toHaveBeenCalledWith("/api/article/topmost/3", undefined, { + headers: JSON_HEADERS, + params: { siteId: 5, enable: true }, + }); + }); + + it("save/delete: siteId 走 body,不带 params(守卫勿误伤)", () => { + articleApi.delete({ id: 2, siteId: 5 }); + expect(alovaInstance.Post).toHaveBeenCalledWith("/api/article/delete", { id: 2, siteId: 5 }, { + headers: JSON_HEADERS, + params: undefined, + }); + }); +}); + +describe("assetUrl 站内资产路径补 serviceHost 前缀(线上主题截图破图回归)", () => { + afterEach(() => { + confState.conf.serviceHost = ""; + confState.conf.isMock = false; + }); + + it("生产:/api 裸路径拼上 context-path(/yschcms)", () => { + confState.conf.serviceHost = "/yschcms"; + expect(assetUrl("/api/theme/default/asset/screenshot.png")).toBe( + "/yschcms/api/theme/default/asset/screenshot.png" + ); + }); + + it("dev/mock:serviceHost 为空串时原样返回", () => { + expect(assetUrl("/api/theme/default/asset/screenshot.png")).toBe( + "/api/theme/default/asset/screenshot.png" + ); + }); + + it("绝对地址(http/data/blob)不拼前缀", () => { + confState.conf.serviceHost = "/yschcms"; + expect(assetUrl("https://cdn.example.com/x.png")).toBe("https://cdn.example.com/x.png"); + expect(assetUrl("data:image/png;base64,AAAA")).toBe("data:image/png;base64,AAAA"); + expect(assetUrl("blob:http://localhost/uuid")).toBe("blob:http://localhost/uuid"); + }); + + it("空值原样返回;isMock 时不拼 serviceHost", () => { + expect(assetUrl("")).toBe(""); + confState.conf.serviceHost = "/yschcms"; + confState.conf.isMock = true; + expect(assetUrl("/api/theme/default/asset/screenshot.png")).toBe( + "/api/theme/default/asset/screenshot.png" + ); + }); +}); diff --git a/src/api/module/cms.ts b/src/api/module/cms.ts index 7abd0d1..64cb180 100644 --- a/src/api/module/cms.ts +++ b/src/api/module/cms.ts @@ -41,10 +41,11 @@ function get(url: string, params?: IObj): Method { return alovaInstance.Get(url, { params }); } -/** POST JSON(@RequestBody) */ -function postJSON(url: string, data?: unknown): Method { +/** POST JSON(@RequestBody);params 为追加到 URL 的 query 参数(alova Post config 同 Get) */ +function postJSON(url: string, data?: unknown, params?: IObj): Method { return alovaInstance.Post(url, data, { headers: { "Content-Type": "application/json;charset=UTF-8" }, + params, }); } @@ -58,6 +59,16 @@ export function portalBase(siteCode: string): string { return `${conf.isMock ? "" : conf.serviceHost}/portal/${siteCode}`; } +/** + * 后端下发的站内资产路径补 serviceHost 前缀(如主题截图 /api/theme/{name}/asset/screenshot.png)。 + * 生产 context-path=/yschcms,裸 /api 绝对路径会打到网关根上 404(破图); + * dev/mock 下 serviceHost 为空串,拼接后与原路径一致不受影响。 + */ +export function assetUrl(url: string): string { + if (!url || /^(https?:|data:|blob:)/i.test(url)) return url; + return `${conf.isMock ? "" : conf.serviceHost}${url}`; +} + class AuthApi { /** 登录态:用户 + 可管理站点 + 是否超管 */ getUser() { @@ -137,21 +148,15 @@ class ArticleApi { /** 发布(publishTime 为未来时间即定时发布) */ publish(siteId: number, id: number) { - return postJSON(`${ARTICLE}/publish/${id}`, undefined, { - params: { siteId }, - }); + return postJSON(`${ARTICLE}/publish/${id}`, undefined, { siteId }); } offline(siteId: number, id: number) { - return postJSON(`${ARTICLE}/offline/${id}`, undefined, { - params: { siteId }, - }); + return postJSON(`${ARTICLE}/offline/${id}`, undefined, { siteId }); } topmost(siteId: number, id: number, enable: boolean) { - return postJSON(`${ARTICLE}/topmost/${id}`, undefined, { - params: { siteId, enable }, - }); + return postJSON(`${ARTICLE}/topmost/${id}`, undefined, { siteId, enable }); } delete(data: DeleteRequest) { diff --git a/src/components/SchoolLayout.vue b/src/components/SchoolLayout.vue index e1bfcb8..8efbe23 100644 --- a/src/components/SchoolLayout.vue +++ b/src/components/SchoolLayout.vue @@ -1,7 +1,8 @@