diff --git a/src/__tests__/siteVisualAid.spec.ts b/src/__tests__/siteVisualAid.spec.ts
new file mode 100644
index 0000000..27c6cf3
--- /dev/null
+++ b/src/__tests__/siteVisualAid.spec.ts
@@ -0,0 +1,107 @@
+// @vitest-environment jsdom
+/**
+ * F5(docs/20)页脚/基础展示视觉位置示意:
+ * ① 页脚分组底部渲染 FooterPreview 结构示意图(六处位置标注,已填内容回显);
+ * ② asset 字段的 aspect 占位框(favicon 1:1 / 二维码 4:3,无值虚线框、有值比例预览)。
+ */
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { createApp, defineComponent, h, nextTick, ref } from "vue";
+import ViewUIPlus from "view-ui-plus";
+
+vi.mock("@/api/module/cms", () => ({
+ assetUrl: (u: string) => u,
+ uploadUrl: () => "/api/attachment/upload",
+}));
+
+import SiteConfigForm from "@/views/cms/components/SiteConfigForm.vue";
+
+const cleanups: (() => void)[] = [];
+async function settle(frames = 4) {
+ for (let i = 0; i < frames; i++) {
+ await nextTick();
+ }
+ await new Promise(r => setTimeout(r, 0));
+ await nextTick();
+}
+
+const FOOTER_CONFIG = JSON.stringify({
+ contact: "电话 0571-88888888",
+ footer_addresses: "潭山校区:南湖路 237 号\n淮河校区:大学路 1 号",
+ qr_wechat: "/cms/upload/qr-wechat.png",
+ social_weibo: "https://weibo.com/xxx",
+ copyright: "XX学校 · 版权所有 © 2026",
+ beian: "浙ICP备XXXXXXXX号-1",
+ logo: "/cms/upload/logo.png",
+});
+
+async function mountForm(configJson: string, groups?: string[]) {
+ const el = document.createElement("div");
+ document.body.appendChild(el);
+ const model = ref(configJson);
+ const app = createApp(
+ defineComponent({
+ render: () =>
+ h(SiteConfigForm, {
+ modelValue: model.value,
+ groups,
+ "onUpdate:modelValue": (v: string) => (model.value = v),
+ }),
+ }),
+ );
+ app.use(ViewUIPlus);
+ app.mount(el);
+ cleanups.push(() => {
+ app.unmount();
+ el.remove();
+ });
+ await settle();
+ return el;
+}
+
+afterEach(() => {
+ cleanups.splice(0).forEach(fn => fn());
+});
+
+describe("F5 视觉位置示意(docs/20)", () => {
+ it("页脚分组:底部渲染结构示意图,六处位置标注 + 已填内容回显", async () => {
+ const el = await mountForm(FOOTER_CONFIG, ["footer"]);
+ const preview = el.querySelector(".footer-preview")!;
+ expect(preview).toBeTruthy();
+ const text = preview.textContent || "";
+ // 已填字段实时回显内容(而非占位标注)
+ expect(text).toContain("电话 0571-88888888");
+ expect(text).toContain("潭山校区:南湖路 237 号");
+ expect(text).toContain("XX学校 · 版权所有 © 2026");
+ expect(text).toContain("浙ICP备XXXXXXXX号-1");
+ expect(preview.querySelector('img[src="/cms/upload/qr-wechat.png"]')).toBeTruthy();
+ // 未填字段显示位置标注(微博二维码空 → 占位)
+ expect(text).toContain("微博二维码");
+ expect(text).toContain("社媒图标行");
+ });
+
+ it("页脚全空配置:六处位置标注完整(① 联系方式/② 地址/③ 二维码/④ 社媒/⑤ 版权/⑥ 备案)", async () => {
+ const el = await mountForm("{}", ["footer"]);
+ const text = el.querySelector(".footer-preview")!.textContent || "";
+ expect(text).toContain("① 联系方式");
+ expect(text).toContain("② 多校区地址");
+ expect(text).toContain("微信二维码");
+ expect(text).toContain("③ 4:3");
+ expect(text).toContain("④ 社媒图标行");
+ expect(text).toContain("⑤ 版权文字");
+ expect(text).toContain("⑥ 备案号");
+ });
+
+ it("基础展示分组:无示意图(仅页脚有)", async () => {
+ const el = await mountForm(FOOTER_CONFIG, ["display"]);
+ expect(el.querySelector(".footer-preview")).toBeFalsy();
+ });
+
+ it("aspect 占位框:二维码无值时 4:3 虚线占位、favicon 无值时 1:1 占位", async () => {
+ const el = await mountForm("{}", ["display", "footer"]);
+ const holders = el.querySelectorAll(".iaf-holder");
+ expect(holders.length).toBe(3); // favicon 1:1 + 微信/微博 4:3
+ expect(el.querySelector(".iaf-holder-1x1")).toBeTruthy();
+ expect(el.querySelectorAll(".iaf-holder-4x3").length).toBe(2);
+ expect(holders[0].textContent).toContain("1:1");
+ });
+});
diff --git a/src/components/FooterPreview.vue b/src/components/FooterPreview.vue
new file mode 100644
index 0000000..ab2a752
--- /dev/null
+++ b/src/components/FooterPreview.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
diff --git a/src/components/ImageAssetField.vue b/src/components/ImageAssetField.vue
index 4ec27ad..d998ad6 100644
--- a/src/components/ImageAssetField.vue
+++ b/src/components/ImageAssetField.vue
@@ -15,6 +15,9 @@ const props = defineProps<{
/** 附件场景(空=不带 scene;svc 场景已迁办事大厅,CMS 内为 article/banner 等) */
scene?: string;
placeholder?: string;
+ /** F5(docs/20):预览/占位框比例("1:1" favicon / "4:3" 二维码;缺省 8:5 通用横图),
+ * 无值时按比例显示虚线占位框,帮助确认最终在门户中的尺寸感 */
+ aspect?: "1:1" | "4:3";
}>();
const emit = defineEmits<{ (e: "update:modelValue", v: string): void }>();
@@ -76,12 +79,16 @@ function clearValue() {
>
-
+
+
+
+ {{ aspect }} 位
+
@@ -123,6 +130,16 @@ function clearValue() {
border: 1px solid #dcdee2;
background: #f8f8f9;
}
+ &.iaf-thumb-1x1 img {
+ width: 48px;
+ height: 48px;
+ object-fit: contain;
+ }
+ &.iaf-thumb-4x3 img {
+ width: 88px;
+ height: 66px;
+ object-fit: contain;
+ }
.iaf-thumb-del {
position: absolute;
top: -7px;
@@ -142,6 +159,24 @@ function clearValue() {
display: block;
}
}
+ .iaf-holder {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px dashed #cfd8e6;
+ border-radius: 4px;
+ color: #b4bfce;
+ font-size: 11px;
+ background: #fafbfd;
+ &.iaf-holder-1x1 {
+ width: 48px;
+ height: 48px;
+ }
+ &.iaf-holder-4x3 {
+ width: 88px;
+ height: 66px;
+ }
+ }
.iaf-manual {
max-width: 420px;
margin-top: 6px;
diff --git a/src/utils/siteConfigForm.ts b/src/utils/siteConfigForm.ts
index fe7dbc3..3351483 100644
--- a/src/utils/siteConfigForm.ts
+++ b/src/utils/siteConfigForm.ts
@@ -17,6 +17,8 @@ export interface SiteConfigFieldDef {
multiline?: boolean;
/** url: http(s) URL;host: 域名(不带协议);asset: / 开头路径或 http(s) URL;text: 仅长度 */
kind?: "text" | "url" | "host" | "asset";
+ /** F5(docs/20):asset 类字段的预览/占位框比例(favicon 1:1、二维码 4:3) */
+ aspect?: "1:1" | "4:3";
maxLength?: number;
}
@@ -28,7 +30,7 @@ export const SITE_CONFIG_FIELD_DEFS: SiteConfigFieldDef[] = [
tip: "门户顶栏与页脚品牌位展示;建议先在「附件管理」上传,复制路径填到这里。",
},
{
- key: "favicon", label: "浏览器图标 favicon", group: "display", kind: "asset", maxLength: 500,
+ key: "favicon", label: "浏览器图标 favicon", group: "display", kind: "asset", maxLength: 500, aspect: "1:1",
placeholder: "如 /yschcms/upload/…/favicon.png(留空用主题内置图标)",
tip: "浏览器标签页与收藏夹的小图标,不显示在页面正文中。",
},
@@ -52,12 +54,12 @@ export const SITE_CONFIG_FIELD_DEFS: SiteConfigFieldDef[] = [
tip: "逐行展示在页脚品牌区旁;换行即分行(最多 500 字)。",
},
{
- key: "qr_wechat", label: "微信二维码", group: "footer", kind: "asset", maxLength: 500,
+ key: "qr_wechat", label: "微信二维码", group: "footer", kind: "asset", maxLength: 500, aspect: "4:3",
placeholder: "如 /yschcms/upload/…/qr-wechat.png(留空不展示)",
tip: "页脚二维码,悬停放大预览;建议先在「附件管理」上传。",
},
{
- key: "qr_weibo", label: "微博二维码", group: "footer", kind: "asset", maxLength: 500,
+ key: "qr_weibo", label: "微博二维码", group: "footer", kind: "asset", maxLength: 500, aspect: "4:3",
placeholder: "如 /yschcms/upload/…/qr-weibo.png(留空不展示)",
tip: "显示在页脚右侧二维码区,与微信二维码并排,悬停放大预览。",
},
diff --git a/src/views/cms/components/SiteConfigForm.vue b/src/views/cms/components/SiteConfigForm.vue
index 20d5133..88e9ba9 100644
--- a/src/views/cms/components/SiteConfigForm.vue
+++ b/src/views/cms/components/SiteConfigForm.vue
@@ -11,6 +11,7 @@ import {
validateFieldValue,
} from "@/utils/siteConfigForm";
import ImageAssetField from "@/components/ImageAssetField.vue";
+import FooterPreview from "@/components/FooterPreview.vue";
/**
* 站点配置结构化表单(docs/09 §3.1;docs/16 §3.3 分区作用域;docs/20 F6 扩展参数区退役):
@@ -120,6 +121,7 @@ function defsOf(group: string): SiteConfigFieldDef[] {
v-model="formModel.fields[def.key]"
:site-id="siteId"
:placeholder="def.placeholder"
+ :aspect="def.aspect"
@update:model-value="emitUp"
/>
{{ def.tip }}
+
+