Files
drawlogo/_design-test/spec/build-spec.js
T

850 lines
63 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 甄房业务系统 · UI 规范与标准(定稿 v1.2)
// 品牌资产(LOGO 定稿)+ SAP Fiori Morning Horizon(官方实测色值)+ 组件规范 + ElementUI 落地清单
//
// ⚠ 数据来源纪律:本文件所有 Fiori 色值均取自官方主题包实测,不做记忆引用。
// npm @sap-theming/theming-base-content content/Base/baseLib/sap_horizon/css_variables.css
// npm element-ui@2.15.14 packages/theme-chalk/src/common/var.scss
// CSS 变量、令牌文件、规范表格三处均由下方 T 表统一生成,杜绝三处不一致。
//
// 用法: node _design-test/spec/build-spec.js
const fs = require('fs');
const path = require('path');
const HERE = __dirname;
const LOGO = path.join(HERE, '..', 'logo');
const FZ = JSON.parse(fs.readFileSync(path.join(LOGO, 'final-z.json'), 'utf8'));
const MS = JSON.parse(fs.readFileSync(path.join(LOGO, 'mark-scales.json'), 'utf8'));
const PCT = {};
MS.stages.forEach(s => { PCT[s.pct] = s; });
const MAIN = PCT[80];
const H04 = 'M3 11.99v2.51c0 3.3 0 4.95 1.025 5.975S6.7 21.5 10 21.5h4c3.3 0 4.95 0 5.975-1.025S21 17.8 21 14.5v-2.51c0-1.682 0-2.522-.356-3.25s-1.02-1.244-2.346-2.276l-2-1.555C14.233 3.303 13.2 2.5 12 2.5s-2.233.803-4.298 2.409l-2 1.555C4.375 7.496 3.712 8.012 3.356 8.74S3 10.308 3 11.99';
const FG = 'translate(5.3304 5.3294) scale(2.22222)';
const zScale = s => 'translate(' + MS.center[0] + ' ' + MS.center[1] + ') scale(' + s.toFixed(4) +
') translate(' + (-MS.center[0]) + ' ' + (-MS.center[1]) + ')';
const mark = (pct, ink, size) =>
'<svg viewBox="0 0 64 64" width="' + size + '" height="' + size + '">' +
'<g transform="' + FG + '"><path d="' + H04 + '" fill="none" stroke="' + ink +
'" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></g>' +
'<g transform="' + zScale(PCT[pct].scale) + '"><path d="' + FZ.final + '" fill="' + ink + '"/></g></svg>';
// ═══════════════════════════════════════════════════════════════
// T:全部设计令牌(唯一数据源)
// v = 取值 src = 官方来源变量 / 「派生」
// use = 用途 dev = 与官方的偏离说明(可选)
// c = [底色, 门槛] 对比度检查(可选)
// ═══════════════════════════════════════════════════════════════
const GROUP = [
['品牌层', '甄房自有色彩,用于标识与导航。绝不进入交互色 —— 界面交互一律用主色,标识一律用墨蓝', [
['--zf-brand-ink', '#0C2340', '甄房自有', '品牌主色 · LOGO / 侧栏底 / 登录页底', ['#FFFFFF', 4.5]],
['--zf-brand-ink-deep', '#071527', '甄房自有', '最深底 · 应用图标、启动图', null],
['--zf-brand-gold', '#C6A15B', '甄房自有', '品牌点缀 · 选中指示条、关键数字,占比 ≤1%', null],
['--zf-brand-gold-soft', '#E3CB99', '甄房自有', '金色浅阶 · 深底上的次级强调', null]
]],
['交互层', '主色取官方强调按钮底色;悬停加深一档;<b>按下态官方是「白底蓝字蓝框」</b>,不是更深的蓝', [
['--zf-primary', '#0070F2', 'sapButton_Emphasized_Background', '主按钮底 · 关键图标', ['#FFFFFF', 4.5]],
['--zf-primary-hover', '#0064D9', 'sapButton_Emphasized_Hover_Background', '主按钮悬停底', ['#FFFFFF', 4.5]],
['--zf-primary-active-bg', '#FFFFFF', 'sapButton_Emphasized_Active_Background', '主按钮按下底(官方为白)', null],
['--zf-primary-active-ink', '#0064D9', 'sapButton_Emphasized_Active_TextColor', '主按钮按下时的字与框色', ['#FFFFFF', 4.5]],
['--zf-link', '#0064D9', 'sapLinkColor', '链接色(官方链接比主色深一档,两者不同色)', ['#FFFFFF', 4.5]]
]],
['表面层', '列表/表格悬停是<b>中性灰</b>,选中才是浅蓝 —— 与「悬停也用蓝」的常见做法不同', [
['--zf-bg-base', '#FFFFFF', 'sapList_Background', '卡片、表格、内容容器底', null],
['--zf-bg-sub', '#F5F6F7', 'sapBackgroundColor', '页面底 · 表头 · 斑马纹(sapList_AlternatingBackground 同值)', null],
['--zf-bg-hover', '#EAECEE', 'sapList_Hover_Background', '行/项悬停底(中性灰)', null],
['--zf-bg-active', '#DEE2E5', 'sapList_Active_Background', '行/项按下底', null],
['--zf-bg-selected', '#EBF8FF', 'sapList_SelectionBackgroundColor', '行/项选中底(极浅蓝)', null]
]],
['边框', '分两类:<b>功能性描边</b>(输入框,深色保证可辨识)与<b>分隔线</b>(表格卡片,浅色)', [
['--zf-border', '#E5E5E5', 'sapList_BorderColor', '分隔线 · 表格线 · 卡片边框', null],
['--zf-border-field', '#556B81', 'sapField_BorderColor', '输入框/下拉/日期等控件描边(功能性,需 ≥3:1)', ['#FFFFFF', 3]],
['--zf-border-strong', '#758CA4', 'sapContent_ForegroundBorderColor', '强分隔 · 未选中控件 · 分组边界', ['#FFFFFF', 3]]
]],
['文字', '白底上能过 AA 4.5:1 的灰最深只到 #5F7590,所以二级与三级灰必然接近 —— 拉开层级要靠字号字重,不是继续变浅', [
['--zf-text-1', '#131E29', 'sapTextColor', '一级文字 · 标题、正文、数字(同时也是 sapField_TextColor)', ['#FFFFFF', 4.5]],
['--zf-text-2', '#556B82', 'sapContent_LabelColor', '二级文字 · 表单标签、表头、说明', ['#FFFFFF', 4.5]],
['--zf-text-3', '#5F7590', '派生', '三级文字 · 单位、时间戳、辅助信息', ['#FFFFFF', 4.5],
'官方 Neutral #788FA6 白底仅 3.34:1,不能用于正文级小字;此值按 4.5:1 上限反推'],
['--zf-text-placeholder', '#5F7590', '派生(官方为 #556B82)', '占位符 · 禁用态文字', ['#FFFFFF', 4.5],
'官方占位符与二级文字同色,会让人误以为已填内容;这里刻意浅一档以示区别'],
['--zf-text-inverse', '#FFFFFF', 'sapContent_ContrastTextColor', '深底上的文字', ['#0C2340', 4.5]],
['--zf-text-inverse-2', '#BBC1CA', '派生(rgba(255,255,255,.72) 在 #0C2340 上的合成值)',
'深底上的二级文字 · 侧栏未选中项', ['#0C2340', 4.5]]
]],
['语义色', '一律「描边 + 浅底」,不用实心高饱和色块。<b>只有成功/错误能直接当正文色</b>,警告与中性只能作图形', [
['--zf-success', '#256F3A', 'sapPositiveColor', '成功 · 有效 · 已完成(正文可用)', ['#FFFFFF', 4.5]],
['--zf-error', '#AA0808', 'sapNegativeColor', '错误 · 已下架 · 危险操作(正文可用)', ['#FFFFFF', 4.5]],
['--zf-warning', '#E76500', 'sapCriticalColor', '警告 · 维护中 · 待处理(仅描边/底色,正文需配深色字)', ['#FFFFFF', 3]],
['--zf-info', '#0070F2', 'sapInformativeColor', '信息提示(同主色)', ['#FFFFFF', 4.5]],
['--zf-neutral', '#788FA6', 'sapNeutralColor', '中性 · 仅图形与 ≥18px 大字,禁用于正文', ['#FFFFFF', 3]]
]],
['形状与层级', '圆角全部来自官方 rem 值(根字号 16px);阴影官方未提供 UI 级令牌,为派生值', [
['--zf-radius-s', '4px', 'sapField_BorderCornerRadius(.25rem)', '输入类控件 · 标签 · 复选框', null],
['--zf-radius', '8px', 'sapButton_BorderCornerRadius(.5rem)', '按钮 · 卡片(基准)', null],
['--zf-radius-l', '12px', 'sapElement_BorderCornerRadius(.75rem)', '弹窗 · 抽屉 · 大面积容器', null],
['--zf-radius-tile', '16px', 'sapTile_BorderCornerRadius(1rem)', '大卡片 · 磁贴', null],
['--zf-radius-pill', '999px', '派生', '状态标签 · 头像', null],
['--zf-shadow-1', '0 1px 2px rgba(34,53,72,.08)', '派生', '卡片默认(官方无 UI 阴影令牌)', null],
['--zf-shadow-2', '0 2px 8px rgba(34,53,72,.12)', '派生', '下拉 · 弹窗 · 浮层', null]
]]
];
// 展开成便于生成 CSS 的扁平表
const TOKENS = [];
GROUP.forEach(([g, desc, list]) => list.forEach(([n, v, src, use, c, dev]) =>
TOKENS.push({ g, gdesc: desc, n, v, src, use, c, dev })));
// 字号阶梯(除 caption 外均来自官方 rem 值,根字号 16px)
const TYPE = [
['display', 24, 600, 1.35, 'sapFontHeader3Size(1.5rem)', '页面主标题'],
['title', 20, 600, 1.4, 'sapFontHeader4Size(1.25rem)', '模块标题 · 抽屉标题'],
['title-s', 16, 600, 1.45, 'sapFontHeader5Size / sapFontLargeSize(1rem)', '卡片标题 · 分区小标题'],
['body', 14, 400, 1.6, 'sapFontSize(.875rem)', '正文基准 · 表格内容 · 表单输入值'],
['body-s', 12, 400, 1.6, 'sapFontSmallSize(.75rem)', '次级说明 · 表格辅助行'],
['caption', 11, 400, 1.5, '派生', '标签 · 单位 · 时间戳(不要再小)']
];
const SPACE = [4, 8, 12, 16, 20, 24, 32, 40];
// 无障碍检查清单
const A11Y = [
['一级文字 / 白底', '#131E29', '#FFFFFF', 4.5],
['二级文字 / 白底', '#556B82', '#FFFFFF', 4.5],
['三级文字 / 白底', '#5F7590', '#FFFFFF', 4.5],
['一级文字 / 次级底', '#131E29', '#F5F6F7', 4.5],
['二级文字 / 次级底', '#556B82', '#F5F6F7', 4.5],
['一级文字 / 选中底', '#131E29', '#EBF8FF', 4.5],
['白字 / 交互主色(按钮)', '#FFFFFF', '#0070F2', 4.5],
['白字 / 主色悬停', '#FFFFFF', '#0064D9', 4.5],
['链接色 / 白底', '#0064D9', '#FFFFFF', 4.5],
['成功 / 白底(正文)', '#256F3A', '#FFFFFF', 4.5],
['错误 / 白底(正文)', '#AA0808', '#FFFFFF', 4.5],
['警告 / 白底(仅图形)', '#E76500', '#FFFFFF', 3],
['中性 / 白底(仅图形)', '#788FA6', '#FFFFFF', 3],
['控件描边 / 白底', '#556B81', '#FFFFFF', 3],
['白字 / 品牌墨蓝(侧栏)', '#FFFFFF', '#0C2340', 4.5],
['侧栏二级字 / 品牌墨蓝', '#BBC1CA', '#0C2340', 4.5]
];
// ═══════════════════════════════════════════════════════════════
// ElementUI 2.15.14 落地清单
// m = 必改(不改主题就是半成品) a = 自动派生(无需覆盖) d = 刻意偏离官方
// ═══════════════════════════════════════════════════════════════
const ELMAP = [
['$--color-primary', '--zf-primary', '#0070F2', 'm', '主色,ElementUI 全组件取此值'],
['$--color-success', '--zf-success', '#256F3A', 'm', ''],
['$--color-warning', '--zf-warning', '#E76500', 'm', '同时须改白字,见下方风险项'],
['$--color-danger', '--zf-error', '#AA0808', 'm', '官方 Negative 色'],
['$--color-info', '--zf-text-3', '#5F7590', 'd', '官方 Informative 与主色同值,若照搬会让「信息按钮」与「主按钮」无法区分,故退为中性灰'],
['$--color-text-primary', '--zf-text-1', '#131E29', 'm', ''],
['$--color-text-regular', '--zf-text-1', '#131E29', 'd', '官方正文只有一档 sapTextColor;ElementUI 分 primary/regular 两级,这里刻意拉平'],
['$--color-text-secondary', '--zf-text-3', '#5F7590', 'm', '同时决定表头文字色'],
['$--color-text-placeholder', '--zf-text-placeholder', '#5F7590', 'd', '官方与二级文字同色,会与已填内容混淆'],
['$--border-color-base', '--zf-border-field', '#556B81', 'm', '功能性描边。官方输入框描边本就深,这是 Horizon 的高辨识度来源'],
['$--border-color-light', '--zf-border', '#E5E5E5', 'm', ''],
['$--border-color-lighter', '--zf-border', '#E5E5E5', 'm', '同时决定表格线'],
['$--border-color-extra-light', '--zf-bg-active', '#DEE2E5', 'm', ''],
['$--border-color-hover', '--zf-border-strong', '#758CA4', 'm', ''],
['$--background-color-base', '--zf-bg-sub', '#F5F6F7', 'm', '同时是表格悬停的默认值,必须连带覆盖下一行'],
['$--border-radius-base', '--zf-radius', '8px', 'm', '官方按钮圆角 .5rem'],
['$--border-radius-small', '--zf-radius-s', '4px', 'm', '官方输入类圆角 .25rem'],
['$--button-border-radius', '--zf-radius', '8px', 'm', '自动继承 base,写出只为明确'],
['$--font-size-base', '--zf-fs-body', '14px', 'm', '官方 sapFontSize = .875rem,与 ElementUI 默认一致,<b>无需改动</b>'],
['$--font-size-small', '--zf-fs-body-s', '12px', 'd', '官方小号 = .75rem。由 ElementUI 默认的 13px 降到 12px,会影响部分组件,需目测回归'],
['$--font-size-extra-small', '--zf-fs-body-s', '12px', 'm', ''],
['$--font-size-medium', '--zf-fs-title-s', '16px', 'm', ''],
['$--font-size-large', '--zf-fs-title', '20px', 'd', '官方 Header4 = 1.25rem。由 ElementUI 默认 18px 提到 20px,会改变标题观感'],
['$--font-size-extra-large', '--zf-fs-display', '24px', 'm', '官方 Header3 = 1.5rem'],
['$--table-border-color', '--zf-border', '#E5E5E5', 'm', ''],
['$--table-font-color', '--zf-text-1', '#131E29', 'm', '默认继承 text-regular,已同上'],
['$--table-header-font-color', '--zf-text-3', '#5F7590', 'm', '默认继承 text-secondary,已同上'],
['$--table-header-background-color', '--zf-bg-sub', '#F5F6F7', 'm', '<b>必改</b>:默认是纯白,不改则表头与内容无法区分'],
['$--table-row-hover-background-color', '--zf-bg-hover', '#EAECEE', 'm', '<b>必改</b>:默认继承 background-color-base,不改则悬停变灰底而非中性灰阶'],
['$--table-current-row-background-color', '--zf-bg-selected', '#EBF8FF', 'm', '默认由 color-primary-light-9 派生,显式写死更可控'],
['$--box-shadow-base', '--zf-shadow-2', '0 2px 8px rgba(34,53,72,.12)', 'm', ''],
['$--menu-background-color', '--zf-brand-ink', '#0C2340', 'm', '深色侧栏底色'],
['$--color-primary-light-1 … -9', '—', '自动派生', 'a', '由 <code>mix($--color-white, $--color-primary, N*10%)</code> 基于覆盖后的主色计算,<b>顺序正确即无需覆盖</b>:必须在 @import theme-chalk 之前定义 $--color-primary'],
['$--color-success-light / -lighter 等 8 个', '—', '自动派生', 'a', '同上,按语义色 mix 得到'],
['$--table-border', '—', '自动派生', 'a', '= 1px solid $--table-border-color'],
['$--button-small-border-radius / -mini-border-radius', '—', '自动派生', 'a', '= $--border-radius-base - 1 = 7px,官方无对应值,如需一致请显式设为 6px']
];
// 落地风险项(必须人工处理,光改变量解决不了)
const RISKS = [
['警告按钮白字不达标', '#E76500 底 + 白字实测只有 3.36:1,连正文 4.5:1 都不到(官方 Fiori 也没有橙底白字的实心按钮)。两条出路:① 橙色只用于描边/文字按钮;② 保留实心橙底但把 <code>$--button-warning-font-color</code> 改成深色 <code>#131E29</code>(对 #E76500 为 4.6:1)'],
['按钮悬停方向与官方相反', 'ElementUI 的主按钮悬停 = <code>mix(白, 主色, 20%)</code> 即<b>变浅</b>,按下 = <code>mix(黑, 主色, 10%)</code> 变深;而 Fiori 是悬停 <b>#0064D9 变深</b>、按下变白底蓝框。二者不可兼得:<br>· 若照 ElementUI 走,只需改色,但规范里「悬停 #0064D9」不成立,按钮观感偏「淡」;<br>· 若照 Fiori 走,必须给 <code>.el-button--primary</code> 写自定义 hover/active 样式(变量层面做不到)。<br><b>建议按 Fiori 走并写自定义样式</b>,否则主按钮的悬停反馈会与全站其余悬停(深一档)不一致'],
['深色侧栏变量不够用', 'ElementUI 只暴露 4 个菜单变量(<code>$--menu-background-color</code>、<code>$--menu-item-font-color</code>、<code>$--menu-item-hover-fill</code>、<code>$--menu-item-font-size</code>)。改成墨蓝底后至少还要把 <code>$--menu-item-font-color</code> 设为白色(否则默认 #131E29 在墨蓝上几乎看不见),且子菜单底色、选中态仍无变量可用,必须补自定义 CSS'],
['表格只有一种边框色', 'ElementUI 全表共用 <code>$--table-border-color</code>,做不出「外框深、内线浅」的两级分隔。这不是配置问题,要两级就必须写自定义样式']
];
// 项目现有主题文件里已经失效的变量(实测 ElementUI 2.15.14 中不存在)
const DEADVARS = [
['$--color-primary-dark-2', 'Element Plus 才有此变量;ElementUI 的按下态由 <code>mix(黑, 主色, 10%)</code> 算出'],
['$--border-radius-round', '不存在;ElementUI 只有 <code>$--border-radius-circle: 100%</code>'],
['$--table-header-bg', '不存在;正确名为 <code>$--table-header-background-color</code>'],
['$--table-row-hover-bg', '不存在;正确名为 <code>$--table-row-hover-background-color</code>'],
['$--table-current-row-bg', '不存在;正确名为 <code>$--table-current-row-background-color</code>']
];
// 图表分类色板(v1.2 新增)—— ECharts 多序列用,色相拉开 + 明度错位,相邻序差异最大
const CHART = [
['#0057D2', '蓝', 'sapShell_Category_1(主色系,与品牌呼应)'],
['#E76500', '橙', 'sapCriticalColor'],
['#188918', '绿', 'sapShell_Category_8'],
['#DF1278', '洋红', 'sapShell_Category_2'],
['#07838F', '青', 'sapShell_Category_6'],
['#D27700', '琥珀', 'sapAccentColor1'],
['#7858FF', '紫', 'sapShell_Category_12'],
['#D20A0A', '红', 'sapShell_Category_11'],
['#5B738B', '蓝灰', 'sapShell_Category_10(中性序列)'],
['#C6A15B', '品牌金', '甄房品牌色']
];
// 业务语义色(v1.2 定稿)
const BIZ = [
['房源等级 A(优质)', '#C6A15B', '品牌金 —— 金色表达「优质/尊贵」,比红色更贴合甄房调性'],
['房源等级 B(一般)', '#E76500', 'sapCriticalColor'],
['房源等级 C(低质)', '#788FA6', 'sapNeutralColor'],
['客源热度 H(热)', '#AA0808', 'sapNegativeColor —— 温度用红,与等级的质量语义区分开'],
['客源热度 M(温)', '#E76500', 'sapCriticalColor'],
['客源热度 L(冷)', '#788FA6', 'sapNeutralColor'],
['跟进 · 3 天内', '#256F3A', 'sapPositiveColor'],
['跟进 · 7 天内', '#0070F2', '品牌交互蓝'],
['跟进 · 15 天内', '#E76500', 'sapCriticalColor'],
['跟进 · 超 15 天', '#AA0808', 'sapNegativeColor']
];
const esc = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
// ── 由 TOKENS 生成 CSS :root ────────────────────────────────────
const cssRoot = () => GROUP.map(([g, , list], i) =>
' /* ── ' + g + ' ── */\n' + list.map(t => ' ' + t[0] + ': ' + t[1] + ';').join('\n')
).join('\n\n');
const FS = [['caption', 11], ['body-s', 12], ['body', 14], ['title-s', 16], ['title', 20], ['display', 24]];
const cssFontSizes = () => FS.map(([k, v]) => ' --zf-fs-' + k + ': ' + v + 'px;').join('\n');
const cssSpace = () => SPACE.map((v, i) => ' --zf-sp-' + (i + 1) + ': ' + v + 'px;').join(' ');
// ═══════════════════════════════════════════════════════════════
// 页面样式
// ═══════════════════════════════════════════════════════════════
const CSS = `
*{box-sizing:border-box;margin:0;padding:0}
:root{
${cssRoot()}
${cssFontSizes()}
${cssSpace()}
--zf-font:"72","PingFang SC","Microsoft YaHei","Helvetica Neue",Arial,sans-serif;
}
body{background:#0a0d13;color:#e9edf5;font-family:var(--zf-font);padding:40px 24px 90px;line-height:1.6}
.wrap{max-width:1420px;margin:0 auto}
h1{font-size:26px;letter-spacing:.2px}h1 b{color:#E3CB99}
.hdr{border-bottom:1px solid rgba(255,255,255,.12);padding-bottom:18px;margin-bottom:6px}
.meta{display:flex;gap:22px;flex-wrap:wrap;font-size:12px;color:#8b94a7;margin-top:12px}
.meta b{color:#cfd6e4;font-weight:500}
h2{font-size:19px;margin:48px 0 8px;padding-left:11px;border-left:4px solid #C6A15B}
h2 em{font-size:11px;font-weight:400;color:#8b94a7;font-style:normal;margin-left:10px}
h3{font-size:15px;margin:26px 0 10px;color:#E3CB99}
p.note{font-size:12.5px;color:#8b94a7;margin:8px 0 16px;line-height:1.9}
p.note b{color:#cfd6e4}
code{font-family:Menlo,Consolas,monospace;font-size:11.5px}
table.doc{width:100%;border-collapse:collapse;font-size:12.5px;margin-top:12px}
table.doc th,table.doc td{border:1px solid rgba(255,255,255,.09);padding:8px 11px;text-align:left;vertical-align:top}
table.doc th{background:rgba(255,255,255,.045);font-weight:600;color:#cfd6e4;font-size:11.5px;white-space:nowrap}
table.doc td.k{color:#E3CB99;font-family:Menlo,Consolas,monospace;font-size:11.5px}
table.doc td.v{font-family:Menlo,Consolas,monospace;font-size:11.5px;white-space:nowrap}
table.doc td.s{font-family:Menlo,Consolas,monospace;font-size:10.5px;color:#8b94a7}
table.doc td.u{color:#98a1b3}
table.doc tr.hi td{background:rgba(94,224,138,.10)}
table.doc tr.hi td:first-child{box-shadow:inset 3px 0 0 #5ee08a}
.chip{display:inline-block;width:13px;height:13px;border-radius:4px;vertical-align:-2px;margin-right:7px;
border:1px solid rgba(255,255,255,.22)}
.pass,.fail{font-size:10px;padding:1px 7px;border-radius:20px;border:1px solid;white-space:nowrap}
.pass{color:#5ee08a;border-color:rgba(94,224,138,.42)}
.fail{color:#f0a55c;border-color:rgba(240,165,92,.45)}
.tag2{display:inline-block;font-size:10px;padding:1px 7px;border-radius:20px;border:1px solid;white-space:nowrap}
.tag2.m{color:#f0a55c;border-color:rgba(240,165,92,.5)}
.tag2.d{color:#8ab4f8;border-color:rgba(138,180,248,.5)}
.tag2.a{color:#8b94a7;border-color:rgba(139,148,167,.4)}
.stage{background:var(--zf-bg-base);color:var(--zf-text-1);border-radius:var(--zf-radius-l);
border:1px solid rgba(255,255,255,.13);padding:22px;margin-top:14px;font-family:var(--zf-font)}
.stage.dark{background:var(--zf-brand-ink);color:#fff}
.stage.plain{background:var(--zf-bg-sub)}
.row{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
.btn{font-family:inherit;font-size:var(--zf-fs-body);border-radius:var(--zf-radius);padding:7px 16px;
border:1px solid var(--zf-border);background:var(--zf-bg-base);color:var(--zf-text-1);cursor:default;white-space:nowrap}
.btn.pri{background:var(--zf-primary);border-color:var(--zf-primary);color:#fff}
.btn.pri.h{background:var(--zf-primary-hover);border-color:var(--zf-primary-hover)}
.btn.pri.a{background:var(--zf-primary-active-bg);border-color:var(--zf-primary-active-ink);color:var(--zf-primary-active-ink)}
.btn.gho{border-color:var(--zf-primary);color:var(--zf-primary);background:transparent}
.btn.tx{border-color:transparent;background:transparent;color:var(--zf-link);padding:7px 6px}
.btn.dan{background:var(--zf-error);border-color:var(--zf-error);color:#fff}
.btn.dis{background:var(--zf-bg-sub);border-color:var(--zf-border);color:var(--zf-text-placeholder)}
.btn.sm{font-size:var(--zf-fs-body-s);padding:4px 11px}
.fld{display:flex;flex-direction:column;gap:5px}
.fld label{font-size:var(--zf-fs-body-s);color:var(--zf-text-2)}
.fld label .req{color:var(--zf-error);margin-left:3px}
.fld input,.fld select{font-family:inherit;font-size:var(--zf-fs-body);color:var(--zf-text-1);
background:var(--zf-bg-base);border:1px solid var(--zf-border-field);border-radius:var(--zf-radius-s);
padding:8px 11px;min-width:190px}
.fld input::placeholder{color:var(--zf-text-placeholder)}
.fld input:focus,.fld select:focus{outline:none;border-color:var(--zf-primary);box-shadow:0 0 0 3px var(--zf-bg-selected)}
.fld .hint{font-size:var(--zf-fs-caption);color:var(--zf-text-3)}
.fld.err input{border-color:var(--zf-error)}
.fld.err .hint{color:var(--zf-error)}
.fld.dis input{background:var(--zf-bg-sub);color:var(--zf-text-placeholder);border-color:var(--zf-border)}
.tbl{width:100%;border-collapse:collapse;font-size:var(--zf-fs-body);font-variant-numeric:tabular-nums;color:var(--zf-text-1)}
.tbl th{text-align:left;font-weight:600;font-size:var(--zf-fs-body-s);color:var(--zf-text-3);
background:var(--zf-bg-sub);padding:9px 12px;border-bottom:1px solid var(--zf-border);white-space:nowrap}
.tbl td{padding:9px 12px;border-bottom:1px solid var(--zf-border)}
.tbl tbody tr:nth-child(even){background:var(--zf-bg-sub)}
.tbl tbody tr.hov{background:var(--zf-bg-hover)}
.tbl tbody tr.sel{background:var(--zf-bg-selected)}
.tbl tbody tr.sel td:first-child{box-shadow:inset 3px 0 0 var(--zf-primary)}
.tbl .lk{color:var(--zf-link)}
.tbl .sub{display:block;font-size:var(--zf-fs-caption);color:var(--zf-text-3);margin-top:2px}
.tbl .num{text-align:right}
.tbl .act a{color:var(--zf-link);text-decoration:none;margin-right:10px;font-size:var(--zf-fs-body-s)}
.tbl .act a.dan{color:var(--zf-error)}
.tg{display:inline-block;font-size:var(--zf-fs-caption);padding:2px 9px;border-radius:var(--zf-radius-pill);
border:1px solid;white-space:nowrap;background:var(--zf-bg-base)}
.tg.ok{color:var(--zf-success);border-color:var(--zf-success);background:#F1F7F3}
.tg.wn{color:#B44F00;border-color:var(--zf-warning);background:#FDF1E8}
.tg.er{color:var(--zf-error);border-color:var(--zf-error);background:#FBF0F0}
.tg.pr{color:var(--zf-primary);border-color:var(--zf-primary);background:var(--zf-bg-selected)}
.tg.nu{color:var(--zf-text-2);border-color:var(--zf-border-strong);background:var(--zf-bg-base)}
.tg.gd{color:#8a6a2f;border-color:var(--zf-brand-gold);background:#FAF6EE}
.tabs{display:flex;gap:2px;border-bottom:1px solid var(--zf-border)}
.tabs a{font-size:var(--zf-fs-body);padding:9px 14px;color:var(--zf-text-2);text-decoration:none;border-bottom:2px solid transparent}
.tabs a.on{color:var(--zf-primary);border-bottom-color:var(--zf-primary);font-weight:600}
.pg{min-width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;
border:1px solid var(--zf-border);border-radius:var(--zf-radius-s);background:var(--zf-bg-base);
color:var(--zf-text-1);font-size:var(--zf-fs-body-s);margin-right:6px}
.pg.on{background:var(--zf-primary);border-color:var(--zf-primary);color:#fff;font-weight:600}
.al{display:flex;gap:9px;align-items:flex-start;font-size:var(--zf-fs-body);padding:11px 14px;
border-radius:var(--zf-radius);border:1px solid;border-left-width:3px;color:var(--zf-text-1);margin-bottom:9px}
.al b{font-weight:600}
.al.i{border-color:var(--zf-primary);background:var(--zf-bg-selected)}
.al.s{border-color:var(--zf-success);background:#F1F7F3}
.al.w{border-color:var(--zf-warning);background:#FDF1E8}
.al.e{border-color:var(--zf-error);background:#FBF0F0}
.card{background:var(--zf-bg-base);border:1px solid var(--zf-border);border-radius:var(--zf-radius);
box-shadow:var(--zf-shadow-1);padding:16px}
.kpi{background:var(--zf-bg-base);border:1px solid var(--zf-border);border-radius:var(--zf-radius);
padding:13px 15px;box-shadow:var(--zf-shadow-1)}
.kpi .t{font-size:var(--zf-fs-body-s);color:var(--zf-text-3)}
.kpi .v{font-size:var(--zf-fs-display);font-weight:600;margin-top:4px;font-variant-numeric:tabular-nums}
.kpi .v em{font-size:var(--zf-fs-body-s);font-style:normal;font-weight:400;color:var(--zf-text-3);margin-left:4px}
.kpi .d{font-size:var(--zf-fs-caption);margin-top:3px;color:var(--zf-success)}
.kpi .d.dn{color:var(--zf-error)}
.kpi.gold .v{color:#8a6a2f}
.app{display:grid;grid-template-columns:200px 1fr;border:1px solid var(--zf-border);
border-radius:var(--zf-radius-l);overflow:hidden;background:var(--zf-bg-base)}
.app .side{background:var(--zf-brand-ink);padding:14px 0}
.app .side .brand{display:flex;align-items:center;gap:10px;padding:2px 15px 16px;color:#fff}
.app .side .brand b{font-size:var(--zf-fs-title-s);letter-spacing:1px}
.app .side .brand em{display:block;font-size:8px;letter-spacing:1.4px;opacity:.6;font-style:normal}
.app .side a{display:flex;align-items:center;gap:9px;color:var(--zf-text-inverse-2);text-decoration:none;
font-size:var(--zf-fs-body);padding:9px 15px;border-left:3px solid transparent}
.app .side a.on{background:rgba(255,255,255,.10);color:#fff;border-left-color:var(--zf-brand-gold);font-weight:600}
.app .side a .dot{width:6px;height:6px;border-radius:2px;background:currentColor;opacity:.55}
.app .main{min-width:0}
.app .head{display:flex;align-items:center;gap:14px;padding:11px 18px;border-bottom:1px solid var(--zf-border)}
.app .head .cb{font-size:var(--zf-fs-body);color:var(--zf-text-2)}
.app .head .cb b{color:var(--zf-text-1)}
.app .head .usr{margin-left:auto;display:flex;align-items:center;gap:9px;font-size:var(--zf-fs-body-s);color:var(--zf-text-2)}
.app .head .av{width:26px;height:26px;border-radius:50%;background:var(--zf-bg-selected);
color:var(--zf-primary);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600}
.app .bd{padding:16px 18px;background:var(--zf-bg-sub)}
.st{border:1px dashed var(--zf-border-strong);border-radius:var(--zf-radius);padding:26px;
text-align:center;color:var(--zf-text-3);font-size:var(--zf-fs-body)}
.st .ic{font-size:26px;display:block;margin-bottom:9px;opacity:.55}
.st b{display:block;color:var(--zf-text-2);font-size:var(--zf-fs-body);margin-bottom:5px;font-weight:600}
.sk{background:var(--zf-bg-sub);border-radius:var(--zf-radius-s);height:13px;margin:7px 0}
.dont{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:12px;margin-top:12px}
.dont div{background:rgba(240,92,92,.06);border:1px solid rgba(240,92,92,.3);border-radius:10px;
padding:12px;text-align:center;font-size:11px;color:#f0a5a5}
.dont div .pv{height:78px;display:flex;align-items:center;justify-content:center;background:#fff;
border-radius:7px;margin-bottom:9px;overflow:hidden}
.risk{background:rgba(240,165,92,.08);border:1px solid rgba(240,165,92,.32);border-radius:12px;
padding:14px 16px;font-size:12.5px;line-height:1.95;color:#f2c48a;margin-top:10px}
.risk b{color:#ffd9a8}
.risk code{background:rgba(255,255,255,.07);padding:1px 5px;border-radius:4px}
pre{background:#0d1220;border:1px solid rgba(255,255,255,.09);border-radius:11px;padding:14px 16px;
font-size:11.5px;line-height:1.85;color:#cfd6e4;overflow-x:auto;margin-top:12px;font-family:Menlo,Consolas,monospace}
a.dl{color:#8ab4f8}
`;
// ── 令牌表 ──
const tokenTables = GROUP.map(([g, desc, list]) => `
<h3>${g}</h3>
<p class="note" style="margin:0 0 4px">${desc}</p>
<table class="doc"><thead><tr><th>令牌</th><th>取值</th><th>官方来源</th><th>用途</th><th>对比度</th></tr></thead><tbody>
${list.map(([n, v, src, use, c, dev]) => `<tr>
<td class="k">${n}</td>
<td class="v"><span class="chip" style="background:${v}"></span>${v}</td>
<td class="s">${src}</td>
<td class="u">${use}${dev ? '<br><span style="color:#f0a55c">⚠ 偏离官方:' + dev + '</span>' : ''}</td>
<td>${c ? '<span class="cc" data-fg="' + v + '" data-bg="' + c[0] + '" data-req="' + c[1] + '"></span>' : '—'}</td>
</tr>`).join('')}
</tbody></table>`).join('');
// ═══════════════════════════════════════════════════════════════
const html = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8">
<title>甄房业务系统 · UI 规范与标准(定稿 v1.2)</title>
<style>${CSS}</style></head><body><div class="wrap">
<div class="hdr">
<h1>甄房业务系统 · <b>UI 规范与标准</b></h1>
<div class="meta">
<span>版本 <b>v1.2(定稿)</b></span>
<span>色彩体系 <b>SAP Fiori Morning Horizon</b></span>
<span>LOGO <b>Forte Z + 甄房墨蓝 · Z 占比 80%</b></span>
<span>UI 库 <b>element-ui 2.15.14(Vue 2.6)</b></span>
<span>日期 <b>2026-09-19</b></span>
</div>
</div>
<p class="note" style="margin-top:18px">
<b>本页是该项目的唯一权威 UI 标准。</b>任何界面实现以本页的令牌名与取值为准;不要直接写十六进制色值,不要引入第二套色彩。<br>
<b>色值来源纪律</b>:全部 Fiori 色值取自官方主题包
<code>@sap-theming/theming-base-content → sap_horizon/css_variables.css</code> 实测,ElementUI 变量名与默认值取自
<code>element-ui@2.15.14 → theme-chalk/src/common/var.scss</code> 实测,逐条标注官方来源变量名,<b>无记忆引用</b>。
对比度由页面运行时真实计算(WCAG 2.1)。
</p>
<h2>一、品牌资产 · LOGO<em>已定稿</em></h2>
<p class="note">主 logo 采用 <b>Z 在房框内 80% 档</b>;房框与描边粗细恒定,四档只缩放框内 Z。全部资产为矢量转曲路径。</p>
<div class="stage">
<div class="row" style="gap:26px">
<div style="text-align:center">${mark(80, '#0C2340', 88)}<div style="font-size:11px;color:#8b94a7;margin-top:8px">主 logo · 浅底</div></div>
<div style="text-align:center;background:#0C2340;padding:14px;border-radius:10px">${mark(80, '#FFFFFF', 88)}<div style="font-size:11px;color:rgba(255,255,255,.6);margin-top:8px">主 logo · 深底反白</div></div>
<div style="text-align:center">${mark(80, '#C6A15B', 88)}<div style="font-size:11px;color:#8b94a7;margin-top:8px">金色点缀版(限定场景)</div></div>
</div>
</div>
<h3>四档占比</h3>
<table class="doc"><thead><tr><th>档位</th><th>Z 外接框(64 网格)</th><th>相对主 logo</th><th>用途</th></tr></thead><tbody>
${MS.stages.map(s => {
const use = { 90: '需要更强视觉重量时:小图标位、深色底上的白标(不要用于 32px 以下)',
80: '<b>主 logo,一律默认用这一档</b>', 70: '大幅面(≥1200px)与印刷物料',
60: '名片、信封、大留白版式' }[s.pct];
return `<tr${s.pct === 80 ? ' class="hi"' : ''}>
<td class="k">${s.pct}%${s.pct === 80 ? ' ★' : ''}</td>
<td class="v">${s.size[0].toFixed(1)} × ${s.size[1].toFixed(1)}</td>
<td class="v">×${s.vsMain.toFixed(2)}</td><td class="u">${use}</td></tr>`;
}).join('')}
</tbody></table>
<h3>规则</h3>
<table class="doc"><thead><tr><th>项</th><th>标准</th></tr></thead><tbody>
<tr><td class="k">配色</td><td class="u">标识只用 <b>#0C2340</b>(浅底)或 <b>#FFFFFF</b>(深底)。<b style="color:#f0a55c">严禁使用交互蓝 #0070F2 作为标识色</b> —— 否则用户无法区分「品牌标记」与「可点元素」。<br>注意:v1.1 起链接色已改为 #0064D9,标识色也<b>不要</b>用它</td></tr>
<tr><td class="k">最小尺寸</td><td class="u">图标主形 ≥ 24px;横版锁定标 ≥ 120px 宽。16px 场景用 favicon 专用版,不要硬缩主形</td></tr>
<tr><td class="k">安全留白</td><td class="u">四周留白 ≥ 图标高度的 25%(64 网格里留 16)</td></tr>
<tr><td class="k">深色场景</td><td class="u">用反白版;不要在深底上用墨蓝正片</td></tr>
<tr><td class="k">禁止</td><td class="u">拉伸变形 · 改比例 · 加描边/阴影/发光 · 换非品牌色 · 改动框与 Z 的相对大小 · 在花哨照片上直接压标</td></tr>
</tbody></table>
<div class="dont">
<div><div class="pv"><div style="transform:scaleX(1.5)">${mark(80, '#0C2340', 62)}</div></div>拉伸变形</div>
<div><div class="pv"><div style="transform:rotate(-12deg)">${mark(80, '#0C2340', 62)}</div></div>旋转</div>
<div><div class="pv">${mark(80, '#0070F2', 62)}</div>用交互蓝作标识色</div>
<div><div class="pv"><div style="filter:drop-shadow(2px 2px 3px rgba(0,0,0,.55))">${mark(80, '#0C2340', 62)}</div></div>加阴影</div>
<div><div class="pv">${mark(90, '#0C2340', 62)}</div>在 24px 以下用 90% 档(易糊线)</div>
<div><div class="pv" style="background:#9aa4b8">${mark(80, '#0C2340', 62)}</div>在中间调灰底上用墨蓝</div>
</div>
<h2>二、色彩系统<em>SAP Fiori Morning Horizon · 官方实测值</em></h2>
<p class="note">
核心策略:<b>品牌色与交互色分离</b>。甄房墨蓝 <b>#0C2340</b> 承担品牌与导航,Fiori 的 <b>#0070F2</b> 承担交互反馈。
两者的明度差足够大 —— 墨蓝在界面里读作「近黑的中性色」,不会与亮蓝争夺注意力,也不会被误读为可点击元素。
</p>
${tokenTables}
<h3>用色配比</h3>
<table class="doc"><thead><tr><th>层</th><th>占比</th><th>说明</th></tr></thead><tbody>
<tr><td class="k">中性(白 / 灰)</td><td class="v">≈ 75%</td><td class="u">页面基底与分隔靠灰阶承担,这是数据密集型界面不累的前提</td></tr>
<tr><td class="k">品牌墨蓝</td><td class="v">≈ 15%</td><td class="u">侧栏与页头;形成稳定视觉锚点,让导航边界一眼可辨</td></tr>
<tr><td class="k">交互蓝</td><td class="v">≈ 7%</td><td class="u">主操作、链接、选中态。一屏内主操作建议 ≤ 1 个</td></tr>
<tr><td class="k">语义色</td><td class="v">≈ 2%</td><td class="u">状态标签、提示条。一律描边+浅底,不用实心块</td></tr>
<tr><td class="k">品牌金</td><td class="v">≤ 1%</td><td class="u">只放在「点」上:选中指示条、关键数字。铺面即俗</td></tr>
</tbody></table>
<h2>三、字体与文字层级<em>官方 rem 值,根字号 16px</em></h2>
<p class="note">
字族:<code>--zf-font</code> = "72", "PingFang SC", "Microsoft YaHei", …("72" 为 Fiori 官方字族,未安装时回落系统中文黑体)。
数字一律加 <code>font-variant-numeric: tabular-nums</code>。
</p>
<div class="stage">
${TYPE.map(([n, sz, w, lh, src, u]) => `
<div style="display:flex;align-items:baseline;gap:14px;padding:9px 0;border-bottom:1px solid #E5E5E5">
<code style="width:104px;color:#5F7590;font-size:11px;flex:none">--zf-fs-${n}</code>
<span style="font-size:${sz}px;font-weight:${w};line-height:${lh};flex:1;color:#131E29">甄房 · 二手房房源 1,284 套</span>
<span style="font-size:11px;color:#5F7590;width:250px;text-align:right;flex:none">${sz}px / ${w} / 行高 ${lh} · ${u}</span>
</div>
<div style="font-size:10.5px;color:#8b94a7;padding:0 0 7px 104px;font-family:Menlo,Consolas,monospace">${src}</div>`).join('')}
</div>
<p class="note">
<b>只有 6 档,不要再加。</b>中文最小 11px(caption)—— 再小在 Windows 上会糊。
</p>
<h2>四、间距 · 圆角 · 阴影</h2>
<h3>间距(4pt 基数)</h3>
<div class="stage plain">
<div class="row" style="align-items:flex-end;gap:14px">
${SPACE.map(v => `<div style="text-align:center">
<div style="width:${v}px;height:${v}px;background:#0070F2;border-radius:2px;margin:0 auto 6px"></div>
<code style="font-size:10.5px;color:#5F7590">${v}</code></div>`).join('')}
</div>
<p class="note" style="margin:14px 0 0">
常用:控件内边距 <b>8 / 11 / 16</b>;表单行间距 <b>12</b>;卡片内边距 <b>16</b>;区块间距 <b>20 / 24</b>;页面左右边距 <b>18 / 24</b>。
不要出现 5、7、9、13 这类非 4 倍数。
</p>
</div>
<h3>圆角与阴影</h3>
<div class="stage">
<div class="row" style="align-items:stretch">
<div class="card" style="flex:1;border-radius:4px"><code style="font-size:11px;color:#5F7590">4px · 输入类</code><div style="font-size:12px;color:#556B82;margin-top:6px">输入框、下拉、标签、复选框</div></div>
<div class="card" style="flex:1"><code style="font-size:11px;color:#5F7590">8px · 基准</code><div style="font-size:12px;color:#556B82;margin-top:6px">按钮、卡片</div></div>
<div class="card" style="flex:1;border-radius:12px"><code style="font-size:11px;color:#5F7590">12px</code><div style="font-size:12px;color:#556B82;margin-top:6px">弹窗、抽屉、大容器</div></div>
<div class="card" style="flex:1;border-radius:16px"><code style="font-size:11px;color:#5F7590">16px · 磁贴</code><div style="font-size:12px;color:#556B82;margin-top:6px">大卡片、看板磁贴</div></div>
<div class="card" style="flex:1;border-radius:999px"><code style="font-size:11px;color:#5F7590">pill</code><div style="font-size:12px;color:#556B82;margin-top:6px">状态标签、头像</div></div>
</div>
<p class="note" style="margin:14px 0 0">
阴影只用两级:<code>--zf-shadow-1</code> 卡片默认、<code>--zf-shadow-2</code> 浮层。
<b>分隔优先用边框而不是阴影</b> —— 一屏几十张卡片都带阴影会整体发糊。
</p>
</div>
<h2>五、组件规范</h2>
<h3>按钮</h3>
<div class="stage">
<div class="row">
<button class="btn pri">主操作</button>
<button class="btn pri h">悬停</button>
<button class="btn pri a">按下</button>
<button class="btn gho">次要操作</button>
<button class="btn">普通按钮</button>
<button class="btn tx">文字按钮</button>
<button class="btn dan">危险操作</button>
<button class="btn dis">禁用</button>
<button class="btn pri sm">小号主操作</button>
</div>
<p class="note" style="margin:16px 0 0">
<b>「按下」是白底蓝字蓝框</b> —— 这是 Fiori 官方定义(<code>sapButton_Emphasized_Active_Background: #fff</code> + 蓝字蓝框),不是做错了。
其余状态:悬停 <code>#0064D9</code> 加深一档、禁用灰底。
</p>
<table class="doc" style="margin-top:14px"><thead><tr><th>类型</th><th>用途</th><th>一屏数量</th></tr></thead><tbody>
<tr><td class="k">主操作 pri</td><td class="u">推动流程前进的唯一动作:查询、提交、保存、确认</td><td class="v">≤ 1</td></tr>
<tr><td class="k">次要 gho</td><td class="u">辅助动作:高级筛选、批量导入</td><td class="v">≤ 2</td></tr>
<tr><td class="k">普通 / 文字</td><td class="u">重置、取消、查看详情</td><td class="v">不限</td></tr>
<tr><td class="k">危险 dan</td><td class="u">删除、作废、强制回收;必须二次确认</td><td class="v">≤ 1</td></tr>
</tbody></table>
</div>
<h3>表单</h3>
<div class="stage">
<div class="row" style="align-items:flex-start;gap:18px">
<div class="fld" style="width:200px"><label>房源编码</label><input placeholder="SH-ES-202609-…"><span class="hint">支持模糊匹配</span></div>
<div class="fld" style="width:200px"><label>所属楼盘<span class="req">*</span></label><select><option>滨江壹号</option></select><span class="hint">必填</span></div>
<div class="fld err" style="width:200px"><label>维护人<span class="req">*</span></label><input value="张"><span class="hint">请输入完整姓名(至少 2 个字)</span></div>
<div class="fld dis" style="width:200px"><label>房源编号(系统生成)</label><input value="SH-ES-202609-00037" disabled><span class="hint">不可编辑</span></div>
</div>
<p class="note" style="margin:16px 0 0">
标签在上、控件在下的纵向排布。标签 12px 不加重。错误态只改边框与提示文字色,<b>不要给整个字段加红底</b>。<br>
控件描边用 <code>--zf-border-field #556B81</code>(深色)—— 这是 Horizon 的特色,输入框「有框」才看得清;分隔线才用浅色 <code>#E5E5E5</code>。两者不可混用。
</p>
</div>
<h3>数据表格</h3>
<div class="stage" style="padding:0;overflow:hidden">
<table class="tbl">
<thead><tr><th>房源编码</th><th>楼盘 / 房号</th><th class="num">面积</th><th class="num">售价</th><th>状态</th><th>维护人</th><th>操作</th></tr></thead>
<tbody>
<tr><td class="lk">SH-ES-202609-00037</td><td>滨江壹号 3幢 1802<span class="sub">滨江区 · 钱塘江畔</span></td><td class="num">128.5 ㎡</td><td class="num">1,285 万</td><td><span class="tg ok">有效</span></td><td>张伟</td><td class="act"><a>查看</a><a>编辑</a></td></tr>
<tr class="hov"><td class="lk">SH-ES-202609-00041</td><td>钱江新城 7幢 2201<span class="sub">上城区 · 万象城</span></td><td class="num">156.0 ㎡</td><td class="num">1,860 万</td><td><span class="tg pr">待检核</span></td><td>李敏</td><td class="act"><a>查看</a><a>编辑</a></td></tr>
<tr class="sel"><td class="lk">SH-ES-202609-00052</td><td>滨江壹号 5幢 901<span class="sub">滨江区 · 钱塘江畔</span></td><td class="num">98.2 ㎡</td><td class="num">920 万</td><td><span class="tg gd">独家</span></td><td>王强</td><td class="act"><a>查看</a><a>编辑</a></td></tr>
<tr><td class="lk">SH-ES-202609-00058</td><td>绿城桂语 2幢 1503<span class="sub">西湖区 · 文三路</span></td><td class="num">142.6 ㎡</td><td class="num">1,420 万</td><td><span class="tg wn">维护中</span></td><td>张伟</td><td class="act"><a>查看</a><a>编辑</a></td></tr>
<tr><td class="lk">SH-ES-202609-00063</td><td>万科未来 1幢 608<span class="sub">余杭区 · 未来科技城</span></td><td class="num">89.0 ㎡</td><td class="num">610 万</td><td><span class="tg er">已下架</span></td><td>赵磊</td><td class="act"><a>查看</a><a class="dan">删除</a></td></tr>
</tbody></table>
<div style="padding:13px 16px;font-size:12px;color:#556B82;display:flex;align-items:center">
共 1,284 条<span class="pg on" style="margin-left:16px">1</span><span class="pg">2</span><span class="pg">3</span><span class="pg">…</span><span class="pg">65</span>
<span style="margin-left:auto;font-size:11.5px">每页 20 条</span></div>
</div>
<table class="doc" style="margin-top:14px"><thead><tr><th>项</th><th>标准</th></tr></thead><tbody>
<tr><td class="k">斑马纹</td><td class="u">偶数行 <code>--zf-bg-sub #F5F6F7</code>,与白底差值仅 2–3%。<b>这是数据表最高频的疲劳源</b>,不要加大对比</td></tr>
<tr><td class="k">悬停 / 选中</td><td class="u">悬停是<b>中性灰</b> <code>#EAECEE</code>,选中才是<b>浅蓝</b> <code>#EBF8FF</code> —— 官方如此,两者不要都做成蓝。选中行左侧加 3px 主色竖条,长表格滚动后仍可辨认</td></tr>
<tr><td class="k">数字列</td><td class="u">右对齐 + tabular-nums;金额带千分位,单位写表头(「售价(万)」)不逐行重复</td></tr>
<tr><td class="k">操作列</td><td class="u">固定最后一列,文字链 ≤ 3 个;破坏性动作放最后并标红</td></tr>
<tr><td class="k">边框</td><td class="u">全表只有一种线色 <code>#E5E5E5</code>(ElementUI 也只暴露一个变量)。想要「外框深、内线浅」必须写自定义样式</td></tr>
</tbody></table>
<h3>状态标签</h3>
<div class="stage">
<div class="row">
<span class="tg ok">有效</span><span class="tg wn">维护中</span><span class="tg er">已下架</span>
<span class="tg pr">待检核</span><span class="tg nu">草稿</span><span class="tg gd">独家</span>
</div>
<p class="note" style="margin:14px 0 0">
一律 <b>描边 + 浅底</b>,圆角 pill,字号 11。<br>
<b>注意警告标签的文字色是 #B44F00 而不是 #E76500</b> —— 官方 Critical 色在白底只有 3.36:1,直接当文字色不达标;
底色仍用 <code>#E76500</code> 描边,文字压深一档。<b>不要</b>用白字压在 #E76500 上(3.36:1,同样不达标)。
</p>
</div>
<h3>页签 · 分页</h3>
<div class="stage">
<div class="tabs"><a class="on">全部 1284</a><a>有效 892</a><a>维护中 236</a><a>已成交 121</a><a>暂缓 35</a></div>
<div style="margin-top:14px">
<span class="pg on">1</span><span class="pg">2</span><span class="pg">3</span><span class="pg">…</span><span class="pg">65</span>
</div>
</div>
<h3>提示条</h3>
<div class="stage">
<div class="al i"><span>ℹ</span><span><b>提示:</b>该房源委托期将于 7 天后到期,请及时续签。</span></div>
<div class="al s"><span>✓</span><span><b>成功:</b>房源 SH-ES-202609-00037 已提交检核。</span></div>
<div class="al w"><span>⚠</span><span><b>警告:</b>钥匙编号与系统记录不一致,请核对后重试。</span></div>
<div class="al e"><span>✕</span><span><b>错误:</b>售价不能低于政府指导价,请修改后重试。</span></div>
</div>
<h3>卡片 · KPI · 页头导航</h3>
<div class="stage plain">
<div class="row" style="align-items:stretch;margin-bottom:16px">
<div class="kpi" style="flex:1"><div class="t">在售房源</div><div class="v">1,284<em>套</em></div><div class="d">↑ 3.2% 较上周</div></div>
<div class="kpi" style="flex:1"><div class="t">本周新增客源</div><div class="v">96<em>位</em></div><div class="d">↑ 12 位</div></div>
<div class="kpi" style="flex:1"><div class="t">待跟进</div><div class="v">23<em>条</em></div><div class="d dn">↓ 5 条</div></div>
<div class="kpi gold" style="flex:1"><div class="t">本月预估佣金</div><div class="v">86.4<em>万</em></div><div class="d">↑ 8.6%</div></div>
</div>
<div class="app">
<aside class="side">
<div class="brand">${mark(80, '#FFFFFF', 30)}<span><b>甄房</b><em>ZHENFANG</em></span></div>
<a class="on"><span class="dot"></span>工作台</a>
<a><span class="dot"></span>房源管理</a>
<a><span class="dot"></span>客源管理</a>
<a><span class="dot"></span>交易管理</a>
<a><span class="dot"></span>钥匙管理</a>
<a><span class="dot"></span>我的佣金</a>
</aside>
<div class="main">
<div class="head">
<div class="cb">房源管理 / <b>二手房房源</b></div>
<div class="usr"><span class="av">张</span>张伟 · 滨江门店</div>
</div>
<div class="bd" style="font-size:12px;color:#5F7590">内容区底色 <code>--zf-bg-sub</code>,卡片用白底与 1px 边框区分层级</div>
</div>
</div>
<p class="note" style="margin:14px 0 0">
侧栏固定用 <b>墨蓝 #0C2340</b>。这是本项目<b>刻意偏离 Fiori 官方 Shell</b> 的地方(官方 <code>sapShell_Background</code> 是浅色 <code>#EFF1F2</code>)——
为了减少整屏亮度、形成稳定视觉锚点。浅灰侧栏在长时间使用中会与内容区「糊在一起」。
选中项用金色 3px 左指示条 + 10% 白底。<b>代价是 ElementUI 的菜单变量不够用,需补自定义样式(见第九章)。</b>
</p>
</div>
<h2>六、页面状态</h2>
<div class="stage plain">
<div class="row" style="align-items:stretch;gap:14px">
<div class="st" style="flex:1;background:#fff"><span class="ic">⏳</span><b>加载中</b>用骨架屏(灰条),不要整页转圈</div>
<div class="st" style="flex:1;background:#fff"><span class="ic">📭</span><b>没有数据</b>「还没有房源,点右上角新增」+ 主操作按钮</div>
<div class="st" style="flex:1;background:#fff"><span class="ic">🔍</span><b>筛选无结果</b>「没有符合条件的房源,试试放宽条件」+ 重置按钮</div>
<div class="st" style="flex:1;background:#fff"><span class="ic">⚠</span><b>加载失败</b>「数据加载失败」+ 重试按钮 + 错误码</div>
<div class="st" style="flex:1;background:#fff"><span class="ic">🔒</span><b>无权限</b>「你没有查看该门店房源的权限」+ 申请入口</div>
</div>
<div class="card" style="margin-top:14px">
<div style="font-size:12px;color:#5F7590;margin-bottom:9px">骨架屏示例</div>
<div class="sk" style="width:38%"></div>
<div class="sk" style="width:72%"></div>
<div class="sk" style="width:56%"></div>
</div>
<p class="note" style="margin:14px 0 0">
五种状态都必须单独设计。「没有数据」与「筛选无结果」文案必须不同 —— 否则用户会以为自己的数据丢了。
</p>
</div>
<h2>七、长时使用约束<em>针对经纪人每天连续使用 6–8 小时</em></h2>
<table class="doc"><thead><tr><th>约束</th><th>做法</th><th>解决什么</th></tr></thead><tbody>
<tr><td class="k">深色品牌侧栏</td><td class="u">侧栏统一 <code>#0C2340</code>,不用浅灰(偏离官方 Shell 浅色)</td><td class="u">形成稳定视觉锚点;浅灰侧栏会与内容区糊在一起</td></tr>
<tr><td class="k">斑马纹极低对比</td><td class="u">条纹 <code>#F5F6F7</code>,与白底差值 2–3%</td><td class="u">帮助横向追行不串行,又不产生条纹眩光</td></tr>
<tr><td class="k">语义色降饱和</td><td class="u">描边+浅底,不用实心块;警告文字用深一档的 #B44F00</td><td class="u">一屏几十个状态标签时的最大刺眼源</td></tr>
<tr><td class="k">悬停与选中分工</td><td class="u">悬停中性灰 <code>#EAECEE</code>、选中浅蓝 <code>#EBF8FF</code></td><td class="u">长时间划动列表时,中性灰比蓝底更不累眼;蓝色只留给「已选定」这一个强语义</td></tr>
<tr><td class="k">金色只做点</td><td class="u">占比 ≤1%,只用于指示条与关键数字</td><td class="u">保住品牌质感,避免大面积金色显廉价</td></tr>
<tr><td class="k">正文不用纯黑</td><td class="u">正文 <code>#131E29</code> 而非 <code>#000</code></td><td class="u">降低硬反差带来的持续眼疲劳</td></tr>
</tbody></table>
<h2>八、无障碍标准</h2>
<p class="note">门槛:正文文字 ≥ 4.5:1,大字与图形/控件边界 ≥ 3:1。下表全部由运行时真实计算。</p>
<table class="doc"><thead><tr><th>检查项</th><th>前景 / 背景</th><th>实测</th><th>门槛</th><th>结论</th></tr></thead><tbody>
${A11Y.map(([n, f, b, r]) => `<tr><td class="u">${n}</td><td class="v"><span class="chip" style="background:${f}"></span>${f} / <span class="chip" style="background:${b}"></span>${b}</td>
<td class="cc" data-fg="${f}" data-bg="${b}" data-req="${r}"></td><td class="v">≥${r}:1</td>
<td><span class="vc" data-fg="${f}" data-bg="${b}" data-req="${r}"></span></td></tr>`).join('')}
</tbody></table>
<h3>其他无障碍要求</h3>
<table class="doc"><thead><tr><th>项</th><th>标准</th></tr></thead><tbody>
<tr><td class="k">焦点态</td><td class="u">所有可交互元素必须有可见焦点环:<code>border-color: --zf-primary</code> + <code>box-shadow: 0 0 0 3px --zf-bg-selected</code>。禁止 <code>outline:none</code> 而不给替代</td></tr>
<tr><td class="k">触达尺寸</td><td class="u">可点区域 ≥ 32×32px(H5 ≥ 44×44px)</td></tr>
<tr><td class="k">不靠颜色单独表意</td><td class="u">状态标签必须带文字,不能只有色点;图表必须带数据标签或图例</td></tr>
<tr><td class="k">键盘可达</td><td class="u">Tab 顺序符合视觉顺序;弹窗打开后焦点移入并锁定,Esc 关闭后归还触发元素</td></tr>
<tr><td class="k">动效</td><td class="u">过渡 ≤ 200ms;尊重 <code>prefers-reduced-motion</code></td></tr>
</tbody></table>
<h2>九、落地清单<em>element-ui 2.15.14 变量映射 · 已逐条核对官方源码</em></h2>
<p class="note">
项目前端为 <b>RuoYi-Vue + element-ui 2.15.14 + Vue 2.6 + sass</b>,变量覆盖写在
<code>ruoyi-ui/src/assets/styles/element-variables.scss</code>(须在 <code>@import "~element-ui/packages/theme-chalk/src/index"</code> <b>之前</b>定义)。<br>
下表 <span class="tag2 m">必改</span> = 不改主题就是半成品 ·
<span class="tag2 d">刻意偏离</span> = 与官方取值不同,已在行内写明原因 ·
<span class="tag2 a">自动派生</span> = ElementUI 会基于主色自动算出,只要定义顺序正确就无需覆盖。
</p>
<table class="doc"><thead><tr><th>ElementUI 变量</th><th>本项目令牌</th><th>取值</th><th>类别</th><th>说明</th></tr></thead><tbody>
${ELMAP.map(([e, t, v, k, note]) => `<tr>
<td class="k">${e}</td><td class="k">${t}</td>
<td class="v"><span class="chip" style="background:${v.indexOf('#') === 0 ? v : 'transparent'}"></span>${v}</td>
<td><span class="tag2 ${k}">${k === 'm' ? '必改' : k === 'd' ? '刻意偏离' : '自动派生'}</span></td>
<td class="u">${note || '—'}</td></tr>`).join('')}
</tbody></table>
<h3>光改变量解决不了的风险项</h3>
${RISKS.map(([t, d]) => `<div class="risk"><b>${t}</b><br>${d}</div>`).join('')}
<h3>项目现有主题文件中的失效变量</h3>
<p class="note">
现有 <a class="dl" href="../../ruoyi-ui/src/assets/styles/element-variables.scss">element-variables.scss</a> 里下面这些变量在 element-ui 2.15.14 中<b>并不存在</b>,
Sass 不会报错、但也不会生效 —— 属于「以为改了其实没改」。同样问题也存在于另一个主题体系(橙色那套)里。
</p>
<table class="doc"><thead><tr><th>写法</th><th>实际情况</th></tr></thead><tbody>
${DEADVARS.map(([v, d]) => `<tr><td class="k" style="color:#f0a55c">${v}</td><td class="u">${d}</td></tr>`).join('')}
</tbody></table>
<h3>图表分类色板(ECharts)<span class="pill">v1.2 新增 · 最多 10 序列</span></h3>
<p class="note">
多序列图表<b>禁止把单色映射逐色套用</b> —— 旧色板里 <code>#ff7a1a</code> 映射成主色蓝后会与原有的 <code>#3b82f6</code> 撞色,
<code>#ffb31a</code>/<code>#d9481c</code> 也会与 <code>#ef4444</code> 挤在橙红区,10 条序列将失去区分度。<br>
下表是重配后的分类色板:色相均匀拉开、相邻序差异最大,全部取自 Fiori 官方 Category / Accent 色(最后一色为品牌金)。
</p>
<div class="stage" style="padding:16px 18px">
<div style="display:flex;gap:6px">
${CHART.map(([c, n, s], i) => `<div style="flex:1;text-align:center" title="${s}">
<div style="height:46px;border-radius:6px;background:${c}"></div>
<div style="font-size:10.5px;color:var(--zf-text-3);margin-top:6px">${i + 1} · ${n}</div>
<div style="font-size:9.5px;color:var(--zf-text-3);opacity:.75">${c}</div>
</div>`).join('')}
</div>
</div>
<pre>const COLORS = ['#0057D2', '#E76500', '#188918', '#DF1278', '#07838F', '#D27700', '#7858FF', '#D20A0A', '#5B738B', '#C6A15B']</pre>
<p class="note">序列少于 10 条时<b>按顺序取前 N 个</b>,不要跳着取 —— 相邻色的差异是按顺序优化过的。分类色只用于「无固有顺序」的维度;有顺序的量(如时间)用单色深浅渐变。</p>
<h3>业务语义色<span class="pill">v1.2 定稿</span></h3>
<p class="note">「等级」是质量语义、「热度」是温度语义,两者刻意分到不同色相族,避免同屏混淆。A 级用品牌金而非红色 —— 金色表达「优质/尊贵」,与甄房墨蓝+金的调性一致。</p>
<table class="doc"><thead><tr><th>用途</th><th>色值</th><th>来源与理由</th></tr></thead><tbody>
${BIZ.map(([u, v, s]) => `<tr><td class="u">${u}</td><td class="v"><span class="chip" style="background:${v}"></span>${v}</td><td class="u">${s}</td></tr>`).join('')}
</tbody></table>
<h3>令牌文件</h3>
<p class="note">可直接下载 <a class="dl" href="zhenfang-tokens.css" download>zhenfang-tokens.css</a>(下列全部令牌的 CSS 变量定义),引入到项目根样式之前。</p>
<pre>${esc(':root {\n' + cssRoot() + '\n\n' + cssFontSizes() + '\n' + cssSpace() + '\n --zf-font: "72", "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;\n}')}</pre>
<h2>十、变更记录</h2>
<table class="doc"><thead><tr><th>版本</th><th>日期</th><th>变更</th></tr></thead><tbody>
<tr class="hi"><td class="k">v1.2</td><td class="v">2026-09-19</td><td class="u">
<b>新增图表分类色板与业务语义色,并在 ZhenOS-ZF 落地。</b><br>
· 图表:旧 ECharts 色板逐色映射会与 <code>#3b82f6</code>/<code>#ef4444</code> 撞色,改为重配的 10 色分类色板(全部 Fiori 官方 Category/Accent 色 + 品牌金);<br>
· 业务色:<b>房源等级 A 由红色改为品牌金</b>(金色表达优质,贴合甄房调性);热度 H 保留红色,与等级的质量语义区分;<br>
· 落地范围:ZhenOS-ZF 的 WEB(ruoyi-ui)+ H5(zhenfang-uni)双端全量换肤,含主题层 9 个 SCSS、业务页面 100+ 文件、H5 tabbar 图标重着色、logo/应用图标资源替换。
</td></tr>
<tr><td class="k">v1.1</td><td class="v">2026-09-19</td><td class="u">
<b>修正 v1.0 的色值错误。</b>v1.0 的 Fiori 色值来自二手记忆,逐条核对官方主题包后发现 16 个色值里有 10 个不准(例如把
<code>#0064D9</code> 写成 <code>#0057D2</code>、把插图/分类色 <code>#223548</code> / <code>#d5dadd</code> / <code>#a9b4be</code> 当成 UI 文字与边框令牌、链接色漏了官方单独定义的 <code>#0064D9</code>)。<br>
本次全部改为官方实测值并逐条标注来源变量名;文字只用两级(官方 Neutral 白底 3.34:1 不能用于正文,三级灰改为按 4.5:1 上限反推的 <code>#5F7590</code>);<br>
字号阶梯改为官方 rem 值(正文 14px 而非 13px、标题 20/24px);<br>
ElementUI 映射从 22 条补到 37 条,区分「必改 / 刻意偏离 / 自动派生」,并新增 4 条光改变量解决不了的风险项(警告按钮白字不达标、按钮悬停方向与官方相反、深色侧栏变量不够用、表格只有一种边框色);<br>
新增「项目现有主题文件中的失效变量」一节,列出 5 个在当前 ElementUI 版本中不存在的变量写法;<br>
CSS 变量、令牌文件、规范表格改由同一份数据生成,杜绝三处不一致。
</td></tr>
<tr><td class="k">v1.0</td><td class="v">2026-09-19</td><td class="u">
色彩体系定稿为 SAP Fiori Morning Horizon;LOGO 定稿 Z 在房框内 80% 档为主 logo,派生 90/70/60 三档;
确立「品牌色与交互色分离」;首次发布组件规范、页面状态规范、长时使用约束与 ElementUI 变量映射。
</td></tr>
</tbody></table>
<p class="note" style="margin-top:34px;color:#6d768a">
本页由 <code>_design-test/spec/build-spec.js</code> 生成。LOGO 数据源自 <code>mark-scales.json</code> 与 <code>final-z.json</code>;
Fiori 色值源自 <code>@sap-theming/theming-base-content</code> 的 <code>sap_horizon/css_variables.css</code>;
ElementUI 变量名与默认值源自 <code>element-ui@2.15.14</code> 的 <code>theme-chalk/src/common/var.scss</code>。
</p>
</div>
<script>
function toRgb(c){
if (c.indexOf('rgba') === 0) return c.slice(5, c.length - 1).split(',').slice(0, 3).map(Number);
var h = c.replace('#', '');
if (h.length === 3) h = h.split('').map(function(x){ return x + x; }).join('');
return [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16)];
}
function lum(c){
var v = toRgb(c).map(function(x){ x /= 255; return x <= .03928 ? x/12.92 : Math.pow((x+.055)/1.055, 2.4); });
return .2126*v[0] + .7152*v[1] + .0722*v[2];
}
function ratio(a, b){
var l1 = lum(a), l2 = lum(b);
return (Math.max(l1,l2) + .05) / (Math.min(l1,l2) + .05);
}
var ok = 0, bad = 0;
Array.prototype.forEach.call(document.querySelectorAll('.cc'), function(el){
var r = ratio(el.dataset.fg, el.dataset.bg), req = parseFloat(el.dataset.req);
if (r >= req) { ok++; } else { bad++; }
el.innerHTML = '<span class="' + (r >= req ? 'pass' : 'fail') + '">' + r.toFixed(2) + ':1</span>';
});
Array.prototype.forEach.call(document.querySelectorAll('.vc'), function(el){
var r = ratio(el.dataset.fg, el.dataset.bg), req = parseFloat(el.dataset.req);
el.innerHTML = '<span class="' + (r >= req ? 'pass' : 'fail') + '">' + (r >= req ? '通过' : '未达标') + '</span>';
});
document.title = document.title + '(对比度 ' + ok + '/' + (ok + bad) + ' 通过)';
</script>
</body></html>`;
fs.writeFileSync(path.join(HERE, 'index.html'), html);
// ── 令牌文件(与页面同源生成)────────────────────────────────────
const tokensCss = `/* 甄房业务系统 · 设计令牌 v1.1(定稿 2026-09-19)
* 色彩体系:SAP Fiori Morning Horizon(官方实测值)
* 引入顺序:本文件应在项目根样式之前引入
* 规则:界面实现一律使用 var(--zf-*) ,禁止直接写十六进制色值
* 来源:@sap-theming/theming-base-content → sap_horizon/css_variables.css
*/
:root {
${cssRoot()}
/* ── 字号(官方 rem 值,根字号 16px)── */
${cssFontSizes()}
/* ── 间距(4pt 基数)── */
${cssSpace()}
/* ── 字族 ── */
--zf-font: "72", "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
}
`;
fs.writeFileSync(path.join(HERE, 'zhenfang-tokens.css'), tokensCss);
console.log('已生成 spec/index.html 与 spec/zhenfang-tokens.css');
console.log(' 令牌组', GROUP.length, '| 令牌总数', TOKENS.length,
'| 字号档', TYPE.length, '| ElementUI 映射', ELMAP.length,
'| 风险项', RISKS.length, '| 失效变量', DEADVARS.length);