From bb5c5e513ffb934b09af28193465131d03ef2505 Mon Sep 17 00:00:00 2001 From: chenxing Date: Mon, 7 Sep 2026 20:14:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(B3):=20=E7=AB=99=E7=82=B9=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=BC=B9=E7=AA=97=E5=A2=9E=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E9=9D=A2=E6=9D=BF(config=5Fjson.custom=5Fcss?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 折叠面板 textarea(等宽字体/10240计数/示例提示),面板为唯一编辑入口 - custom_css 入 RESERVED_SITE_CONFIG_KEYS(结构化表单保留键,不与扩展行重复编辑) - vitest 25/25 绿(新增保留键往返用例)+build 成功 --- src/utils/siteConfigForm.spec.ts | 7 ++++ src/utils/siteConfigForm.ts | 2 +- src/views/cms/SiteManage.vue | 63 +++++++++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/utils/siteConfigForm.spec.ts b/src/utils/siteConfigForm.spec.ts index 2928e72..b8d6c32 100644 --- a/src/utils/siteConfigForm.spec.ts +++ b/src/utils/siteConfigForm.spec.ts @@ -115,4 +115,11 @@ describe("siteConfigForm", () => { "domain", "public_base_url", ]); }); + it("B3:custom_css 为保留键,不入扩展行,parse/serialize 无损往返", () => { + const json = JSON.stringify({ custom_css: ".site-header{padding:16px 0}", my_ext: "v" }); + const m = parseSiteConfig(json); + expect(m.extras).toEqual([{ key: "my_ext", value: "v" }]); + expect(m.preserved.custom_css).toBe(".site-header{padding:16px 0}"); + expect(JSON.parse(serializeSiteConfig(m)).custom_css).toBe(".site-header{padding:16px 0}"); + }); }); diff --git a/src/utils/siteConfigForm.ts b/src/utils/siteConfigForm.ts index 990fc45..d19b121 100644 --- a/src/utils/siteConfigForm.ts +++ b/src/utils/siteConfigForm.ts @@ -75,7 +75,7 @@ export const SITE_CONFIG_GROUPS: { name: SiteConfigGroup; title: string }[] = [ ]; /** 系统托管键:不进表单,解析时原样保留、序列化时无损回写(theme_params 由主题参数表单维护) */ -export const RESERVED_SITE_CONFIG_KEYS = ["theme_params"]; +export const RESERVED_SITE_CONFIG_KEYS = ["theme_params", "custom_css"]; /** 扩展键名约束(与后端 CONFIG_KEY_PATTERN 一致) */ export const EXTRA_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; diff --git a/src/views/cms/SiteManage.vue b/src/views/cms/SiteManage.vue index c289777..46d1ade 100644 --- a/src/views/cms/SiteManage.vue +++ b/src/views/cms/SiteManage.vue @@ -131,6 +131,9 @@ const form = reactive({ const editThemeParams = ref({}); const editTheme = computed(() => themeMeta.value[form.theme]); +/** B3 自定义样式(config_json.custom_css):面板为唯一编辑入口,SiteConfigForm 侧该键为保留键 */ +const customCss = ref(""); + /** 编辑内预览 */ const previewVisible = ref(false); @@ -192,11 +195,35 @@ function openEdit(row: SiteVo) { configJson: row.configJson || "", status: row.status, }); + customCss.value = readCustomCss(form.configJson); const t = editTheme.value; editThemeParams.value = t ? themeParamsFromConfigJson(row.configJson, t) : {}; modalVisible.value = true; } +function readCustomCss(configJson: string): string { + try { + const obj = configJson && configJson.trim() ? JSON.parse(configJson) : {}; + return obj && typeof obj === "object" && typeof obj.custom_css === "string" ? obj.custom_css : ""; + } catch { + return ""; + } +} + +/** 把面板值写入/移出 configJson(空值移除键;与 SiteConfigForm 的 preserved 往返兼容) */ +function withCustomCss(configJson: string, css: string): string { + let obj: Record; + try { + obj = configJson && configJson.trim() ? JSON.parse(configJson) : {}; + if (!obj || typeof obj !== "object" || Array.isArray(obj)) obj = {}; + } catch { + obj = {}; + } + if (css.trim()) obj.custom_css = css; + else delete obj.custom_css; + return JSON.stringify(obj); +} + function pickTheme(name: string) { if (form.theme === name) return; form.theme = name; @@ -213,6 +240,7 @@ async function save() { try { const t = editTheme.value; const themeParams = t ? editThemeParams.value : {}; + const baseConfig = withCustomCss(form.configJson || "", customCss.value); await siteApi .save({ id: form.id, @@ -223,7 +251,7 @@ async function save() { schName: isSchoolAdmin.value ? undefined : form.schName || undefined, name: form.name, theme: form.theme || undefined, - configJson: t ? mergeThemeParamsIntoConfigJson(form.configJson || "", t, themeParams) : form.configJson || undefined, + configJson: t ? mergeThemeParamsIntoConfigJson(baseConfig, t, themeParams) : baseConfig || undefined, status: form.status, }) .send(); @@ -332,6 +360,23 @@ watch(() => store.currentSchool?.schId, () => run()); + + + + +
+ {{ customCss.length }}/10240 字符 + 保存后门户立即生效(缓存自动失效);服务端将拒绝 </style、<script、@import、expression( 等危险内容 +
+
+