--zf-bg-sub,卡片用白底与 1px 边框区分层级// 甄房业务系统 · 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) =>
'';
// ═══════════════════════════════════════════════════════════════
// 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]
]],
['交互层', '主色取官方强调按钮底色;悬停加深一档;按下态官方是「白底蓝字蓝框」,不是更深的蓝', [
['--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]]
]],
['表面层', '列表/表格悬停是中性灰,选中才是浅蓝 —— 与「悬停也用蓝」的常见做法不同', [
['--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]
]],
['边框', '分两类:功能性描边(输入框,深色保证可辨识)与分隔线(表格卡片,浅色)', [
['--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]]
]],
['语义色', '一律「描边 + 浅底」,不用实心高饱和色块。只有成功/错误能直接当正文色,警告与中性只能作图形', [
['--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 默认一致,无需改动'],
['$--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', '必改:默认是纯白,不改则表头与内容无法区分'],
['$--table-row-hover-background-color', '--zf-bg-hover', '#EAECEE', 'm', '必改:默认继承 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', '由 mix($--color-white, $--color-primary, N*10%) 基于覆盖后的主色计算,顺序正确即无需覆盖:必须在 @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 也没有橙底白字的实心按钮)。两条出路:① 橙色只用于描边/文字按钮;② 保留实心橙底但把 $--button-warning-font-color 改成深色 #131E29(对 #E76500 为 4.6:1)'],
['按钮悬停方向与官方相反', 'ElementUI 的主按钮悬停 = mix(白, 主色, 20%) 即变浅,按下 = mix(黑, 主色, 10%) 变深;而 Fiori 是悬停 #0064D9 变深、按下变白底蓝框。二者不可兼得:
· 若照 ElementUI 走,只需改色,但规范里「悬停 #0064D9」不成立,按钮观感偏「淡」;
· 若照 Fiori 走,必须给 .el-button--primary 写自定义 hover/active 样式(变量层面做不到)。
建议按 Fiori 走并写自定义样式,否则主按钮的悬停反馈会与全站其余悬停(深一档)不一致'],
['深色侧栏变量不够用', 'ElementUI 只暴露 4 个菜单变量($--menu-background-color、$--menu-item-font-color、$--menu-item-hover-fill、$--menu-item-font-size)。改成墨蓝底后至少还要把 $--menu-item-font-color 设为白色(否则默认 #131E29 在墨蓝上几乎看不见),且子菜单底色、选中态仍无变量可用,必须补自定义 CSS'],
['表格只有一种边框色', 'ElementUI 全表共用 $--table-border-color,做不出「外框深、内线浅」的两级分隔。这不是配置问题,要两级就必须写自定义样式']
];
// 项目现有主题文件里已经失效的变量(实测 ElementUI 2.15.14 中不存在)
const DEADVARS = [
['$--color-primary-dark-2', 'Element Plus 才有此变量;ElementUI 的按下态由 mix(黑, 主色, 10%) 算出'],
['$--border-radius-round', '不存在;ElementUI 只有 $--border-radius-circle: 100%'],
['$--table-header-bg', '不存在;正确名为 $--table-header-background-color'],
['$--table-row-hover-bg', '不存在;正确名为 $--table-row-hover-background-color'],
['$--table-current-row-bg', '不存在;正确名为 $--table-current-row-background-color']
];
// 图表分类色板(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, '&').replace(//g, '>');
// ── 由 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]) => `
${desc}
| 令牌 | 取值 | 官方来源 | 用途 | 对比度 |
|---|---|---|---|---|
| ${n} | ${v} | ${src} | ${use}${dev ? ' ⚠ 偏离官方:' + dev + '' : ''} |
${c ? '' : '—'} |
本页是该项目的唯一权威 UI 标准。任何界面实现以本页的令牌名与取值为准;不要直接写十六进制色值,不要引入第二套色彩。
色值来源纪律:全部 Fiori 色值取自官方主题包
@sap-theming/theming-base-content → sap_horizon/css_variables.css 实测,ElementUI 变量名与默认值取自
element-ui@2.15.14 → theme-chalk/src/common/var.scss 实测,逐条标注官方来源变量名,无记忆引用。
对比度由页面运行时真实计算(WCAG 2.1)。
主 logo 采用 Z 在房框内 80% 档;房框与描边粗细恒定,四档只缩放框内 Z。全部资产为矢量转曲路径。
| 档位 | Z 外接框(64 网格) | 相对主 logo | 用途 |
|---|---|---|---|
| ${s.pct}%${s.pct === 80 ? ' ★' : ''} | ${s.size[0].toFixed(1)} × ${s.size[1].toFixed(1)} | ×${s.vsMain.toFixed(2)} | ${use} |
| 项 | 标准 |
|---|---|
| 配色 | 标识只用 #0C2340(浅底)或 #FFFFFF(深底)。严禁使用交互蓝 #0070F2 作为标识色 —— 否则用户无法区分「品牌标记」与「可点元素」。 注意:v1.1 起链接色已改为 #0064D9,标识色也不要用它 |
| 最小尺寸 | 图标主形 ≥ 24px;横版锁定标 ≥ 120px 宽。16px 场景用 favicon 专用版,不要硬缩主形 |
| 安全留白 | 四周留白 ≥ 图标高度的 25%(64 网格里留 16) |
| 深色场景 | 用反白版;不要在深底上用墨蓝正片 |
| 禁止 | 拉伸变形 · 改比例 · 加描边/阴影/发光 · 换非品牌色 · 改动框与 Z 的相对大小 · 在花哨照片上直接压标 |
核心策略:品牌色与交互色分离。甄房墨蓝 #0C2340 承担品牌与导航,Fiori 的 #0070F2 承担交互反馈。 两者的明度差足够大 —— 墨蓝在界面里读作「近黑的中性色」,不会与亮蓝争夺注意力,也不会被误读为可点击元素。
${tokenTables}| 层 | 占比 | 说明 |
|---|---|---|
| 中性(白 / 灰) | ≈ 75% | 页面基底与分隔靠灰阶承担,这是数据密集型界面不累的前提 |
| 品牌墨蓝 | ≈ 15% | 侧栏与页头;形成稳定视觉锚点,让导航边界一眼可辨 |
| 交互蓝 | ≈ 7% | 主操作、链接、选中态。一屏内主操作建议 ≤ 1 个 |
| 语义色 | ≈ 2% | 状态标签、提示条。一律描边+浅底,不用实心块 |
| 品牌金 | ≤ 1% | 只放在「点」上:选中指示条、关键数字。铺面即俗 |
字族:--zf-font = "72", "PingFang SC", "Microsoft YaHei", …("72" 为 Fiori 官方字族,未安装时回落系统中文黑体)。
数字一律加 font-variant-numeric: tabular-nums。
--zf-fs-${n}
甄房 · 二手房房源 1,284 套
${sz}px / ${w} / 行高 ${lh} · ${u}
只有 6 档,不要再加。中文最小 11px(caption)—— 再小在 Windows 上会糊。
${v}常用:控件内边距 8 / 11 / 16;表单行间距 12;卡片内边距 16;区块间距 20 / 24;页面左右边距 18 / 24。 不要出现 5、7、9、13 这类非 4 倍数。
4px · 输入类8px · 基准12px16px · 磁贴pill
阴影只用两级:--zf-shadow-1 卡片默认、--zf-shadow-2 浮层。
分隔优先用边框而不是阴影 —— 一屏几十张卡片都带阴影会整体发糊。
「按下」是白底蓝字蓝框 —— 这是 Fiori 官方定义(sapButton_Emphasized_Active_Background: #fff + 蓝字蓝框),不是做错了。
其余状态:悬停 #0064D9 加深一档、禁用灰底。
| 类型 | 用途 | 一屏数量 |
|---|---|---|
| 主操作 pri | 推动流程前进的唯一动作:查询、提交、保存、确认 | ≤ 1 |
| 次要 gho | 辅助动作:高级筛选、批量导入 | ≤ 2 |
| 普通 / 文字 | 重置、取消、查看详情 | 不限 |
| 危险 dan | 删除、作废、强制回收;必须二次确认 | ≤ 1 |
标签在上、控件在下的纵向排布。标签 12px 不加重。错误态只改边框与提示文字色,不要给整个字段加红底。
控件描边用 --zf-border-field #556B81(深色)—— 这是 Horizon 的特色,输入框「有框」才看得清;分隔线才用浅色 #E5E5E5。两者不可混用。
| 项 | 标准 |
|---|---|
| 斑马纹 | 偶数行 --zf-bg-sub #F5F6F7,与白底差值仅 2–3%。这是数据表最高频的疲劳源,不要加大对比 |
| 悬停 / 选中 | 悬停是中性灰 #EAECEE,选中才是浅蓝 #EBF8FF —— 官方如此,两者不要都做成蓝。选中行左侧加 3px 主色竖条,长表格滚动后仍可辨认 |
| 数字列 | 右对齐 + tabular-nums;金额带千分位,单位写表头(「售价(万)」)不逐行重复 |
| 操作列 | 固定最后一列,文字链 ≤ 3 个;破坏性动作放最后并标红 |
| 边框 | 全表只有一种线色 #E5E5E5(ElementUI 也只暴露一个变量)。想要「外框深、内线浅」必须写自定义样式 |
一律 描边 + 浅底,圆角 pill,字号 11。
注意警告标签的文字色是 #B44F00 而不是 #E76500 —— 官方 Critical 色在白底只有 3.36:1,直接当文字色不达标;
底色仍用 #E76500 描边,文字压深一档。不要用白字压在 #E76500 上(3.36:1,同样不达标)。
--zf-bg-sub,卡片用白底与 1px 边框区分层级
侧栏固定用 墨蓝 #0C2340。这是本项目刻意偏离 Fiori 官方 Shell 的地方(官方 sapShell_Background 是浅色 #EFF1F2)——
为了减少整屏亮度、形成稳定视觉锚点。浅灰侧栏在长时间使用中会与内容区「糊在一起」。
选中项用金色 3px 左指示条 + 10% 白底。代价是 ElementUI 的菜单变量不够用,需补自定义样式(见第九章)。
五种状态都必须单独设计。「没有数据」与「筛选无结果」文案必须不同 —— 否则用户会以为自己的数据丢了。
| 约束 | 做法 | 解决什么 |
|---|---|---|
| 深色品牌侧栏 | 侧栏统一 #0C2340,不用浅灰(偏离官方 Shell 浅色) | 形成稳定视觉锚点;浅灰侧栏会与内容区糊在一起 |
| 斑马纹极低对比 | 条纹 #F5F6F7,与白底差值 2–3% | 帮助横向追行不串行,又不产生条纹眩光 |
| 语义色降饱和 | 描边+浅底,不用实心块;警告文字用深一档的 #B44F00 | 一屏几十个状态标签时的最大刺眼源 |
| 悬停与选中分工 | 悬停中性灰 #EAECEE、选中浅蓝 #EBF8FF | 长时间划动列表时,中性灰比蓝底更不累眼;蓝色只留给「已选定」这一个强语义 |
| 金色只做点 | 占比 ≤1%,只用于指示条与关键数字 | 保住品牌质感,避免大面积金色显廉价 |
| 正文不用纯黑 | 正文 #131E29 而非 #000 | 降低硬反差带来的持续眼疲劳 |
门槛:正文文字 ≥ 4.5:1,大字与图形/控件边界 ≥ 3:1。下表全部由运行时真实计算。
| 检查项 | 前景 / 背景 | 实测 | 门槛 | 结论 |
|---|---|---|---|---|
| ${n} | ${f} / ${b} | ≥${r}:1 |
| 项 | 标准 |
|---|---|
| 焦点态 | 所有可交互元素必须有可见焦点环:border-color: --zf-primary + box-shadow: 0 0 0 3px --zf-bg-selected。禁止 outline:none 而不给替代 |
| 触达尺寸 | 可点区域 ≥ 32×32px(H5 ≥ 44×44px) |
| 不靠颜色单独表意 | 状态标签必须带文字,不能只有色点;图表必须带数据标签或图例 |
| 键盘可达 | Tab 顺序符合视觉顺序;弹窗打开后焦点移入并锁定,Esc 关闭后归还触发元素 |
| 动效 | 过渡 ≤ 200ms;尊重 prefers-reduced-motion |
项目前端为 RuoYi-Vue + element-ui 2.15.14 + Vue 2.6 + sass,变量覆盖写在
ruoyi-ui/src/assets/styles/element-variables.scss(须在 @import "~element-ui/packages/theme-chalk/src/index" 之前定义)。
下表 必改 = 不改主题就是半成品 ·
刻意偏离 = 与官方取值不同,已在行内写明原因 ·
自动派生 = ElementUI 会基于主色自动算出,只要定义顺序正确就无需覆盖。
| ElementUI 变量 | 本项目令牌 | 取值 | 类别 | 说明 |
|---|---|---|---|---|
| ${e} | ${t} | ${v} | ${k === 'm' ? '必改' : k === 'd' ? '刻意偏离' : '自动派生'} | ${note || '—'} |
现有 element-variables.scss 里下面这些变量在 element-ui 2.15.14 中并不存在, Sass 不会报错、但也不会生效 —— 属于「以为改了其实没改」。同样问题也存在于另一个主题体系(橙色那套)里。
| 写法 | 实际情况 |
|---|---|
| ${v} | ${d} |
多序列图表禁止把单色映射逐色套用 —— 旧色板里 #ff7a1a 映射成主色蓝后会与原有的 #3b82f6 撞色,
#ffb31a/#d9481c 也会与 #ef4444 挤在橙红区,10 条序列将失去区分度。
下表是重配后的分类色板:色相均匀拉开、相邻序差异最大,全部取自 Fiori 官方 Category / Accent 色(最后一色为品牌金)。
const COLORS = ['#0057D2', '#E76500', '#188918', '#DF1278', '#07838F', '#D27700', '#7858FF', '#D20A0A', '#5B738B', '#C6A15B']
序列少于 10 条时按顺序取前 N 个,不要跳着取 —— 相邻色的差异是按顺序优化过的。分类色只用于「无固有顺序」的维度;有顺序的量(如时间)用单色深浅渐变。
「等级」是质量语义、「热度」是温度语义,两者刻意分到不同色相族,避免同屏混淆。A 级用品牌金而非红色 —— 金色表达「优质/尊贵」,与甄房墨蓝+金的调性一致。
| 用途 | 色值 | 来源与理由 |
|---|---|---|
| ${u} | ${v} | ${s} |
可直接下载 zhenfang-tokens.css(下列全部令牌的 CSS 变量定义),引入到项目根样式之前。
${esc(':root {\n' + cssRoot() + '\n\n' + cssFontSizes() + '\n' + cssSpace() + '\n --zf-font: "72", "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;\n}')}
| 版本 | 日期 | 变更 |
|---|---|---|
| v1.2 | 2026-09-19 |
新增图表分类色板与业务语义色,并在 ZhenOS-ZF 落地。 · 图表:旧 ECharts 色板逐色映射会与 #3b82f6/#ef4444 撞色,改为重配的 10 色分类色板(全部 Fiori 官方 Category/Accent 色 + 品牌金);· 业务色:房源等级 A 由红色改为品牌金(金色表达优质,贴合甄房调性);热度 H 保留红色,与等级的质量语义区分; · 落地范围:ZhenOS-ZF 的 WEB(ruoyi-ui)+ H5(zhenfang-uni)双端全量换肤,含主题层 9 个 SCSS、业务页面 100+ 文件、H5 tabbar 图标重着色、logo/应用图标资源替换。 |
| v1.1 | 2026-09-19 |
修正 v1.0 的色值错误。v1.0 的 Fiori 色值来自二手记忆,逐条核对官方主题包后发现 16 个色值里有 10 个不准(例如把
#0064D9 写成 #0057D2、把插图/分类色 #223548 / #d5dadd / #a9b4be 当成 UI 文字与边框令牌、链接色漏了官方单独定义的 #0064D9)。本次全部改为官方实测值并逐条标注来源变量名;文字只用两级(官方 Neutral 白底 3.34:1 不能用于正文,三级灰改为按 4.5:1 上限反推的 #5F7590);字号阶梯改为官方 rem 值(正文 14px 而非 13px、标题 20/24px); ElementUI 映射从 22 条补到 37 条,区分「必改 / 刻意偏离 / 自动派生」,并新增 4 条光改变量解决不了的风险项(警告按钮白字不达标、按钮悬停方向与官方相反、深色侧栏变量不够用、表格只有一种边框色); 新增「项目现有主题文件中的失效变量」一节,列出 5 个在当前 ElementUI 版本中不存在的变量写法; CSS 变量、令牌文件、规范表格改由同一份数据生成,杜绝三处不一致。 |
| v1.0 | 2026-09-19 | 色彩体系定稿为 SAP Fiori Morning Horizon;LOGO 定稿 Z 在房框内 80% 档为主 logo,派生 90/70/60 三档; 确立「品牌色与交互色分离」;首次发布组件规范、页面状态规范、长时使用约束与 ElementUI 变量映射。 |
本页由 _design-test/spec/build-spec.js 生成。LOGO 数据源自 mark-scales.json 与 final-z.json;
Fiori 色值源自 @sap-theming/theming-base-content 的 sap_horizon/css_variables.css;
ElementUI 变量名与默认值源自 element-ui@2.15.14 的 theme-chalk/src/common/var.scss。