feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
// 甄房换肤 · Electron 桌面外壳 + docs 独立工具页
|
||||
// 注意:这些文件是「深色外壳」上下文,同一个旧色在这里的含义与 WEB 不同 ——
|
||||
// 例如 #FFB31A 在 WEB 是警告色(→ #E76500),在外壳里是「品牌浅色」(→ #4D9BF6),
|
||||
// 所以不能复用 WEB 的映射表,必须单独一套。
|
||||
// 用法: node _design-test/apply-electron-colors.js (预演)
|
||||
// node _design-test/apply-electron-colors.js --write (真写)
|
||||
const fs = require('fs');
|
||||
|
||||
const WRITE = process.argv.includes('--write');
|
||||
const ZF = 'e:/KF_7/ZhenOS-ZF';
|
||||
|
||||
const FILES = [
|
||||
`${ZF}/xiaohuju/resources/splash/splash.html`,
|
||||
`${ZF}/xiaohuju/resources/offline/offline.html`,
|
||||
`${ZF}/xiaohuju/resources/blocked/blocked.html`,
|
||||
`${ZF}/xiaohuju/src/common/constants.js`,
|
||||
`${ZF}/xiaohuju/src/preload/titlebar.js`,
|
||||
`${ZF}/docs/财务策算测算器-v2.html`,
|
||||
`${ZF}/docs/财务策算测算器.html`
|
||||
];
|
||||
|
||||
// 十六进制(大小写不敏感)
|
||||
const HEX = [
|
||||
['#ff822d', '#0070F2'], // blocked.html 里的品牌亮色
|
||||
['#ff7a1a', '#0070F2'], // 品牌亮色 → 交互蓝
|
||||
['#ff9a4d', '#4D9BF6'],
|
||||
['#ff8a3d', '#4D9BF6'],
|
||||
['#ffb31a', '#4D9BF6'], // 外壳里的「品牌浅色」→ 浅蓝(非警告橙)
|
||||
['#ffc48a', '#A6CFFF'],
|
||||
['#1a130d', '#071527'], // 最深底 → 甄房最深墨蓝
|
||||
['#2e241a', '#0C2340'],
|
||||
['#241c14', '#0A1D35'], // 标题栏底
|
||||
['#33281d', '#0C2340'],
|
||||
['#f1e4d2', '#EAF0F6'], // 标题栏符号(暖白 → 冷白)
|
||||
['#f6ead8', '#F0F4F8'],
|
||||
['#2a2118', '#131E29'],
|
||||
['#3a322a', '#556B82'],
|
||||
['#756a5e', '#5F7590'],
|
||||
['#d9481c', '#AA0808'],
|
||||
['#2e9e5b', '#256F3A'],
|
||||
['#ffb31a', '#4D9BF6']
|
||||
];
|
||||
|
||||
// rgb()/rgba() 三元组(保留 rgb/rgba 前缀与后面的 alpha)
|
||||
const RGB = [
|
||||
[/255,\s*122,\s*26/g, '0, 112, 242'],
|
||||
[/255,\s*130,\s*45/g, '0, 112, 242'],
|
||||
[/255,\s*179,\s*26/g, '77, 155, 246'],
|
||||
[/241,\s*228,\s*210/g, '240, 244, 248'], // 暖白带透明
|
||||
[/58,\s*50,\s*42/g, '12, 35, 64'],
|
||||
[/26,\s*19,\s*13/g, '7, 21, 39'],
|
||||
[/42,\s*33,\s*24/g, '19, 30, 41'],
|
||||
[/51,\s*40,\s*29/g, '12, 35, 64']
|
||||
];
|
||||
|
||||
let grand = 0, touched = 0;
|
||||
for (const f of FILES) {
|
||||
if (!fs.existsSync(f)) { console.log(' 跳过(不存在): ' + f.split('/').pop()); continue; }
|
||||
let txt = fs.readFileSync(f, 'utf8');
|
||||
const orig = txt;
|
||||
const hits = [];
|
||||
for (const [from, to] of HEX) {
|
||||
const re = new RegExp(from, 'gi');
|
||||
const n = (txt.match(re) || []).length;
|
||||
if (n) { txt = txt.replace(re, to); hits.push(from + '→' + to + ' ×' + n); grand += n; }
|
||||
}
|
||||
for (const [re, to] of RGB) {
|
||||
const n = (txt.match(re) || []).length;
|
||||
if (n) { txt = txt.replace(re, to); hits.push('rgb元组 ×' + n); grand += n; }
|
||||
}
|
||||
if (txt !== orig) { touched++; console.log(' ' + f.replace(ZF + '/', '') + '\n ' + hits.join(' ')); }
|
||||
if (WRITE && txt !== orig) fs.writeFileSync(f, txt);
|
||||
}
|
||||
console.log('\n涉及 ' + touched + ' 个文件 / 替换 ' + grand + ' 处 ' + (WRITE ? '(已写入)' : '(预演,加 --write 生效)'));
|
||||
@@ -0,0 +1,63 @@
|
||||
// 把样式层剩余的字面量旧色值替换为甄房新色板(精确 1:1 映射,大小写不敏感)
|
||||
// 只作用于指定的两个文件,不碰任何 .vue 业务页面
|
||||
// 用法: node _design-test/apply-scss-colors.js (预演,只报告不写)
|
||||
// node _design-test/apply-scss-colors.js --write (真写)
|
||||
const fs = require('fs');
|
||||
|
||||
const WRITE = process.argv.includes('--write');
|
||||
|
||||
const MAP = [
|
||||
['#ff7a1a', '#0070F2'], // 品牌橙 → 交互蓝
|
||||
['#e86a0c', '#0064D9'],
|
||||
['#d96408', '#0057D2'],
|
||||
['#ff8a3d', '#4D9BF6'],
|
||||
['#ffc48a', '#A6CFFF'],
|
||||
['#ffe8d1', '#EBF8FF'],
|
||||
['#fff6ec', '#F0F6FE'],
|
||||
['#f6ead8', '#EAECEE'],
|
||||
['#faf7f2', '#F5F6F7'], // 暖白 → 冷白
|
||||
['#e9d3b0', '#758CA4'], // 暖边框 → 冷边框
|
||||
['#efe7dc', '#E5E5E5'],
|
||||
['#fbf5ea', '#F5F6F7'],
|
||||
['#2a2118', '#131E29'], // 暖墨 → 冷墨
|
||||
['#3a322a', '#556B82'],
|
||||
['#756a5e', '#5F7590'],
|
||||
['#9a8e80', '#5F7590'],
|
||||
['#b8ac9e', '#A9B4BE'],
|
||||
['#8a7f73', '#758CA4'],
|
||||
['#c9beb0', '#BBC1CA'],
|
||||
['#ffb31a', '#E76500'], // 琥珀 → 警告橙
|
||||
['#fff6e0', '#FDF1E8'],
|
||||
['#d9481c', '#AA0808'], // 红橙 → 官方 Negative
|
||||
['#fdeee8', '#FBF0F0'],
|
||||
['#2e9e5b', '#256F3A'], // 绿 → 官方 Positive
|
||||
['#ecf7f0', '#F1F7F3'],
|
||||
['#1f8a5b', '#256F3A'],
|
||||
['#ea580c', '#B44F00'],
|
||||
['#f08060', '#D98A8A'],
|
||||
['#fbbf24', '#E76500'],
|
||||
['#2f2820', '#0A1D35'],
|
||||
['#4a4036', '#1E2A36'],
|
||||
['#d3dce6', '#223040'],
|
||||
['#99a9bf', '#6B7F94']
|
||||
];
|
||||
|
||||
const FILES = [
|
||||
'e:/KF_7/ZhenOS-ZF/ruoyi-ui/src/assets/styles/ruoyi.scss',
|
||||
'e:/KF_7/ZhenOS-ZF/ruoyi-ui/src/assets/styles/btn.scss'
|
||||
];
|
||||
|
||||
let grand = 0;
|
||||
for (const f of FILES) {
|
||||
let txt = fs.readFileSync(f, 'utf8');
|
||||
const hits = [];
|
||||
for (const [from, to] of MAP) {
|
||||
const re = new RegExp(from.replace('#', '#'), 'gi');
|
||||
const n = (txt.match(re) || []).length;
|
||||
if (n) { hits.push(from + ' -> ' + to + ' x' + n); txt = txt.replace(re, to); grand += n; }
|
||||
}
|
||||
console.log('\n' + f.split('/').pop() + ' 共 ' + hits.length + ' 种色值');
|
||||
hits.forEach(h => console.log(' ' + h));
|
||||
if (WRITE && hits.length) fs.writeFileSync(f, txt);
|
||||
}
|
||||
console.log('\n合计替换 ' + grand + ' 处 ' + (WRITE ? '(已写入)' : '(预演,未写入;加 --write 生效)'));
|
||||
@@ -0,0 +1,135 @@
|
||||
// 甄房换肤 · WEB 业务层替换器
|
||||
// 用法: node _design-test/apply-vue-colors.js (预演,报告每条规则的命中数)
|
||||
// node _design-test/apply-vue-colors.js --write (真写)
|
||||
//
|
||||
// 为什么不能只做单色 1:1 替换:
|
||||
// 1) ECharts 的 10 色分类色板若逐色映射,会与已有的 #3b82f6 / #ef4444 撞色,序列失去区分度
|
||||
// 2) 同一个渐变串 linear-gradient(135deg,#f08060,#d9481c) 同时用于「房源等级 A」和「客源热度 H」,
|
||||
// 但 A 现在要改成品牌金、H 仍是红 —— 必须按选择器区分
|
||||
// 所以规则分两类:先按「选择器/整串」精确处理,再对剩余单色做通用映射。
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const WRITE = process.argv.includes('--write');
|
||||
// 两个前端:WEB(ruoyi-ui)与 H5(zhenfang-uni)
|
||||
const ROOTS = [
|
||||
{ root: 'e:/KF_7/ZhenOS-ZF/ruoyi-ui/src', skip: new Set(['assets/styles']) },
|
||||
{ root: 'e:/KF_7/ZhenOS-ZF/zhenfang-uni/src', skip: new Set() }
|
||||
];
|
||||
const EXT = new Set(['.vue', '.js', '.ts', '.html']);
|
||||
|
||||
// ── 新的 10 色分类色板(Fiori 官方 Category/Accent 色 + 品牌金,色相拉开 + 明度错位)──
|
||||
const PALETTE10 = ['#0057D2', '#E76500', '#188918', '#DF1278', '#07838F', '#D27700', '#7858FF', '#D20A0A', '#5B738B', '#C6A15B'];
|
||||
const OLD10 = ['#ff7a1a', '#2e9e5b', '#ffb31a', '#f08060', '#0F766E', '#756a5e', '#7c3aed', '#14B8A6', '#ef4444', '#3b82f6'];
|
||||
const OLD6 = ['#ff7a1a', '#2e9e5b', '#ffb31a', '#f08060', '#0F766E', '#756a5e'];
|
||||
|
||||
const arr = a => '[' + a.map(c => "'" + c + "'").join(', ') + ']';
|
||||
|
||||
// ── 规则(按顺序执行)──
|
||||
const RULES = [
|
||||
// 1. 图表 10 色分类色板(4 处)
|
||||
['图表色板 · 10 色', arr(OLD10).replace(/'/g, "'"), arr(PALETTE10), 'literal'],
|
||||
// 2. 图表 6 色(1 处)
|
||||
['图表色板 · 6 色', arr(OLD6).replace(/'/g, "'"), arr(PALETTE10.slice(0, 6)), 'literal'],
|
||||
|
||||
// 3. 房源等级 A → 品牌金
|
||||
['等级 A 渐变 → 品牌金', /(&\.level-A\s*\{[^}]*?linear-gradient\()135deg,\s*#f08060,\s*#d9481c/g,
|
||||
'$1135deg, #E3CB99, #C6A15B', 'regex'],
|
||||
// 4. 客源热度 H(热)→ 保留红系
|
||||
['热度 H 渐变 → 红', /(&\.level-H\s*\{[^}]*?linear-gradient\()135deg,\s*#f08060,\s*#d9481c/g,
|
||||
'$1135deg, #E06B6B, #AA0808', 'regex'],
|
||||
// 5. 等级 B → 警告橙
|
||||
['等级 B 渐变 → 橙', /(&\.level-B\s*\{[^}]*?linear-gradient\()135deg,\s*#FBBF24,\s*#ffb31a/g,
|
||||
'$1135deg, #D27700, #E76500', 'regex'],
|
||||
// 6. 等级 C → 中性
|
||||
['等级 C 渐变 → 中性', /(&\.level-C\s*\{[^}]*?linear-gradient\()135deg,\s*#9a8e80,\s*#756a5e/g,
|
||||
'$1135deg, #A9B4BE, #788FA6', 'regex'],
|
||||
|
||||
// 7. 统计卡 warning 渐变
|
||||
['warning 渐变', /linear-gradient\((90|135)deg,\s*#ffb31a,\s*#FBBF24\)/g,
|
||||
'linear-gradient($1deg, #E76500, #D27700)', 'regex'],
|
||||
// 8. 统计卡 danger 渐变
|
||||
['danger 渐变', /linear-gradient\((90|135)deg,\s*#f08060,\s*#ef4444\)/g,
|
||||
'linear-gradient($1deg, #E76500, #D20A0A)', 'regex'],
|
||||
|
||||
// 9. el-rate 星级(单一琥珀色 ×3)
|
||||
['星级 · 单色', "['#ffb31a', '#ffb31a', '#ffb31a']", "['#D27700', '#D27700', '#D27700']", 'literal'],
|
||||
['星级 · 同色系', "['#2e9e5b', '#2e9e5b', '#2e9e5b']", "['#188918', '#188918', '#188918']", 'literal'],
|
||||
// 10. el-rate 五档评价(差→极佳)
|
||||
['评价五档', "['#d9481c', '#ffb31a', '#FBBF24', '#ff7a1a', '#2e9e5b']",
|
||||
"['#D20A0A', '#E76500', '#D27700', '#C6A15B', '#188918']", 'literal'],
|
||||
// 11. ThemePicker 预置色
|
||||
['主题预置色', "['#ff7a1a', '#ff8a3d', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]",
|
||||
"['#0070F2', '#4D9BF6', '#0C2340','#131E29','#188918', '#07838F', '#7858FF', '#D20A0A', ]", 'literal'],
|
||||
|
||||
// 12. rgb()/rgba() 三元组(保留 rgb/rgba 与后面的 alpha)
|
||||
['rgb 三元组 · 橙', /(rgba?)\(\s*255,\s*122,\s*26\b/g, '$1(0, 112, 242', 'regex'],
|
||||
['rgb 三元组 · 浅橙', /(rgba?)\(\s*255,\s*138,\s*61\b/g, '$1(77, 155, 246', 'regex'],
|
||||
['rgb 三元组 · 琥珀', /(rgba?)\(\s*255,\s*179,\s*26\b/g, '$1(231, 101, 0', 'regex'],
|
||||
['rgb 三元组 · 亮琥珀', /(rgba?)\(\s*251,\s*191,\s*36\b/g, '$1(210, 119, 0', 'regex'],
|
||||
['rgb 三元组 · 暖墨', /(rgba?)\(\s*42,\s*33,\s*24\b/g, '$1(19, 30, 41', 'regex']
|
||||
];
|
||||
|
||||
// ── 通用单色映射(处理剩余的所有字面量)──
|
||||
const GENERIC = [
|
||||
['#ff7a1a', '#0070F2'], ['#e86a0c', '#0064D9'], ['#d96408', '#0057D2'],
|
||||
['#ff8a3d', '#4D9BF6'], ['#ffc48a', '#A6CFFF'], ['#ffe8d1', '#EBF8FF'], ['#fff6ec', '#F0F6FE'],
|
||||
['#f6ead8', '#EAECEE'], ['#faf7f2', '#F5F6F7'], ['#fbf5ea', '#F5F6F7'],
|
||||
['#e9d3b0', '#758CA4'], ['#efe7dc', '#E5E5E5'],
|
||||
['#2a2118', '#131E29'], ['#3a322a', '#556B82'], ['#756a5e', '#5F7590'],
|
||||
['#9a8e80', '#5F7590'], ['#8a7f73', '#758CA4'], ['#b8ac9e', '#A9B4BE'], ['#c9beb0', '#BBC1CA'],
|
||||
['#ffb31a', '#E76500'], ['#fff6e0', '#FDF1E8'],
|
||||
['#d9481c', '#AA0808'], ['#fdeee8', '#FBF0F0'],
|
||||
['#2e9e5b', '#256F3A'], ['#ecf7f0', '#F1F7F3'], ['#1f8a5b', '#256F3A'], ['#ea580c', '#B44F00'],
|
||||
['#f08060', '#D98A8A'], ['#fbbf24', '#D27700'],
|
||||
['#2f2820', '#0A1D35'], ['#4a4036', '#1E2A36'], ['#d3dce6', '#223040'], ['#99a9bf', '#6B7F94'],
|
||||
// 图表/其它残留的 Tailwind 色
|
||||
['#ef4444', '#D20A0A'], ['#3b82f6', '#0057D2'], ['#0f766e', '#07838F'],
|
||||
['#14b8a6', '#07838F'], ['#7c3aed', '#7858FF'], ['#f5222d', '#D20A0A'],
|
||||
// H5 专有
|
||||
['#36cfc9', '#4DA37A'], ['#ff9a3d', '#4D9BF6'], ['#10b981', '#188918']
|
||||
];
|
||||
|
||||
const files = [];
|
||||
for (const { root, skip } of ROOTS) {
|
||||
(function walk(d) {
|
||||
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
||||
const p = path.join(d, e.name);
|
||||
const rel = path.relative(root, p).replace(/\\/g, '/');
|
||||
if (e.isDirectory()) { if (!skip.has(rel)) walk(p); continue; }
|
||||
if (EXT.has(path.extname(e.name).toLowerCase())) files.push(p);
|
||||
}
|
||||
})(root);
|
||||
}
|
||||
|
||||
const tally = new Map();
|
||||
let changedFiles = 0;
|
||||
|
||||
for (const f of files) {
|
||||
let txt = fs.readFileSync(f, 'utf8');
|
||||
const orig = txt;
|
||||
for (const [label, pat, rep, kind] of RULES) {
|
||||
const n = kind === 'literal'
|
||||
? (txt.split(pat).length - 1)
|
||||
: (txt.match(pat) || []).length;
|
||||
if (!n) continue;
|
||||
txt = kind === 'literal' ? txt.split(pat).join(rep) : txt.replace(pat, rep);
|
||||
tally.set(label, (tally.get(label) || 0) + n);
|
||||
}
|
||||
for (const [from, to] of GENERIC) {
|
||||
const n = (txt.match(new RegExp(from, 'gi')) || []).length;
|
||||
if (!n) continue;
|
||||
txt = txt.replace(new RegExp(from, 'gi'), to);
|
||||
const k = '单色 ' + from + ' -> ' + to;
|
||||
tally.set(k, (tally.get(k) || 0) + n);
|
||||
}
|
||||
if (txt !== orig) { changedFiles++; if (WRITE) fs.writeFileSync(f, txt); }
|
||||
}
|
||||
|
||||
console.log('=== 规则命中统计 ===');
|
||||
[...tally.entries()].sort((a, b) => b[1] - a[1]).forEach(([k, v]) => {
|
||||
console.log(' ' + String(v).padStart(5) + ' ' + k);
|
||||
});
|
||||
const total = [...tally.values()].reduce((a, b) => a + b, 0);
|
||||
console.log('\n涉及文件 ' + changedFiles + ' 个 / 替换合计 ' + total + ' 处');
|
||||
console.log(WRITE ? '已写入' : '预演,未写入(加 --write 生效)');
|
||||
@@ -0,0 +1,95 @@
|
||||
// 基座影响审计:逐行验证本次换肤的改动是否「只改颜色」
|
||||
// 做法:解析 git diff -U0,把每段 hunk 的「删除行」与「新增行」都做颜色归一化
|
||||
// (#RRGGBB / rgb() / rgba() → 占位符),再比对两侧是否等价。
|
||||
// 等价 = 纯换色;不等价 = 有非颜色改动,单独列出待人工判断。
|
||||
// 用法: node _design-test/audit-color-only.js
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const ZF = 'e:/KF_7/ZhenOS-ZF';
|
||||
const raw = execSync('git -c core.quotepath=false diff -U0 --no-color', {
|
||||
cwd: ZF, maxBuffer: 1 << 28, encoding: 'utf8'
|
||||
});
|
||||
|
||||
const CO = /#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|\bhsl\([^)]*\)/g;
|
||||
const norm = s => s.replace(CO, '@C@').replace(/\s+/g, ' ').trim();
|
||||
|
||||
const isComment = s => {
|
||||
const t = s.trim();
|
||||
return t.startsWith('//') || t.startsWith('/*') || t.startsWith('*') ||
|
||||
t.startsWith('<!--') || t.startsWith('{#') || t === '' ;
|
||||
};
|
||||
|
||||
let curFile = '';
|
||||
const offenders = [];
|
||||
let hunks = 0, colorOnly = 0, commentOnly = 0, removed = 0, added = 0;
|
||||
|
||||
const lines = raw.split('\n');
|
||||
let bucket = null;
|
||||
const flush = () => {
|
||||
if (!bucket) return;
|
||||
const { file, oldL, newL } = bucket;
|
||||
hunks++;
|
||||
removed += oldL.length; added += newL.length;
|
||||
const a = oldL.map(norm).sort().join('\n');
|
||||
const b = newL.map(norm).sort().join('\n');
|
||||
if (a === b) { colorOnly++; return; }
|
||||
// 只有注释行不同 → 视为注释改动(不影响行为)
|
||||
const o = oldL.filter(l => !isComment(l)), n2 = newL.filter(l => !isComment(l));
|
||||
if (o.map(norm).sort().join('\n') === n2.map(norm).sort().join('\n')) { commentOnly++; return; }
|
||||
offenders.push({ file, oldL, newL });
|
||||
bucket = null;
|
||||
};
|
||||
|
||||
for (const ln of lines) {
|
||||
if (ln.startsWith('diff --git ')) { flush(); bucket = null; curFile = ln.split(' b/').pop(); continue; }
|
||||
if (ln.startsWith('+++ ') || ln.startsWith('--- ') || ln.startsWith('index ') ||
|
||||
ln.startsWith('new file') || ln.startsWith('deleted file') || ln.startsWith('similarity')) continue;
|
||||
if (ln.startsWith('@@')) { flush(); bucket = { file: curFile, oldL: [], newL: [] }; continue; }
|
||||
if (!bucket) continue;
|
||||
if (ln.startsWith('-')) bucket.oldL.push(ln.slice(1));
|
||||
else if (ln.startsWith('+')) bucket.newL.push(ln.slice(1));
|
||||
}
|
||||
flush();
|
||||
|
||||
console.log('=== 换肤改动审计(git diff -U0 逐段比对)===');
|
||||
console.log(' 变更行数 新增 ' + added + ' / 删除 ' + removed);
|
||||
console.log(' 代码段(hunk) 共 ' + hunks);
|
||||
console.log(' · 纯颜色替换 ' + colorOnly);
|
||||
console.log(' · 仅注释差异 ' + commentOnly);
|
||||
console.log(' · 需要人工复核 ' + offenders.length);
|
||||
|
||||
// 本次「整文件重写」的样式文件 —— 它们的差异里必然含注释/结构变化,属已知改动,单列
|
||||
const REWRITTEN = new Set([
|
||||
'ruoyi-ui/src/assets/styles/element-variables.scss',
|
||||
'ruoyi-ui/src/assets/styles/theme.scss',
|
||||
'ruoyi-ui/src/assets/styles/element-ui.scss',
|
||||
'ruoyi-ui/src/assets/styles/variables.scss',
|
||||
'ruoyi-ui/src/assets/styles/ruoyi.scss',
|
||||
'ruoyi-ui/src/assets/styles/sidebar.scss',
|
||||
'ruoyi-ui/src/assets/styles/btn.scss',
|
||||
'ruoyi-ui/src/assets/styles/mixin.scss',
|
||||
'zhenfang-uni/src/uni.scss'
|
||||
]);
|
||||
|
||||
const byFile = {};
|
||||
offenders.forEach(o => { (byFile[o.file] = byFile[o.file] || []).push(o); });
|
||||
const keys = Object.keys(byFile);
|
||||
const rewritten = keys.filter(k => REWRITTEN.has(k));
|
||||
const others = keys.filter(k => !REWRITTEN.has(k));
|
||||
|
||||
console.log('\n ── 重写类文件(已知改动,含注释/结构重排):' + rewritten.length + ' 个');
|
||||
rewritten.forEach(k => console.log(' ' + String(byFile[k].length).padStart(3) + ' 段 ' + k));
|
||||
|
||||
console.log('\n ── 其它文件的疑似非颜色改动:' + others.length + ' 个文件');
|
||||
if (!others.length) {
|
||||
console.log(' 无 —— 业务页面/组件/后端相关文件全部只改了颜色');
|
||||
} else {
|
||||
others.forEach(k => {
|
||||
console.log('\n ' + k + ' (' + byFile[k].length + ' 段)');
|
||||
byFile[k].slice(0, 4).forEach(o => {
|
||||
o.oldL.slice(0, 3).forEach(l => console.log(' - ' + l.trim()));
|
||||
o.newL.slice(0, 3).forEach(l => console.log(' + ' + l.trim()));
|
||||
console.log(' ---');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# 生成 Electron 两个资源:
|
||||
# 1) build/icons/icon.icns —— 真正的 ICNS 容器(原文件其实是改了扩展名的 PNG,属坏文件)
|
||||
# 2) resources/app.png —— 启动背景稿,按甄房品牌重做
|
||||
# 用法: python _design-test/build-electron-assets.py (预演)
|
||||
# python _design-test/build-electron-assets.py --write (真写)
|
||||
import os, sys, struct, io
|
||||
from PIL import Image
|
||||
|
||||
SRC = r'D:\SGP_KF\DrawLogo\_design-test\logo\png'
|
||||
ZF = r'e:\KF_7\ZhenOS-ZF'
|
||||
WRITE = '--write' in sys.argv
|
||||
ICNS_OUT = f'{ZF}/xiaohuju/build/icons/icon.icns'
|
||||
APP_OUT = f'{ZF}/xiaohuju/resources/app.png'
|
||||
|
||||
# ── 1. ICNS:容器 = 'icns' + 总长度 + [type(4) + 长度(4, 含头) + PNG 数据] ──
|
||||
# 类型码为 Apple 官方定义;PNG 载荷自 macOS 10.7 起原生支持
|
||||
ICNS_TYPES = [
|
||||
('icp4', 16), ('icp5', 32), ('icp6', 64),
|
||||
('ic07', 128), ('ic08', 256), ('ic09', 512), ('ic10', 1024),
|
||||
('ic11', 32), ('ic12', 64), ('ic13', 256), ('ic14', 512),
|
||||
]
|
||||
|
||||
base = Image.open(os.path.join(SRC, 'appicon-1024.png')).convert('RGBA')
|
||||
chunks = []
|
||||
for tag, size in ICNS_TYPES:
|
||||
buf = io.BytesIO()
|
||||
base.resize((size, size), Image.LANCZOS).save(buf, format='PNG', optimize=True)
|
||||
data = buf.getvalue()
|
||||
chunks.append(tag.encode('ascii') + struct.pack('>I', len(data) + 8) + data)
|
||||
body = b''.join(chunks)
|
||||
icns = b'icns' + struct.pack('>I', len(body) + 8) + body
|
||||
print(' icon.icns %d 个尺寸 %d 字节 容器合法=%s'
|
||||
% (len(ICNS_TYPES), len(icns), icns[:4] == b'icns' and struct.unpack('>I', icns[4:8])[0] == len(icns)))
|
||||
|
||||
# ── 2. app.png:启动背景稿重做 ──
|
||||
# 原稿是 3.2MB 的满幅蓝色装饰图(2208x2048,全宽 + 上下白边),且全仓库零代码引用(孤儿资源)。
|
||||
# 重做思路:与 splash.html / 应用图标统一到「墨蓝深色外壳」语言 —— 墨蓝渐变底 + 顶部品牌蓝辉光 + 居中白色横版锁定标。
|
||||
W, H = 2208, 2048
|
||||
|
||||
# 底色:墨蓝渐变(#0C2340 → #071527)
|
||||
bg = Image.new('RGB', (W, H))
|
||||
top, bot = (0x0C, 0x23, 0x40), (0x07, 0x15, 0x27)
|
||||
for y in range(H):
|
||||
k = y / (H - 1)
|
||||
bg.paste(tuple(round(top[i] + (bot[i] - top[i]) * k) for i in range(3)), (0, y, W, y + 1))
|
||||
|
||||
# 顶部品牌蓝辉光(呼应 splash.html 的 .splash::before)—— 1/8 分辨率算完再放大,纯 Python 逐像素太慢
|
||||
gw, gh = W // 8, H // 8
|
||||
glow = Image.new('L', (gw, gh), 0)
|
||||
gp = glow.load()
|
||||
cx0, cy0, rx, ry = gw / 2, gh * 0.28, gw * 0.62, gh * 0.34
|
||||
for y in range(gh):
|
||||
dy = (y - cy0) / ry
|
||||
row = dy * dy
|
||||
for x in range(gw):
|
||||
dx = (x - cx0) / rx
|
||||
d = (row + dx * dx) ** 0.5
|
||||
if d < 1:
|
||||
gp[x, y] = int(255 * (1 - d) ** 1.6)
|
||||
glow = glow.resize((W, H), Image.BILINEAR)
|
||||
bg = Image.composite(Image.new('RGB', (W, H), (0x00, 0x70, 0xF2)), bg, glow.point(lambda v: v // 6))
|
||||
|
||||
# 居中横版锁定标(反白),宽度占画布 42%
|
||||
lock = Image.open(os.path.join(SRC, 'logo-reverse-800.png')).convert('RGBA')
|
||||
lb = lock.getchannel('A').getbbox()
|
||||
lock = lock.crop(lb)
|
||||
k = (W * 0.42) / lock.width
|
||||
lock = lock.resize((round(lock.width * k), round(lock.height * k)), Image.LANCZOS)
|
||||
|
||||
canvas = bg.convert('RGBA')
|
||||
canvas.paste(lock, ((W - lock.width) // 2, (H - lock.height) // 2), lock)
|
||||
out = canvas.convert('RGB')
|
||||
print(' app.png 新稿 %dx%d 锁定标 %dx%d 居中' % (W, H, lock.width, lock.height))
|
||||
|
||||
if WRITE:
|
||||
open(ICNS_OUT, 'wb').write(icns)
|
||||
out.save(APP_OUT, optimize=True)
|
||||
print('\n已写入 icon.icns / app.png')
|
||||
else:
|
||||
print('\n预演,未写入(加 --write 生效)')
|
||||
@@ -0,0 +1,56 @@
|
||||
# 生成甄房 logo 位图替换资源
|
||||
# 关键:旧 logo 的墨迹占画布约 90%,而甄房 mark 只占 69% —— 直接替换会让 logo 视觉缩小 25%。
|
||||
# 本脚本把 mark 的墨迹等比缩放到占画布 90%,保持与原资源等大,替换后不改变版式。
|
||||
# 用法: python _design-test/build-logos.py (预演)
|
||||
# python _design-test/build-logos.py --write (真写)
|
||||
import os, sys, shutil
|
||||
from PIL import Image
|
||||
|
||||
SRC = r'D:\SGP_KF\DrawLogo\_design-test\logo\png'
|
||||
ZF = r'e:\KF_7\ZhenOS-ZF'
|
||||
WRITE = '--write' in sys.argv
|
||||
INK_RATIO = 0.90 # 墨迹占画布比例,与原资源对齐
|
||||
|
||||
|
||||
def fitted(src_name, canvas, ratio=INK_RATIO):
|
||||
"""把 src 的墨迹等比缩放到占 canvas 的 ratio,居中放到透明画布上"""
|
||||
im = Image.open(os.path.join(SRC, src_name)).convert('RGBA')
|
||||
bb = im.getchannel('A').getbbox()
|
||||
ink = im.crop(bb)
|
||||
k = (canvas * ratio) / max(ink.width, ink.height)
|
||||
ink = ink.resize((max(1, round(ink.width * k)), max(1, round(ink.height * k))), Image.LANCZOS)
|
||||
out = Image.new('RGBA', (canvas, canvas), (0, 0, 0, 0))
|
||||
out.paste(ink, ((canvas - ink.width) // 2, (canvas - ink.height) // 2), ink)
|
||||
return out
|
||||
|
||||
|
||||
JOBS = [
|
||||
# 目标路径, 生成方式, 说明
|
||||
(f'{ZF}/ruoyi-ui/src/assets/logo/logo.png', ('fitted', 'mark-p80-ink.png', 1000), 'WEB 登录页/工具页 · 墨蓝标'),
|
||||
(f'{ZF}/ruoyi-ui/src/assets/logo/logo-sidebar.png', ('fitted', 'mark-p80-white.png', 1000), 'WEB 侧栏(墨蓝底)· 白标'),
|
||||
(f'{ZF}/zhenfang-uni/src/static/logo.png', ('fitted', 'mark-p80-ink.png', 1000), 'H5 登录页(白卡内)· 墨蓝标'),
|
||||
(f'{ZF}/ruoyi-ui/public/favicon.png', ('plain', 'appicon-512.png', None), 'WEB favicon · 应用图标'),
|
||||
(f'{ZF}/xiaohuju/build/icons/icon.png', ('plain', 'appicon-512.png', None), 'Electron 托盘/窗口图标'),
|
||||
]
|
||||
|
||||
ICO_OUT = f'{ZF}/xiaohuju/build/icons/icon.ico'
|
||||
|
||||
for dest, spec, label in JOBS:
|
||||
before = os.path.getsize(dest) if os.path.exists(dest) else 0
|
||||
kind = spec[0]
|
||||
img = fitted(spec[1], spec[2]) if kind == 'fitted' else Image.open(os.path.join(SRC, spec[1])).convert('RGBA')
|
||||
print(' %-58s %sx%s (%dB -> ~)' % (label, img.width, img.height, before))
|
||||
if WRITE:
|
||||
img.save(dest)
|
||||
|
||||
# 多尺寸 ICO(含 256,Windows 图标规范)
|
||||
ico_sizes = [16, 24, 32, 48, 64, 128, 256]
|
||||
base = Image.open(os.path.join(SRC, 'appicon-1024.png')).convert('RGBA')
|
||||
imgs = [base.resize((s, s), Image.LANCZOS) for s in ico_sizes]
|
||||
print(' %-58s %s' % ('Electron 应用图标 ICO', '/'.join(str(s) for s in ico_sizes)))
|
||||
if WRITE:
|
||||
imgs[-1].save(ICO_OUT, format='ICO', sizes=[(s, s) for s in ico_sizes])
|
||||
|
||||
print('\n' + ('已写入' if WRITE else '预演,未写入(加 --write 生效)'))
|
||||
print('注:xiaohuju/build/icons/icon.icns(macOS)需在 macOS 上用 iconutil 生成,本机跳过;')
|
||||
print(' xiaohuju/resources/app.png 是启动背景稿(含蓝色主体图形),需设计重做,本脚本不动。')
|
||||
@@ -0,0 +1,989 @@
|
||||
// 生成「甄房」LOGO 素材挑选页(自包含单文件 HTML,可直接双击打开)
|
||||
// 用法: node _design-test/build-page.js
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(path.join(__dirname, 'house-icons.json'), 'utf8'));
|
||||
const TIERS = data.tiers;
|
||||
|
||||
// ── 字体清单(本地 OFL + 本机系统字体)────────────────────────────
|
||||
const FONTS = JSON.parse(fs.readFileSync(path.join(__dirname, 'font-specimens.json'), 'utf8'));
|
||||
const FONT_CATS = Object.keys(FONTS.cats);
|
||||
const fontFaces = FONTS.fonts.filter(f => f.kind === 'ofl').map(f =>
|
||||
"@font-face{font-family:'" + f.family + "';src:url('" + f.src + "') format('" +
|
||||
(f.fmt === 'ttf' ? 'truetype' : (f.fmt || 'woff2')) + "');font-display:swap}"
|
||||
).join('\n');
|
||||
const LIC_BAD = /BY-SA|BY-NC|BY-ND|GPL|-NC-|NonCommercial|非商业/i;
|
||||
|
||||
// ── 「甄房」LOGO 概念方案(本次设计)──────────────────────────────
|
||||
const CONCEPTS = [
|
||||
{
|
||||
id: 'zhen-01', name: '甄选之屋', en: 'Selected House', tag: '对勾 · 最直白',
|
||||
desc: '屋顶 + 墙体 + 对勾。一套图形讲完「甄选好房」,识别度最高,缩小到 favicon 仍清晰。适合做主标。',
|
||||
mode: 'stroke',
|
||||
svg: '<path d="M6 30.5 L32 8 L58 30.5"/><path d="M13.5 29.5 V56 H50.5 V29.5"/><path d="M23.5 43 l7 7 L44 31.5"/>'
|
||||
},
|
||||
{
|
||||
id: 'zhen-02', name: '檐瓦徽章', en: 'Eave Tile Badge', tag: '中式 · 圆章',
|
||||
desc: '圆形徽章内一条上翘屋檐 + 两重瓦纹。「甄」本义为陶瓦甄别,此稿把字义直接画进图形,最有文化底子。',
|
||||
mode: 'stroke',
|
||||
svg: '<circle cx="32" cy="32" r="26"/><path d="M11 25.5 C19 15, 45 15, 53 25.5"/><path d="M19.5 34 q12.5 -7 25 0"/><path d="M19.5 43 q12.5 -7 25 0"/>'
|
||||
},
|
||||
{
|
||||
id: 'zhen-03', name: '窗格之家', en: 'Window House', tag: '几何 · 秩序',
|
||||
desc: '屋顶 + 田字窗格。窗格本身有「筛选、框选」的隐喻,结构方正,延展做栅格、水印、加载态都很顺。',
|
||||
mode: 'stroke',
|
||||
svg: '<path d="M7 27 L32 7.5 L57 27"/><rect x="14.5" y="27" width="35" height="29.5" rx="3.5"/><path d="M32 27 V56.5"/><path d="M14.5 41.5 H49.5"/>'
|
||||
},
|
||||
{
|
||||
id: 'zhen-04', name: '印章 · 甄', en: 'Seal Zhen', tag: '汉字 · 印鉴',
|
||||
desc: '圆角方印 + 甄字。最贴近「甄房」二字的字面表达,气质正统稳重,适合政务、金融类房产业务线。',
|
||||
mode: 'mixed',
|
||||
svg: '<rect x="5.5" y="5.5" width="53" height="53" rx="11"/>' +
|
||||
'<text x="32" y="43.5" text-anchor="middle" font-size="31" font-weight="600" font-family="Songti SC,STSong,SimSun,serif" fill="currentColor" stroke="none">甄</text>'
|
||||
},
|
||||
{
|
||||
id: 'zhen-05', name: '钥匙之家', en: 'Key House', tag: '门锁 · 交付',
|
||||
desc: '实心屋形挖出钥匙孔。指向「交付钥匙 = 成交」,适合交易、租售、代办过户等业务。',
|
||||
mode: 'stroke',
|
||||
svg: '<path d="M7 28.5 L32 7.5 L57 28.5 V56 H7 Z"/><circle cx="32" cy="34" r="6.5"/><path d="M32 40.5 V51"/>'
|
||||
},
|
||||
{
|
||||
id: 'zhen-06', name: '找房定位', en: 'Find A Home', tag: '定位 · 找房',
|
||||
desc: '地图定位针内嵌小屋。天然表达「在你想要的位置找到房」,适合 C 端找房 App、地图找房入口。',
|
||||
mode: 'stroke',
|
||||
svg: '<path d="M32 5 C22.5 5 15 12.5 15 22 c0 11.5 17 29 17 29 s17 -17.5 17 -29 C49 12.5 41.5 5 32 5 z"/><path d="M23.5 22.5 L32 15.5 L40.5 22.5"/><path d="M26.5 22.5 V30 H37.5 V22.5"/>'
|
||||
},
|
||||
{
|
||||
id: 'zhen-07', name: '联排双屋', en: 'Twin Houses', tag: '多房源 · 平台',
|
||||
desc: '两栋联排小屋并列。强调「房源多、成规模」,适合房产平台、小区/楼盘库这类主标。',
|
||||
mode: 'stroke',
|
||||
svg: '<path d="M4 34 L17 23 L30 34"/><path d="M8 33 V56 H26 V33"/>' +
|
||||
'<path d="M34 34 L47 23 L60 34"/><path d="M38 33 V56 H56 V33"/>'
|
||||
},
|
||||
{
|
||||
id: 'zhen-08', name: '层叠甄选', en: 'Layered Pick', tag: '抽象 · 现代',
|
||||
desc: '屋顶下三层递减横线,像层层筛选的房源列表,也呼应「甄」字的多横结构。最现代、最像互联网品牌。',
|
||||
mode: 'stroke',
|
||||
svg: '<path d="M6 25 L32 7 L58 25"/><path d="M13 32.5 H51"/><path d="M18 40.5 H46"/><path d="M23 48.5 H41"/>'
|
||||
}
|
||||
];
|
||||
|
||||
// ── 第二批:home-04 外框 + 字母 Z(直接用 home-04 原始路径,外框几何固定)────
|
||||
// 外框 = 素材库 hugeicons:home-04(MIT)的原始 path,按 24→64 网格等比放大(stroke 同步放大,粗细比例与原图一致)
|
||||
// 唯一改动:去掉原图底部那条内部横线(M16 17H8)——它正好落在 Z 的位置,且不属于「外框」
|
||||
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)'; // 24 网格 → 64 网格,外框落位 12~52 / 10.9~53.1,中心 (32,32)
|
||||
const INK = '#0C2340', GOLD = '#C6A15B', WH = '#FFFFFF'; // #0C2340 = 定稿色板「甄房蓝 900」
|
||||
|
||||
// 定稿字形:Forte 的 Z(已由 fontTools 抽成真实曲线,见 logo/glyphs.json)
|
||||
const GLYPH_FILE = path.join(__dirname, 'logo', 'glyphs.json');
|
||||
const GLYPHS = fs.existsSync(GLYPH_FILE) ? JSON.parse(fs.readFileSync(GLYPH_FILE, 'utf8')) : {};
|
||||
const FORTEZ = GLYPHS.forteZ ? GLYPHS.forteZ.d : '';
|
||||
const ZCX = GLYPHS.forteZ ? GLYPHS.forteZ.placed[0] + GLYPHS.forteZ.placed[2] / 2 : 32; // Z 着墨中心 x
|
||||
// 水平拉伸(只拉宽、不动高):绕 Z 自身中心缩放,宽度倍数 sx
|
||||
const wideZ = (sx) => '<g transform="translate(' + ZCX + ' 0) scale(' + sx + ' 1) translate(-' + ZCX + ' 0)"><path d="' +
|
||||
FORTEZ + '" fill="' + INK + '"/></g>';
|
||||
const frame = (sw, color) => '<g transform="' + FG + '"><path d="' + H04 + '" fill="none" stroke="' + color +
|
||||
'" stroke-width="' + sw + '" stroke-linecap="round" stroke-linejoin="round"/></g>';
|
||||
const ZSTD = 'M21.5 30 H42.5 V35.4 L30 42.1 H42.5 V47.5 H21.5 V42.1 L34 35.4 H21.5 Z'; // 实心方块 Z(深底反白用)
|
||||
const ZSKEL = 'M23 31.5 H41 L23 46 H41'; // 圆角 Z 骨架
|
||||
|
||||
// Z 统一字面框:x 21.5~42.5、y 30~47.5(各款按笔画宽自动内缩,保证字面大小一致)
|
||||
const ZB = { x0: 21.5, x1: 42.5, y0: 30, y1: 47.5 };
|
||||
// 骨架式 Z:给定笔画宽度与端点,自动算出中心线端点,使描边后的外沿正好落进字面框
|
||||
function zline(w, o) {
|
||||
o = o || {};
|
||||
var caps = o.caps || 'butt', pad = caps === 'round' ? w / 2 : 0;
|
||||
var x0 = ZB.x0 + w / 2 + pad, x1 = ZB.x1 - w / 2 - pad;
|
||||
var y0 = ZB.y0 + w / 2 + pad, y1 = ZB.y1 - w / 2 - pad;
|
||||
return '<path d="M' + x0 + ' ' + y0 + ' H' + x1 + ' L' + x0 + ' ' + y1 + ' H' + x1 +
|
||||
'" fill="none" stroke="' + (o.color || INK) + '" stroke-width="' + w +
|
||||
'" stroke-linecap="' + caps + '" stroke-linejoin="' + (o.join || 'miter') + '"/>';
|
||||
}
|
||||
|
||||
const CONCEPTS_FRAME = [
|
||||
// ── 已定稿 ───────────────────────────────────────────
|
||||
...(FORTEZ ? [{
|
||||
id: 'zf-final', star: 2, name: 'Forte Z', en: 'Final', tag: '已定稿 ✓',
|
||||
desc: '你选定的最终字形:Forte 的大写 Z。已用 fontTools 从字体里抽出真实曲线(不是位图描摹),所以不依赖字体环境即可使用;完整资产(SVG + 36 个位图 + 色板)见顶部「定稿交付」链接。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) + '<path d="' + FORTEZ + '" fill="' + INK + '"/>'
|
||||
},
|
||||
{
|
||||
id: 'zf-w115', name: 'Forte Z · 拉宽 15%', en: 'Wide 115', tag: '水平拉伸 ×1.15',
|
||||
desc: '外框与字高完全不动,只把 Z 水平拉宽 15%。最克制的一档,比原版略舒展,几乎看不出被拉过——想要「稳」就选它。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) + wideZ(1.15)
|
||||
},
|
||||
{
|
||||
id: 'zf-w130', name: 'Forte Z · 拉宽 30%', en: 'Wide 130', tag: '水平拉伸 ×1.30',
|
||||
desc: '明显变宽的一档:横画被拉长、字腔更开阔,整体更「阔气」。放在方框里左右留白更窄,视觉重心更稳,是我建议优先考虑的一档。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) + wideZ(1.30)
|
||||
},
|
||||
{
|
||||
id: 'zf-w145', name: 'Forte Z · 拉宽 45%', en: 'Wide 145', tag: '水平拉伸 ×1.45',
|
||||
desc: '宽体感最强的一档,接近专业字体的「extended」字重观感。注意:水平拉伸会让竖画显厚、斜画变缓,再往上就会开始显胖。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) + wideZ(1.45)
|
||||
}] : []),
|
||||
// ── 保留三款 ─────────────────────────────────────────
|
||||
{
|
||||
id: 'zf-k1', star: 1, name: '深底反白', en: 'Dark Reverse', tag: '已保留 · 深色场景',
|
||||
desc: '深墨蓝圆角底 + 白框白 Z。深色界面、App 图标、深色名片用这一版。',
|
||||
mode: 'solid',
|
||||
svg: '<rect x="5" y="5" width="54" height="54" rx="13" fill="' + INK + '"/>' +
|
||||
frame(1.5, WH) + '<path d="' + ZSTD + '" fill="' + WH + '"/>'
|
||||
},
|
||||
{
|
||||
id: 'zf-k2', star: 1, name: '圆角 Z', en: 'Round Z', tag: '已保留 · 亲和',
|
||||
desc: 'Z 用 5.2 圆头描边,转角全圆,跟圆润的框最统一。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) +
|
||||
'<path d="' + ZSKEL + '" fill="none" stroke="' + INK + '" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>'
|
||||
},
|
||||
{
|
||||
id: 'zf-k3', star: 1, name: '线框 Z', en: 'Line Z', tag: '已保留 · 轻',
|
||||
desc: 'Z 用 3.6 描边空心字,整体透气,设计工作室调性。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) +
|
||||
'<path d="' + ZSKEL + '" fill="none" stroke="' + INK + '" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>'
|
||||
},
|
||||
|
||||
// ── 新增 Z 字形(外框不动,只换 Z)──────────────────────
|
||||
{
|
||||
id: 'zf-01', name: '锐折 Z', en: 'Sharp Z', tag: '尖角 · 硬朗',
|
||||
desc: '3.6 等宽细线,但转角是尖的(miter 直角),端点平切。比圆角版硬朗、比实心版轻,最有「精密工具」气质。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) + zline(3.6, { caps: 'butt', join: 'miter' })
|
||||
},
|
||||
{
|
||||
id: 'zf-02', name: '方头粗 Z', en: 'Square Bold', tag: '几何 · 压得住',
|
||||
desc: '5.4 粗线、方头方角,像 Helvetica 的骨架直接画出来。远近都清楚,做门头招牌没问题。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) + zline(5.4, { caps: 'butt', join: 'miter' })
|
||||
},
|
||||
{
|
||||
id: 'zf-03', name: '细线 Z', en: 'Hairline', tag: '极细 · 高级',
|
||||
desc: '笔画只有 2.4,跟外框的细线等重。整个标看起来像一张线描稿,适合高端、克制的物料。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) + zline(2.4, { caps: 'butt', join: 'miter' })
|
||||
},
|
||||
{
|
||||
id: 'zf-04', name: '超粗 Z', en: 'Heavy', tag: '最重 · 冲击力',
|
||||
desc: '笔画 6.6,几乎把框内填满。远看是一个厚重的方块符号,适合做车贴、导视、单色印章。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) + zline(6.6, { caps: 'butt', join: 'miter' })
|
||||
},
|
||||
{
|
||||
id: 'zf-05', name: '宽体 Z', en: 'Extended', tag: '宽扁 · 稳',
|
||||
desc: '字面拉宽到 27(比标准宽 28%),笔画 4.4。宽扁的字给人「稳、可信」的心理暗示,适合金融/政务向。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) + '<path d="M20.7 32.2 H43.3 L20.7 45.3 H43.3" fill="none" stroke="' + INK +
|
||||
'" stroke-width="4.4" stroke-linecap="butt" stroke-linejoin="miter"/>'
|
||||
},
|
||||
{
|
||||
id: 'zf-06', name: '窄体 Z', en: 'Condensed', tag: '瘦高 · 利落',
|
||||
desc: '字面收到 13(标准的一半多点),笔画 5.4。瘦高的字显精致、有设计感,也留出更多呼吸。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) + '<path d="M28.2 32.7 H35.8 L28.2 44.8 H35.8" fill="none" stroke="' + INK +
|
||||
'" stroke-width="5.4" stroke-linecap="butt" stroke-linejoin="miter"/>'
|
||||
},
|
||||
{
|
||||
id: 'zf-07', name: '曲线斜画', en: 'Curved Z', tag: '柔 · 流动',
|
||||
desc: '把 Z 的斜画改成一条 S 弯。硬邦邦的字母一下有了流动感,和外框的圆弧线条也更呼应。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) + '<path d="M23.3 31.8 H40.7 C33.5 36, 30.5 41.5, 23.3 45.7 H40.7" fill="none" stroke="' + INK +
|
||||
'" stroke-width="3.6" stroke-linecap="butt" stroke-linejoin="round"/>'
|
||||
},
|
||||
{
|
||||
id: 'zf-08', name: '断笔 Z', en: 'Stencil', tag: '断开 · 有节奏',
|
||||
desc: '三笔全断开,像模板喷字。笔画的间隙让这个 Z 有了节奏感,也更容易在低精度场景下不糊成一团。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) +
|
||||
'<path d="M23.3 32.2 H40.7" fill="none" stroke="' + INK + '" stroke-width="3.4" stroke-linecap="butt"/>' +
|
||||
'<path d="M38.6 36.2 L25.4 42.6" fill="none" stroke="' + INK + '" stroke-width="3.4" stroke-linecap="butt"/>' +
|
||||
'<path d="M23.3 46.6 H40.7" fill="none" stroke="' + INK + '" stroke-width="3.4" stroke-linecap="butt"/>'
|
||||
},
|
||||
{
|
||||
id: 'zf-09', name: '空心字 Z', en: 'Hollow', tag: '真·空心',
|
||||
desc: '不是用线画的,是把一个实心 Z 内部挖空——所以横竖有真实的粗细与字腔,放大看是很正经的字母设计。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) +
|
||||
'<path fill-rule="evenodd" fill="' + INK + '" d="M21.5 30 H42.5 V35.4 L30 42.1 H42.5 V47.5 H21.5 V42.1 L34 35.4 H21.5 Z ' +
|
||||
'M25.5 33 H38.5 V35.6 L30.5 41.9 H38.5 V44.5 H25.5 V41.9 L33.5 35.6 H25.5 Z"/>'
|
||||
},
|
||||
{
|
||||
id: 'zf-10', name: '勾尾 Z', en: 'Check Tail', tag: '甄选 · 语义',
|
||||
desc: 'Z 的底横末端收笔上扬成一个勾,等于把「甄选/通过」这层意思藏进了字母里。形义双关,是这批里最有故事的一稿。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) +
|
||||
'<path d="M23.3 31.8 H40.7 L23.3 45.7 H32 L39.5 38.2" fill="none" stroke="' + INK +
|
||||
'" stroke-width="3.6" stroke-linecap="round" stroke-linejoin="round"/>'
|
||||
},
|
||||
{
|
||||
id: 'zf-11', name: '粗细对比 Z', en: 'Contrast', tag: '衬线感 · 优雅',
|
||||
desc: '横画加粗到 6、斜画压细到 2.3,做出传统衬线字的粗细对比。优雅、有书卷气,适合线下高端品牌。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) +
|
||||
'<path fill="' + INK + '" d="M21.5 30 H42.5 V36 L29 41.5 H42.5 V47.5 H21.5 V41.5 H23 L36.5 36 H21.5 Z"/>'
|
||||
},
|
||||
{
|
||||
id: 'zf-12', name: '金线 Z', en: 'Gold Line', tag: '墨蓝 + 金',
|
||||
desc: '线框 Z 的配色版:框留墨蓝、Z 换金色 #C6A15B。比实心金 Z 更含蓄,烫金、压凹工艺都能做。',
|
||||
mode: 'solid',
|
||||
svg: frame(1.5, INK) + zline(3.6, { caps: 'butt', join: 'miter', color: GOLD })
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
// ── 数据瘦身 + 预置检索字段 ──────────────────────────────────────
|
||||
const items = data.list.map((it, i) => ({
|
||||
i: i,
|
||||
id: it.id, n: it.name, t: it.tier, s: it.source, sn: it.sourceName,
|
||||
ty: it.type, lic: it.license, bad: LIC_BAD.test(it.license || '') ? 1 : 0,
|
||||
svg: it.svg
|
||||
}));
|
||||
|
||||
const sources = {};
|
||||
items.forEach(x => { sources[x.s] = (sources[x.s] || 0) + 1; });
|
||||
const sourceList = Object.keys(sources).sort((a, b) => sources[b] - sources[a]);
|
||||
|
||||
const payload = {
|
||||
builtAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
||||
total: items.length,
|
||||
sourceCount: sourceList.length,
|
||||
tiers: TIERS.map(t => ({ key: t.key, label: t.label, desc: t.desc })),
|
||||
sources: sourceList.map(id => ({ id: id, count: sources[id], name: (items.find(x => x.s === id) || {}).sn || id })),
|
||||
items: items
|
||||
};
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>甄房 ZhenFang · LOGO 素材挑选台</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#0a0d13; --panel:#111621; --panel2:#161c29; --line:rgba(255,255,255,.08);
|
||||
--tx:#e9edf5; --mut:#8b94a7; --dim:#5d6577;
|
||||
--gold:#c9a961; --gold2:#e6cd93; --ok:#4ade80;
|
||||
--serif:"Songti SC","STSong","SimSun","Noto Serif SC",serif;
|
||||
}
|
||||
body{font-family:"PingFang SC","Microsoft YaHei","Segoe UI",sans-serif;background:var(--bg);color:var(--tx);
|
||||
-webkit-font-smoothing:antialiased;padding:0 0 80px}
|
||||
a{color:var(--gold)}
|
||||
.wrap{max-width:1560px;margin:0 auto;padding:0 28px}
|
||||
|
||||
/* ── Hero ── */
|
||||
.hero{padding:52px 0 34px;border-bottom:1px solid var(--line);
|
||||
background:radial-gradient(900px 320px at 18% -10%,rgba(201,169,97,.16),transparent 60%)}
|
||||
.hero h1{font-size:34px;letter-spacing:1px}
|
||||
.hero h1 b{font-family:var(--serif);font-weight:600;letter-spacing:6px;
|
||||
background:linear-gradient(100deg,var(--gold2),var(--gold));-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||
.hero .en{color:var(--dim);letter-spacing:7px;font-size:11px;margin-top:8px}
|
||||
.hero p.lead{color:var(--mut);font-size:13.5px;margin-top:16px;line-height:1.85;max-width:900px}
|
||||
.kpis{display:flex;gap:34px;margin-top:24px;flex-wrap:wrap}
|
||||
.kpi b{display:block;font-size:24px;color:var(--gold2);font-weight:600}
|
||||
.kpi span{font-size:11.5px;color:var(--mut)}
|
||||
|
||||
h2.sec{font-size:19px;margin:46px 0 6px;display:flex;align-items:center;gap:10px}
|
||||
h2.sec:before{content:"";width:4px;height:18px;border-radius:2px;background:linear-gradient(180deg,var(--gold2),var(--gold))}
|
||||
.secsub{color:var(--mut);font-size:12.5px;margin-bottom:20px;line-height:1.7}
|
||||
|
||||
/* ── 方案 ── */
|
||||
.concepts{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:18px}
|
||||
.cc{background:linear-gradient(180deg,var(--panel2),var(--panel));border:1px solid var(--line);border-radius:16px;
|
||||
padding:20px;display:flex;gap:18px;transition:.2s;position:relative;overflow:hidden}
|
||||
.cc:hover{border-color:rgba(201,169,97,.42);transform:translateY(-2px)}
|
||||
.cc .mark{width:88px;height:88px;flex:none;border-radius:20px;display:flex;align-items:center;justify-content:center;
|
||||
background:linear-gradient(150deg,#f7f3e9,#efe7d5);box-shadow:0 8px 22px rgba(0,0,0,.35)}
|
||||
.cc .mark svg{width:58px;height:58px;color:#1d2431}
|
||||
.cc.solid .mark{background:#fff;box-shadow:0 6px 18px rgba(0,0,0,.38),0 0 0 1px rgba(0,0,0,.06) inset}
|
||||
.cc.solid .mark svg{width:62px;height:62px}
|
||||
code{background:#0d1220;border:1px solid var(--line);border-radius:5px;padding:1px 5px;font-size:11px;color:var(--gold2)}
|
||||
.star{color:var(--gold)}
|
||||
.cc .body{flex:1;min-width:0}
|
||||
.cc .nm{font-size:15.5px;font-weight:600;display:flex;align-items:center;gap:8px}
|
||||
.cc .nm .star{color:var(--gold);font-size:12px;line-height:1}
|
||||
.cc .nm em{font-style:normal;font-size:10px;color:var(--gold);border:1px solid rgba(201,169,97,.4);
|
||||
padding:1px 7px;border-radius:20px;font-weight:400}
|
||||
.cc .lock{display:flex;align-items:baseline;gap:9px;margin:9px 0 8px}
|
||||
.cc .lock .zh{font-family:var(--serif);font-size:27px;letter-spacing:5px;font-weight:600}
|
||||
.cc .lock .en2{font-size:9.5px;letter-spacing:3.5px;color:var(--dim)}
|
||||
.cc .ds{font-size:12px;color:var(--mut);line-height:1.75}
|
||||
.cc .acts{margin-top:11px;display:flex;gap:8px}
|
||||
.cc .acts button{font-size:11px;padding:4px 10px;border-radius:7px;border:1px solid var(--line);
|
||||
background:#0d1220;color:var(--mut);cursor:pointer}
|
||||
.cc .acts button:hover{color:var(--gold2);border-color:rgba(201,169,97,.45)}
|
||||
|
||||
/* ── 挑选台 ── */
|
||||
.picker{display:grid;grid-template-columns:1fr 348px;gap:22px;align-items:start;margin-top:8px}
|
||||
.filters{position:sticky;top:0;z-index:9;background:linear-gradient(180deg,var(--bg) 72%,rgba(10,13,19,0));
|
||||
padding:12px 0 14px;display:flex;flex-direction:column;gap:11px}
|
||||
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
|
||||
input[type=search],select{background:#0d1220;border:1px solid var(--line);color:var(--tx);
|
||||
border-radius:9px;padding:8px 12px;font-size:12.5px;outline:none;font-family:inherit}
|
||||
input[type=search]{flex:1;min-width:220px}
|
||||
input[type=search]:focus,select:focus{border-color:rgba(201,169,97,.5)}
|
||||
.tabs{display:flex;gap:7px;flex-wrap:wrap}
|
||||
.tab{padding:6px 13px;border-radius:20px;border:1px solid var(--line);background:#0d1220;color:var(--mut);
|
||||
font-size:12px;cursor:pointer;white-space:nowrap;transition:.15s}
|
||||
.tab:hover{color:var(--tx)}
|
||||
.tab.on{background:linear-gradient(120deg,var(--gold),#a8873f);color:#17130a;border-color:transparent;font-weight:600}
|
||||
.tab i{font-style:normal;opacity:.62;margin-left:5px;font-size:10.5px}
|
||||
.chk{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--mut);cursor:pointer;user-select:none}
|
||||
.chk input{accent-color:var(--gold)}
|
||||
.cnt{font-size:11.5px;color:var(--dim);margin-left:auto}
|
||||
input[type=color]{width:34px;height:30px;padding:0;border:1px solid var(--line);border-radius:8px;background:#0d1220;cursor:pointer}
|
||||
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(112px,1fr));gap:10px}
|
||||
.ic{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:11px 7px 8px;cursor:pointer;
|
||||
display:flex;flex-direction:column;align-items:center;gap:7px;transition:.14s;position:relative}
|
||||
.ic:hover{border-color:rgba(201,169,97,.5);background:var(--panel2)}
|
||||
.ic.on{border-color:var(--gold);box-shadow:0 0 0 1px var(--gold) inset}
|
||||
.ic .bx{width:52px;height:52px;display:flex;align-items:center;justify-content:center;color:#c3cbd9}
|
||||
.ic .bx.multi{background:#f5f2ea;border-radius:12px}
|
||||
.ic.on .bx{color:var(--gold2)}
|
||||
.ic .lb{font-size:9.5px;color:var(--mut);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.ic .lic{position:absolute;top:5px;right:6px;font-size:8px;color:#f0a55c;display:none}
|
||||
.ic.bad .lic{display:block}
|
||||
.ic .fav{position:absolute;top:3px;left:4px;font-size:11px;line-height:1;opacity:0;transition:.15s}
|
||||
.ic:hover .fav,.ic.faved .fav{opacity:1}
|
||||
.more{grid-column:1/-1;text-align:center;padding:22px 0}
|
||||
.more button{padding:10px 26px;border-radius:10px;border:1px solid var(--line);background:var(--panel);
|
||||
color:var(--mut);cursor:pointer;font-size:12.5px;font-family:inherit}
|
||||
.more button:hover{color:var(--gold2);border-color:rgba(201,169,97,.45)}
|
||||
.empty{grid-column:1/-1;text-align:center;color:var(--dim);padding:60px 0;font-size:13px}
|
||||
|
||||
/* ── 详情面板 ── */
|
||||
.detail{position:sticky;top:14px;background:linear-gradient(180deg,var(--panel2),var(--panel));
|
||||
border:1px solid var(--line);border-radius:16px;padding:18px}
|
||||
.detail h3{font-size:13px;color:var(--mut);font-weight:500;margin-bottom:13px;display:flex;justify-content:space-between}
|
||||
.prev{display:flex;gap:10px;justify-content:center;margin-bottom:14px}
|
||||
.prev .p{width:82px;height:82px;border-radius:19px;display:flex;align-items:center;justify-content:center}
|
||||
.prev .p.round{border-radius:50%}
|
||||
.prev .p.square{border-radius:9px}
|
||||
.lockup{display:flex;align-items:center;gap:11px;padding:14px;border-radius:12px;background:#0d1220;
|
||||
border:1px solid var(--line);margin-bottom:13px}
|
||||
.lockup .bd{width:50px;height:50px;border-radius:13px;display:flex;align-items:center;justify-content:center;flex:none}
|
||||
.lockup .tw .zh{font-family:var(--serif);font-size:23px;letter-spacing:4px;font-weight:600;line-height:1.1}
|
||||
.lockup .tw .en3{font-size:8.5px;letter-spacing:3px;color:var(--dim);margin-top:3px}
|
||||
.meta{font-size:11px;color:var(--mut);line-height:1.95;word-break:break-all}
|
||||
.meta b{color:var(--dim);font-weight:400}
|
||||
.meta .warn{color:#f0a55c}
|
||||
.dacts{display:flex;gap:7px;flex-wrap:wrap;margin-top:13px}
|
||||
.dacts button{flex:1;min-width:82px;padding:8px 4px;border-radius:9px;border:1px solid var(--line);
|
||||
background:#0d1220;color:var(--mut);font-size:11.5px;cursor:pointer;font-family:inherit}
|
||||
.dacts button:hover{color:var(--gold2);border-color:rgba(201,169,97,.45)}
|
||||
.dacts button.pri{background:linear-gradient(120deg,var(--gold),#a8873f);color:#17130a;border-color:transparent;font-weight:600}
|
||||
.ph{color:var(--dim);font-size:12px;text-align:center;padding:34px 0;line-height:1.9}
|
||||
.faved-list{max-height:150px;overflow:auto;margin-top:11px;border-top:1px solid var(--line);padding-top:11px}
|
||||
.faved-list .fi{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:11px;color:var(--mut)}
|
||||
.faved-list .fi svg{width:18px;height:18px;flex:none;color:#c3cbd9}
|
||||
.faved-list .fi span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
||||
.faved-list .fi em{cursor:pointer;color:var(--dim);font-style:normal}
|
||||
.toast{position:fixed;left:50%;bottom:34px;transform:translateX(-50%) translateY(16px);opacity:0;
|
||||
background:#1b2130;border:1px solid rgba(201,169,97,.4);color:var(--gold2);padding:10px 20px;
|
||||
border-radius:11px;font-size:12.5px;pointer-events:none;transition:.22s;z-index:99}
|
||||
.toast.on{opacity:1;transform:translateX(-50%) translateY(0)}
|
||||
footer{margin-top:52px;border-top:1px solid var(--line);padding-top:22px;color:var(--dim);font-size:11.5px;line-height:2}
|
||||
@media(max-width:1080px){.picker{grid-template-columns:1fr}.detail{position:static}}
|
||||
|
||||
/* ── 字体样本 ── */
|
||||
.fsgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(198px,1fr));gap:12px}
|
||||
.fs{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:12px 10px 11px;text-align:center;
|
||||
cursor:pointer;transition:.14s}
|
||||
.fs:hover{border-color:rgba(201,169,97,.45);background:var(--panel2);transform:translateY(-2px)}
|
||||
.fs .tile{background:#fff;border-radius:12px;height:126px;display:flex;align-items:center;justify-content:center;
|
||||
margin-bottom:10px;box-shadow:0 4px 12px rgba(0,0,0,.28)}
|
||||
.fs .fn{font-size:11.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.fs .fc{font-size:9.5px;color:var(--mut);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.fs .fl{display:inline-block;font-size:9px;padding:1px 7px;border-radius:20px;margin-top:6px}
|
||||
.fs .fl.ofl{color:#5ee08a;border:1px solid rgba(94,224,138,.35)}
|
||||
.fs .fl.sys{color:#f0a55c;border:1px solid rgba(240,165,92,.3)}
|
||||
${fontFaces}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hero"><div class="wrap">
|
||||
<h1>「甄房」LOGO · <b>素材挑选台</b></h1>
|
||||
<div class="en">Z H E N F A N G · L O G O P I C K E R</div>
|
||||
<p class="lead">
|
||||
第一步:从你的设计项目素材库(画个标 · 共享图标库 222 源 / 37 万图标)里,把跟「房子」相关的素材全部捞了出来,
|
||||
去重、分层、按风格精选成 <b style="color:var(--gold2)">${items.length}</b> 款可挑选图形;<br>
|
||||
第二步:基于这批素材的造型语言,我另外设计了两批「甄房」LOGO 概念方案(下方 ${8 + CONCEPTS_FRAME.length} 稿,均为原创矢量,可直接用)——
|
||||
第一批 8 稿走<b style="color:var(--gold2)">线条 / 中式 / 印章</b>路线;第二批 ${CONCEPTS_FRAME.length} 稿是<b style="color:var(--gold2)">home-04 房子外框 + Z 字形</b>(外框一路不动,只换 Z 的字形与笔画);
|
||||
第三步:Z 的字形不再手画,直接扫你机器上已装的<b style="color:var(--gold2)">${FONTS.total} 款真字体</b>(素材库 OFL 12 款 + 系统字体 ${FONTS.total - 12} 款)渲染对比(第三节)。
|
||||
</p>
|
||||
<div class="kpis">
|
||||
<div class="kpi"><b>${items.length}</b><span>精选房子类素材</span></div>
|
||||
<div class="kpi"><b>${sourceList.length}</b><span>覆盖图标库源</span></div>
|
||||
<div class="kpi"><b>${8 + CONCEPTS_FRAME.length}</b><span>甄房 LOGO 方案</span></div>
|
||||
<div class="kpi"><b>${FONTS.total}</b><span>可试 Z 的字体</span></div>
|
||||
<div class="kpi"><b>${payload.builtAt}</b><span>素材生成时间</span></div>
|
||||
</div>
|
||||
<div style="margin-top:26px;background:linear-gradient(120deg,rgba(198,161,91,.13),rgba(12,35,64,.55));
|
||||
border:1px solid rgba(198,161,91,.42);border-radius:14px;padding:16px 18px;
|
||||
display:flex;align-items:center;gap:16px;flex-wrap:wrap">
|
||||
<span style="font-size:13.5px;color:#E3CB99;font-weight:600">✓ 定稿交付已完成</span>
|
||||
<span style="font-size:12.5px;color:var(--mut)">Forte Z + 甄房蓝(主色 #0C2340)|10 个矢量资产 + 36 个位图 + 完整色板规范</span>
|
||||
<a href="logo/index.html" target="_blank" style="margin-left:auto;font-size:12.5px;color:#0C2340;
|
||||
background:linear-gradient(120deg,#E3CB99,#C6A15B);padding:8px 18px;border-radius:9px;
|
||||
text-decoration:none;font-weight:600">打开定稿交付页 →</a>
|
||||
<a href="logo/brush-z.html" target="_blank" style="font-size:12.5px;color:#2a1f06;
|
||||
background:linear-gradient(120deg,#F3D98F,#E0B95C);padding:8px 18px;border-radius:9px;
|
||||
text-decoration:none;font-weight:700">★ 原创笔刷 Z(版权归你)→</a>
|
||||
<a href="logo/ofl-brush.html" target="_blank" style="font-size:12.5px;color:#0a2c19;
|
||||
background:linear-gradient(120deg,#8fe8ad,#4fd07f);padding:8px 18px;border-radius:9px;
|
||||
text-decoration:none;font-weight:600">OFL 可商用字体试排 →</a>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="wrap">
|
||||
|
||||
<h2 class="sec">一、甄房 LOGO 概念方案 · 第一批(线条 / 中式 / 印章 · 8 稿)</h2>
|
||||
<div class="secsub">
|
||||
取名「甄房」:甄,本义为制陶甄别、去伪存真,引申为<b style="color:var(--gold2)">甄选</b>;房即房源。
|
||||
因此所有方案都围绕「房子 + 甄选」两条线索做叠加。每稿都附合体字标(甄房 / ZHENFANG),可直接定为终稿方向。
|
||||
</div>
|
||||
<div class="concepts" id="concepts"></div>
|
||||
|
||||
<h2 class="sec">二、home-04 外框 + Z 字形探索(12 款新字形 + 3 款已留)</h2>
|
||||
<div class="secsub">
|
||||
外框固定不动 = <code>hugeicons:home-04</code>(MIT)原图路径等比放大,形状/比例/线宽全部不动。<b style="color:var(--gold2)">这一节只换里面的 Z</b>:字面统一锁在 21×17.5 的字面框内,各款按笔画宽度自动内缩,所以 12 款的字面大小一致、只有字形不同。<br>
|
||||
<span class="star">★</span> 标记的三款是你已选定保留的(深底反白 / 圆角 Z / 线框 Z),放在最前面方便对照。
|
||||
</div>
|
||||
<div class="concepts" id="concepts-frame"></div>
|
||||
|
||||
<h2 class="sec">三、Z 字母 · 字体精选(${FONTS.total} 款)</h2>
|
||||
<div class="secsub">
|
||||
不再手画字形——直接用<b style="color:var(--gold2)">真字体</b>渲染 Z。外框依旧是 home-04 原图(不动),Z 用各款字体的大写 Z,并<b style="color:var(--gold2)">等比放大到填满同一个字面框(21.5 × 20)</b>——与你保留的三款(Z 约 21 宽 × 19.7 高、竖直中心 38.75)取同一比例,所以不存在「大小不一」的干扰,只看字形本身的宽窄与笔画性格。<br>
|
||||
<span style="color:#5ee08a">●</span> 本地素材库字体(OFL-1.1,<b>可直接商用</b>)
|
||||
<span style="color:#f0a55c">●</span> 你本机已安装的系统字体(<b>仅作预览,商用需自行取得授权</b>)<br>
|
||||
点任意一张卡片即可复制它的 SVG。注意:SVG 里的 Z 是<b>活文字</b>(依赖字体),定稿后我可以把你选中的那一款转成曲线路径。
|
||||
</div>
|
||||
<div class="row" style="margin-bottom:14px">
|
||||
<div class="tabs" id="fontcats"></div>
|
||||
<label class="chk"><input type="checkbox" id="onlyOfl"> 只看可商用(OFL)</label>
|
||||
<span class="cnt" id="fontcnt"></span>
|
||||
</div>
|
||||
<div class="fsgrid" id="fsgrid"></div>
|
||||
|
||||
<h2 class="sec">四、房子类素材挑选(${items.length} 款 · 点击即预览成标)</h2>
|
||||
<div class="secsub">
|
||||
分五层:<b>房屋本体</b>(做主图形首选)、<b>门户构件</b>(屋顶/门窗/钥匙)、<b>楼宇建筑</b>(楼宇/公共建筑/城市)、
|
||||
<b>房产交易</b>(成交/租售/服务)、<b>品牌标志</b>(知名品牌 Logo,仅作参考,有商标风险)。
|
||||
点任意一款 → 右侧实时看它套上「甄房」字标的效果,可换主色、换形状、复制/下载 SVG。
|
||||
</div>
|
||||
|
||||
<div class="picker">
|
||||
<div>
|
||||
<div class="filters">
|
||||
<div class="row">
|
||||
<input type="search" id="q" placeholder="搜索名称 / 来源,如 house、key、villa、lucide…">
|
||||
<label class="chk"><input type="checkbox" id="onlyFav"> 只看已收藏</label>
|
||||
<span class="cnt" id="cnt"></span>
|
||||
</div>
|
||||
<div class="tabs" id="tabs"></div>
|
||||
<div class="row">
|
||||
<select id="src"></select>
|
||||
<select id="ty">
|
||||
<option value="">全部样式</option>
|
||||
<option value="icon">单色(可换主色)</option>
|
||||
<option value="multi">彩色(保留原色)</option>
|
||||
</select>
|
||||
<select id="shape">
|
||||
<option value="squircle">形状:圆角方</option>
|
||||
<option value="round">形状:圆形</option>
|
||||
<option value="square">形状:方形</option>
|
||||
</select>
|
||||
<label class="chk">主色 <input type="color" id="brand" value="#1f6feb"></label>
|
||||
<select id="mode">
|
||||
<option value="light">底:浅色</option>
|
||||
<option value="dark" selected>底:深色</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid" id="grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="detail" id="detail"></div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
素材来源:画个标(DrawLogo)共享图标库 · 按 manifest/icons.json 的 source 定向读取后本地提取,未改动原库任何文件。<br>
|
||||
协议提示:标记 <span style="color:#f0a55c">⚠</span> 的素材为 BY-SA / BY-NC / BY-ND / GPL 等限制性协议,商用前请替换或单独确认;常用 MIT / Apache-2.0 / ISC / CC BY 4.0 可商用(建议署名)。<br>
|
||||
「品牌标志」层为第三方注册商标,仅作造型参考,<b style="color:#f0a55c">不得直接用作甄房 LOGO</b>。<br>
|
||||
第二节外框造型取自 <code>hugeicons:home-04</code>(MIT)原图路径,未改动其形状;框内 Z 字全部为原创几何,可放心用于甄房。<br>
|
||||
生成时间 ${payload.builtAt} · 由 _design-test/extract-house.js + build-page.js 生成
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
window.HOUSE = ${JSON.stringify(payload).replace(/</g, '\\u003c')};
|
||||
</script>
|
||||
<script>
|
||||
(function(){
|
||||
var D = window.HOUSE;
|
||||
var ITEMS = D.items;
|
||||
|
||||
/* ── 图标渲染(复刻画个标判定 + 按真实颜色纠偏,保证深底可见)── */
|
||||
var STROKE_SRC = {'feather':1,'lucide':1,'tabler':1};
|
||||
// 是否真彩色素材(含明显彩度/亮色)→ 保留原色;否则按单色参与换色
|
||||
// 注意:<defs>(mask/渐变定义)里的颜色不代表成品配色,先剥离再判定
|
||||
function isVivid(svg){
|
||||
var s = svg.replace(/<defs[\\s\\S]*?<\\/defs>/g, '');
|
||||
var hex = s.match(/#[0-9a-fA-F]{3,8}/g) || [], i, v, r, g, b, mx, mn;
|
||||
for(i = 0; i < hex.length; i++){
|
||||
v = hex[i].slice(1);
|
||||
if(v.length === 3) v = v[0]+v[0]+v[1]+v[1]+v[2]+v[2];
|
||||
if(v.length >= 6 && /^[0-9a-f]+$/i.test(v.substr(0,6))){
|
||||
r = parseInt(v.substr(0,2),16); g = parseInt(v.substr(2,2),16); b = parseInt(v.substr(4,2),16);
|
||||
mx = Math.max(r,g,b); mn = Math.min(r,g,b);
|
||||
if((mx - mn) > 24 || mx > 96) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isMono(it){ return !isVivid(it.svg); }
|
||||
// 素材内色值平均亮度(<defs> 不计)
|
||||
function avgLum(svg){
|
||||
var s = svg.replace(/<defs[\\s\\S]*?<\\/defs>/g, '');
|
||||
var hex = s.match(/#[0-9a-fA-F]{3,8}/g) || [], tot = 0, n = 0, i, v, r, g, b;
|
||||
for(i = 0; i < hex.length; i++){
|
||||
v = hex[i].slice(1);
|
||||
if(v.length === 3) v = v[0]+v[0]+v[1]+v[1]+v[2]+v[2];
|
||||
if(v.length < 6 || !/^[0-9a-f]+$/i.test(v.substr(0,6))) continue;
|
||||
r = parseInt(v.substr(0,2),16); g = parseInt(v.substr(2,2),16); b = parseInt(v.substr(4,2),16);
|
||||
tot += (0.2126*r + 0.7152*g + 0.0722*b) / 255; n++;
|
||||
}
|
||||
return n ? tot / n : 0.5;
|
||||
}
|
||||
// 彩色素材若主打深色(深色品牌色)→ 需浅色衬底;主打亮色/白色 → 深色卡片底即可
|
||||
function needsChip(it){ return !isMono(it) && avgLum(it.svg) < 0.45; }
|
||||
function isStroke(it){
|
||||
if(!isMono(it)) return false;
|
||||
if(STROKE_SRC[it.s]) return true;
|
||||
var b = it.svg;
|
||||
if(/stroke-width="0"/.test(b)) return false;
|
||||
// 极小 scale 包装(512 系坐标缩到 24,如 selfhst / antd):描边会被同比缩成发丝 → 强制走填充
|
||||
var sc = b.match(/scale\\(([\\d.]+)\\)/);
|
||||
if(sc && parseFloat(sc[1]) < 0.12) return false;
|
||||
// 填充型信号:evenodd 填充规则且元素级无描边(semi-icons 等,误判描边会整图不可见)
|
||||
if(/fill-rule\\s*=\\s*"evenodd"/.test(b) && !/stroke\\s*=\\s*"(?!none")[^"]*"/.test(b)) return false;
|
||||
if(/stroke\\s*=|stroke-linecap\\s*=|stroke-linejoin\\s*=/.test(b)) return true;
|
||||
var g = b.match(/<(?:path|circle|rect|ellipse|line|polyline|polygon)\\b[^>]*>/g);
|
||||
if(g && g.length && !/fill\\s*=\\s*"(?!none")[^"]*"/.test(b)) return true;
|
||||
return false;
|
||||
}
|
||||
// 深色硬编码 + currentColor 统一换成主题色(按亮度判定,覆盖 #0c1221 / #231f20 这类非纯黑深色)
|
||||
function themed(body, color){
|
||||
return body.replace(/currentColor/gi, color).replace(/#[0-9a-fA-F]{3,8}/g, function(h){
|
||||
var v = h.slice(1);
|
||||
if(v.length === 3) v = v[0]+v[0]+v[1]+v[1]+v[2]+v[2];
|
||||
if(v.length < 6 || !/^[0-9a-f]+$/i.test(v.substr(0,6))) return h;
|
||||
var r = parseInt(v.substr(0,2),16), g = parseInt(v.substr(2,2),16), b = parseInt(v.substr(4,2),16);
|
||||
return (0.299*r + 0.587*g + 0.114*b) < 96 ? color : h;
|
||||
});
|
||||
}
|
||||
function iconSvg(it, color, size, w){
|
||||
var mono = isMono(it);
|
||||
if(!mono) return '<svg viewBox="0 0 24 24" width="'+size+'" height="'+size+'" xmlns="http://www.w3.org/2000/svg">'+it.svg+'</svg>';
|
||||
var body = themed(it.svg, color);
|
||||
var attrs = isStroke(it)
|
||||
? 'fill="none" stroke="'+color+'" stroke-width="'+(w||1.9)+'" stroke-linecap="round" stroke-linejoin="round"'
|
||||
: 'fill="'+color+'" stroke="none"';
|
||||
return '<svg viewBox="0 0 24 24" width="'+size+'" height="'+size+'" xmlns="http://www.w3.org/2000/svg" '+attrs+'>'+body+'</svg>';
|
||||
}
|
||||
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];});}
|
||||
|
||||
/* ── 配色 ── */
|
||||
var S = {
|
||||
tier:'all', q:'', src:'', ty:'', onlyFav:false,
|
||||
brand:'#1f6feb', mode:'light', shape:'squircle',
|
||||
active:null, fav:{}
|
||||
};
|
||||
function brandBg(){
|
||||
return S.mode==='dark' ? '#111827' : S.brand;
|
||||
}
|
||||
function multiBg(){
|
||||
return '#f5f2ea'; // 深色作品专用浅色衬底
|
||||
}
|
||||
|
||||
/* ── 方案渲染(两批:线条原创 / 实心几何)── */
|
||||
var G1 = ${JSON.stringify(CONCEPTS).replace(/</g, '\\u003c')};
|
||||
var G2 = ${JSON.stringify(CONCEPTS_FRAME).replace(/</g, '\\u003c')};
|
||||
function conceptSvg(c){
|
||||
if(c.mode === 'solid') return '<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">'+c.svg+'</svg>';
|
||||
var sw = c.mode === 'mixed' ? 3.4 : 3.6;
|
||||
return '<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" '+
|
||||
'stroke-width="'+sw+'" stroke-linecap="round" stroke-linejoin="round">'+c.svg+'</svg>';
|
||||
}
|
||||
function conceptStandalone(c){
|
||||
if(c.mode === 'solid') return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="512" height="512">'+c.svg+'</svg>';
|
||||
var sw = c.mode === 'mixed' ? 3.4 : 3.6;
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="512" height="512" fill="none" '+
|
||||
'stroke="#1f6feb" stroke-width="'+sw+'" stroke-linecap="round" stroke-linejoin="round">'+c.svg+'</svg>';
|
||||
}
|
||||
function renderConcepts(hostId, list){
|
||||
var host = document.getElementById(hostId);
|
||||
host.innerHTML = list.map(function(c){
|
||||
return '<div class="cc'+(c.mode === 'solid' ? ' solid' : '')+'" data-cid="'+c.id+'">'+
|
||||
'<div class="mark">'+conceptSvg(c)+'</div>'+
|
||||
'<div class="body">'+
|
||||
'<div class="nm">'+(c.star ? '<span class="star">'+(c.star > 1 ? '★★' : '★')+'</span>' : '')+esc(c.name)+'<em>'+esc(c.tag)+'</em></div>'+
|
||||
'<div class="lock"><span class="zh">甄房</span><span class="en2">ZHENFANG</span></div>'+
|
||||
'<div class="ds">'+esc(c.desc)+'</div>'+
|
||||
'<div class="acts"><button data-act="dl">下载 SVG</button><button data-act="copy">复制代码</button></div>'+
|
||||
'</div></div>';
|
||||
}).join('');
|
||||
host.addEventListener('click', function(e){
|
||||
var b = e.target.closest('button'); if(!b) return;
|
||||
var c = list.filter(function(x){return x.id === b.closest('.cc').dataset.cid;})[0];
|
||||
if(b.dataset.act === 'dl') download('zhenfang-'+c.id+'.svg', conceptStandalone(c));
|
||||
else copy(conceptStandalone(c), '已复制 '+c.name+' 的 SVG 代码');
|
||||
});
|
||||
}
|
||||
renderConcepts('concepts', G1);
|
||||
renderConcepts('concepts-frame', G2);
|
||||
|
||||
/* ── Z 字母字体样本(真字体渲染 + 统一字高归一化)── */
|
||||
var FG2 = '${FG}', H04P = '${H04}', INKC = '${INK}';
|
||||
var FT = ${JSON.stringify(FONTS.fonts).replace(/</g, '\\u003c')};
|
||||
var FCTS = ${JSON.stringify(FONT_CATS)};
|
||||
var fCat = 'all', onlyOfl = false;
|
||||
|
||||
function famCss(f){ return "'" + String(f.family).replace(/'/g, '') + "', sans-serif"; }
|
||||
function frameTag(){
|
||||
return '<g transform="' + FG2 + '"><path d="' + H04P + '" fill="none" stroke="' + INKC +
|
||||
'" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></g>';
|
||||
}
|
||||
function specimenSvg(f, size, tf){
|
||||
return '<svg viewBox="0 0 64 64" width="' + size + '" height="' + size + '" xmlns="http://www.w3.org/2000/svg">' +
|
||||
frameTag() + '<text x="32" y="47.5" text-anchor="middle" font-family="' + famCss(f) + '" font-size="25" fill="' + INKC + '"' +
|
||||
(tf ? ' transform="' + tf + '"' : '') + '>Z</text></svg>';
|
||||
}
|
||||
|
||||
var fsgrid = document.getElementById('fsgrid');
|
||||
function renderFonts(){
|
||||
var list = FT.filter(function(f){
|
||||
if(onlyOfl && f.kind !== 'ofl') return false;
|
||||
if(fCat !== 'all' && f.cat !== fCat) return false;
|
||||
return true;
|
||||
});
|
||||
fsgrid.innerHTML = list.map(function(f){
|
||||
var badge = f.kind === 'ofl' ? '<span class="fl ofl">OFL · 可商用</span>' : '<span class="fl sys">系统字体 · 需授权</span>';
|
||||
return '<div class="fs" data-s="' + f.slug + '" title="' + esc(f.family) + ' — ' + esc(f.cn) + '">' +
|
||||
'<div class="tile">' + specimenSvg(f, 116) + '</div>' +
|
||||
'<div class="fn">' + esc(f.family) + '</div>' +
|
||||
'<div class="fc">' + esc(f.cn) + '</div>' + badge + '</div>';
|
||||
}).join('');
|
||||
document.getElementById('fontcnt').textContent = '共 ' + list.length + ' 款';
|
||||
fitZ();
|
||||
}
|
||||
// 字形着墨度量:SVG <text> 的 getBBox / 命中测试拿到的都是「行盒」,不是字形,
|
||||
// 只有 canvas measureText 的 actualBoundingBox* 才是真正的着墨范围(对系统字体与 @font-face 都有效)
|
||||
var _mctx = document.createElement('canvas').getContext('2d');
|
||||
function inkMetrics(family, px){
|
||||
_mctx.font = px + 'px "' + String(family).replace(/"/g, '') + '", sans-serif';
|
||||
var m = _mctx.measureText('Z');
|
||||
if(!m || (!m.actualBoundingBoxAscent && !m.actualBoundingBoxDescent)) return null;
|
||||
var asc = m.actualBoundingBoxAscent || 0, desc = m.actualBoundingBoxDescent || 0;
|
||||
var w = (m.actualBoundingBoxLeft || 0) + (m.actualBoundingBoxRight || 0);
|
||||
if(!w) w = m.width;
|
||||
return { asc: asc, desc: desc, w: w, h: asc + desc }; // 25px 测量值 ⇔ viewBox 用户单位(text 的 font-size=25 即 25 用户单位)
|
||||
}
|
||||
var FMAP = {};
|
||||
FT.forEach(function(f){ FMAP[f.slug] = f; });
|
||||
function fitOne(card){
|
||||
var f = FMAP[card.getAttribute('data-s')];
|
||||
var t = card.querySelector('.tile svg text');
|
||||
if(!f || !t) return;
|
||||
var mk = inkMetrics(f.family, 25); // 以 25px 测量,比例与字号无关
|
||||
if(!mk || mk.h < 2) return;
|
||||
// 目标:等比填满 21.5 × 20 的字面框(与保留三款同比例),哪一边先贴死就以哪边为准
|
||||
var k = Math.min(21.5 / mk.w, 20 / mk.h);
|
||||
if(k > 8) k = 8;
|
||||
if(k < 0.1) k = 0.1;
|
||||
// 让字形竖直中心落在 38.75(与保留款一致):反推基线最终位置
|
||||
var baseFinal = 38.75 + (mk.asc - mk.desc) * k / 2;
|
||||
var dy = baseFinal - 47.5;
|
||||
t.setAttribute('transform', 'translate(0 ' + dy + ') translate(32 47.5) scale(' + k + ') translate(-32 -47.5)');
|
||||
card.setAttribute('data-fit', '1');
|
||||
}
|
||||
function fitZ(){
|
||||
var cards = fsgrid.querySelectorAll('.fs');
|
||||
for(var i = 0; i < cards.length; i++) fitOne(cards[i]);
|
||||
}
|
||||
fsgrid.addEventListener('click', function(e){
|
||||
var c = e.target.closest('.fs'); if(!c) return;
|
||||
var f = FT.filter(function(x){ return x.slug === c.dataset.s; })[0];
|
||||
var tf = c.querySelector('text').getAttribute('transform') || '';
|
||||
copy(specimenSvg(f, 512, tf), '已复制 ' + f.family + ' 的 Z(活文字,依赖该字体)');
|
||||
});
|
||||
|
||||
var fcats = document.getElementById('fontcats');
|
||||
fcats.innerHTML = ['all'].concat(FCTS).map(function(c){
|
||||
return '<div class="tab' + (c === 'all' ? ' on' : '') + '" data-k="' + esc(c) + '">' +
|
||||
(c === 'all' ? '全部' : esc(c)) + '</div>';
|
||||
}).join('');
|
||||
fcats.onclick = function(e){
|
||||
var t = e.target.closest('.tab'); if(!t) return;
|
||||
fCat = t.dataset.k;
|
||||
[].forEach.call(fcats.children, function(x){ x.classList.toggle('on', x === t); });
|
||||
renderFonts();
|
||||
};
|
||||
document.getElementById('onlyOfl').onchange = function(e){ onlyOfl = e.target.checked; renderFonts(); };
|
||||
|
||||
renderFonts();
|
||||
function refitAll(){ // 字体异步加载完成后重测(首次可能量到回退字体)
|
||||
var cs = fsgrid.querySelectorAll('.fs');
|
||||
for(var i = 0; i < cs.length; i++) cs[i].removeAttribute('data-fit');
|
||||
fitZ();
|
||||
}
|
||||
if(document.fonts && document.fonts.ready){ document.fonts.ready.then(refitAll); }
|
||||
if(document.fonts && document.fonts.addEventListener){ document.fonts.addEventListener('loadingdone', refitAll); }
|
||||
setTimeout(refitAll, 700); setTimeout(refitAll, 2000);
|
||||
|
||||
/* ── 过滤 ── */
|
||||
function filtered(){
|
||||
var q = S.q.trim().toLowerCase();
|
||||
return ITEMS.filter(function(it){
|
||||
if(S.tier !== 'all' && it.t !== S.tier) return false;
|
||||
if(S.src && it.s !== S.src) return false;
|
||||
if(S.ty === 'icon' && !isMono(it)) return false;
|
||||
if(S.ty === 'multi' && isMono(it)) return false;
|
||||
if(S.onlyFav && !S.fav[it.id]) return false;
|
||||
if(q && (it.n+' '+it.id+' '+it.sn).toLowerCase().indexOf(q) < 0) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/* ── 网格分批渲染 ── */
|
||||
// 坐标自适应:少数素材(slds 等)原始坐标不是 24 系,直接套 24 viewBox 会缩成小点
|
||||
function autofit(root){
|
||||
var svgs = root.querySelectorAll('svg:not([data-fit])');
|
||||
for(var i = 0; i < svgs.length; i++){
|
||||
var svg = svgs[i], bb;
|
||||
svg.setAttribute('data-fit', '1');
|
||||
try{ bb = svg.getBBox(); }catch(e){ continue; }
|
||||
if(!bb || !bb.width || !bb.height) continue;
|
||||
var mx = Math.max(bb.width, bb.height);
|
||||
if(mx >= 12 && mx <= 26) continue;
|
||||
var k = 18 / mx;
|
||||
if(k > 8) k = 8;
|
||||
if(k < 0.4) k = 0.4;
|
||||
var cx = bb.x + bb.width / 2, cy = bb.y + bb.height / 2;
|
||||
var g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
g.setAttribute('transform', 'translate(' + (12 - cx * k) + ' ' + (12 - cy * k) + ') scale(' + k + ')');
|
||||
while(svg.firstChild) g.appendChild(svg.firstChild);
|
||||
svg.appendChild(g);
|
||||
}
|
||||
}
|
||||
var shown = 0, list = [], BATCH = 120;
|
||||
var grid = document.getElementById('grid');
|
||||
function renderGrid(reset){
|
||||
if(reset){ list = filtered(); shown = 0; grid.innerHTML = ''; }
|
||||
var frag = document.createDocumentFragment();
|
||||
var end = Math.min(shown + BATCH, list.length);
|
||||
for(var k = shown; k < end; k++){
|
||||
var it = list[k];
|
||||
var mono = isMono(it);
|
||||
var d = document.createElement('div');
|
||||
d.className = 'ic' + (it.bad ? ' bad' : '') + (S.fav[it.id] ? ' faved' : '') + (S.active && S.active.id === it.id ? ' on' : '');
|
||||
d.dataset.id = it.id;
|
||||
d.title = it.n + ' · ' + it.sn + (it.lic ? ' · ' + it.lic : '');
|
||||
d.innerHTML = '<span class="lic">⚠</span><span class="fav">'+(S.fav[it.id]?'★':'☆')+'</span>'+
|
||||
'<div class="bx'+(needsChip(it)?' multi':'')+'">'+iconSvg(it, '#c3cbd9', mono?40:34, 1.7)+'</div>'+
|
||||
'<div class="lb">'+esc(it.n)+'</div>';
|
||||
frag.appendChild(d);
|
||||
}
|
||||
shown = end;
|
||||
grid.appendChild(frag);
|
||||
autofit(grid);
|
||||
var old = grid.querySelector('.more'); if(old) old.remove();
|
||||
if(shown < list.length){
|
||||
var m = document.createElement('div');
|
||||
m.className = 'more';
|
||||
m.innerHTML = '<button>加载更多(已显示 '+shown+' / '+list.length+')</button>';
|
||||
m.querySelector('button').onclick = function(){ renderGrid(false); };
|
||||
grid.appendChild(m);
|
||||
}
|
||||
if(!list.length){ grid.innerHTML = '<div class="empty">没有匹配的素材,换个关键词或清掉筛选试试。</div>'; }
|
||||
document.getElementById('cnt').textContent = '筛选结果 ' + list.length + ' 款';
|
||||
}
|
||||
|
||||
grid.addEventListener('click', function(e){
|
||||
var card = e.target.closest('.ic'); if(!card) return;
|
||||
var it = ITEMS.filter(function(x){return x.id === card.dataset.id;})[0];
|
||||
if(e.target.classList.contains('fav')){
|
||||
if(S.fav[it.id]) delete S.fav[it.id]; else S.fav[it.id] = it;
|
||||
card.classList.toggle('faved');
|
||||
card.querySelector('.fav').textContent = S.fav[it.id] ? '★' : '☆';
|
||||
if(S.onlyFav) renderGrid(true);
|
||||
renderDetail(); return;
|
||||
}
|
||||
S.active = it;
|
||||
[].forEach.call(grid.querySelectorAll('.ic.on'), function(c){ c.classList.remove('on'); });
|
||||
card.classList.add('on');
|
||||
renderDetail();
|
||||
if(window.innerWidth > 1080) document.getElementById('detail').scrollIntoView({block:'nearest'});
|
||||
});
|
||||
|
||||
/* ── 详情面板 ── */
|
||||
function badge(B, size){
|
||||
var bg = needsChip(B) ? multiBg() : brandBg();
|
||||
var ico = iconSvg(B, '#ffffff', Math.round(size * 0.6), 1.9);
|
||||
return '<div class="p '+S.shape+'" style="width:'+size+'px;height:'+size+'px;background:'+bg+'">'+ico+'</div>';
|
||||
}
|
||||
function lockup(B){
|
||||
var bg = needsChip(B) ? multiBg() : brandBg();
|
||||
return '<div class="lockup">'+
|
||||
'<div class="bd" style="background:'+bg+'">'+iconSvg(B, '#ffffff', 30, 1.9)+'</div>'+
|
||||
'<div class="tw"><div class="zh">甄房</div><div class="en3">ZHENFANG</div></div></div>';
|
||||
}
|
||||
function renderDetail(){
|
||||
var el = document.getElementById('detail');
|
||||
var B = S.active;
|
||||
var nFav = Object.keys(S.fav).length;
|
||||
if(!B){
|
||||
el.innerHTML = '<h3>实时预览<span>'+(nFav?('已收藏 '+nFav):'')+'</span></h3>'+
|
||||
'<div class="ph">← 从左侧点选任意一款房子素材<br>这里会实时合成「甄房」标<br><br>'+
|
||||
'<span style="color:var(--dim);font-size:11px">可换主色 / 形状 / 底色,一键复制或下载 SVG</span></div>'+
|
||||
favBlock();
|
||||
bindFav(el); autofit(el); return;
|
||||
}
|
||||
var multi = !isMono(B);
|
||||
el.innerHTML = '<h3>实时预览<span>'+(nFav?('已收藏 '+nFav):'')+'</span></h3>'+
|
||||
'<div class="prev">'+badge(B, 82)+'</div>'+
|
||||
lockup(B)+
|
||||
'<div class="meta">'+
|
||||
'<div><b>名称</b> '+esc(B.n)+'</div>'+
|
||||
'<div><b>标识</b> '+esc(B.id)+'</div>'+
|
||||
'<div><b>来源</b> '+esc(B.sn)+'('+esc(B.s)+')</div>'+
|
||||
'<div><b>协议</b> <span class="'+(B.bad?'warn':'')+'">'+esc(B.lic||'未标注')+(B.bad?' ⚠ 限制性协议,商用需确认':'')+'</span></div>'+
|
||||
(multi?'<div class="warn">多色素材:保留原色,不随主色变化</div>':'')+
|
||||
'</div>'+
|
||||
'<div class="dacts">'+
|
||||
'<button class="pri" data-a="fav">'+(S.fav[B.id]?'★ 已收藏':'☆ 收藏')+'</button>'+
|
||||
'<button data-a="copy">复制 SVG</button>'+
|
||||
'<button data-a="dl">下载图形</button>'+
|
||||
'<button data-a="dl2">下载带底标</button>'+
|
||||
'</div>'+
|
||||
favBlock();
|
||||
bindFav(el); autofit(el);
|
||||
el.querySelector('[data-a=fav]').onclick = function(){
|
||||
if(S.fav[B.id]) delete S.fav[B.id]; else S.fav[B.id] = B;
|
||||
renderGrid(true); renderDetail();
|
||||
};
|
||||
el.querySelector('[data-a=copy]').onclick = function(){ copy(svgFlat(B), '已复制 '+B.n+' 的 SVG'); };
|
||||
el.querySelector('[data-a=dl]').onclick = function(){ download('zhenfang-icon-'+B.s+'-'+B.n+'.svg', svgFlat(B)); };
|
||||
el.querySelector('[data-a=dl2]').onclick = function(){ download('zhenfang-logo-'+B.s+'-'+B.n+'.svg', svgBadge(B)); };
|
||||
}
|
||||
// 纯图形 SVG(24 坐标,可换色)
|
||||
function svgFlat(B){
|
||||
var multi = !isMono(B);
|
||||
if(multi) return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512" height="512">'+B.svg+'</svg>';
|
||||
var body = themed(B.svg, S.brand);
|
||||
var attrs = isStroke(B)
|
||||
? 'fill="none" stroke="'+S.brand+'" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"'
|
||||
: 'fill="'+S.brand+'" stroke="none"';
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512" height="512" '+attrs+'>'+body+'</svg>';
|
||||
}
|
||||
// 带底标 SVG(512 画布,含底色形状 + 白色图形)
|
||||
function svgBadge(B){
|
||||
var multi = !isMono(B);
|
||||
var bg = needsChip(B) ? multiBg() : brandBg();
|
||||
var bgShape = S.shape === 'round'
|
||||
? '<circle cx="256" cy="256" r="256" fill="'+bg+'"/>'
|
||||
: '<rect x="0" y="0" width="512" height="512" rx="'+(S.shape === 'square' ? 40 : 110)+'" fill="'+bg+'"/>';
|
||||
var ico;
|
||||
if(multi){
|
||||
ico = '<g transform="translate(128 128) scale(10.6667)">'+B.svg+'</g>';
|
||||
} else if(isStroke(B)){
|
||||
ico = '<g transform="translate(128 128) scale(10.6667)" fill="none" stroke="#fff" stroke-width="1.9" '+
|
||||
'stroke-linecap="round" stroke-linejoin="round">'+themed(B.svg, '#fff')+'</g>';
|
||||
} else {
|
||||
ico = '<g transform="translate(128 128) scale(10.6667)" fill="#fff" stroke="none">'+themed(B.svg, '#fff')+'</g>';
|
||||
}
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">'+bgShape+ico+'</svg>';
|
||||
}
|
||||
function favBlock(){
|
||||
var ks = Object.keys(S.fav);
|
||||
if(!ks.length) return '';
|
||||
return '<div class="faved-list">'+ks.map(function(k){
|
||||
var it = S.fav[k];
|
||||
return '<div class="fi" data-id="'+esc(k)+'">'+iconSvg(it,'#c3cbd9',18,1.7)+'<span>'+esc(it.n)+'</span><em>×</em></div>';
|
||||
}).join('')+'</div>';
|
||||
}
|
||||
function bindFav(el){
|
||||
el.querySelectorAll('.faved-list .fi').forEach(function(row){
|
||||
row.querySelector('em').onclick = function(){
|
||||
delete S.fav[row.dataset.id];
|
||||
if(S.active && S.active.id === row.dataset.id) {} // 保留预览
|
||||
renderGrid(true); renderDetail();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/* ── 工具 ── */
|
||||
function toast(m){ var t = document.getElementById('toast'); t.textContent = m; t.classList.add('on');
|
||||
clearTimeout(t._t); t._t = setTimeout(function(){ t.classList.remove('on'); }, 1700); }
|
||||
function copy(text, msg){
|
||||
if(navigator.clipboard && navigator.clipboard.writeText){
|
||||
navigator.clipboard.writeText(text).then(function(){ toast(msg); }, function(){ fallback(text, msg); });
|
||||
} else fallback(text, msg);
|
||||
}
|
||||
function fallback(text, msg){
|
||||
var ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta);
|
||||
ta.select(); try{ document.execCommand('copy'); toast(msg); }catch(e){ toast('复制失败,请手动选择'); }
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
function download(name, text){
|
||||
var b = new Blob([text], {type:'image/svg+xml'});
|
||||
var a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = name;
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||||
setTimeout(function(){ URL.revokeObjectURL(a.href); }, 1500);
|
||||
toast('已下载 ' + name);
|
||||
}
|
||||
|
||||
/* ── 筛选控件 ── */
|
||||
var tabs = document.getElementById('tabs');
|
||||
var countByTier = {all: ITEMS.length};
|
||||
ITEMS.forEach(function(it){ countByTier[it.t] = (countByTier[it.t]||0) + 1; });
|
||||
tabs.innerHTML = [{key:'all',label:'全部'}].concat(D.tiers).map(function(t){
|
||||
var key = t.key || 'all';
|
||||
return '<div class="tab'+(key==='all'?' on':'')+'" data-k="'+key+'">'+esc(t.label)+'<i>'+(countByTier[key]||0)+'</i></div>';
|
||||
}).join('');
|
||||
tabs.onclick = function(e){
|
||||
var t = e.target.closest('.tab'); if(!t) return;
|
||||
S.tier = t.dataset.k;
|
||||
[].forEach.call(tabs.children, function(c){ c.classList.toggle('on', c === t); });
|
||||
renderGrid(true);
|
||||
};
|
||||
document.getElementById('src').innerHTML = '<option value="">全部来源('+D.sourceCount+')</option>' +
|
||||
D.sources.map(function(s){ return '<option value="'+esc(s.id)+'">'+esc(s.name)+' · '+s.count+'</option>'; }).join('');
|
||||
document.getElementById('q').oninput = function(e){ S.q = e.target.value; renderGrid(true); };
|
||||
document.getElementById('src').onchange = function(e){ S.src = e.target.value; renderGrid(true); };
|
||||
document.getElementById('ty').onchange = function(e){ S.ty = e.target.value; renderGrid(true); };
|
||||
document.getElementById('onlyFav').onchange = function(e){ S.onlyFav = e.target.checked; renderGrid(true); };
|
||||
document.getElementById('brand').oninput = function(e){ S.brand = e.target.value; renderGrid(false); renderDetail(); };
|
||||
document.getElementById('mode').onchange = function(e){ S.mode = e.target.value; renderDetail(); };
|
||||
document.getElementById('shape').onchange = function(e){ S.shape = e.target.value; renderDetail(); };
|
||||
|
||||
renderGrid(true);
|
||||
renderDetail();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, 'zhenfang-logo-picker.html'), html);
|
||||
console.log('已生成:', path.join(__dirname, 'zhenfang-logo-picker.html'), (Buffer.byteLength(html) / 1024).toFixed(0) + 'KB');
|
||||
@@ -0,0 +1,11 @@
|
||||
// 生成 element-variables.scss 的校验副本(移除依赖 node_modules 的 element-ui 源码 import),供 sass 单独解析
|
||||
// 用法: node _design-test/check-elvars.js
|
||||
const fs = require('fs');
|
||||
const SRC = 'e:/KF_7/ZhenOS-ZF/ruoyi-ui/src/assets/styles/element-variables.scss';
|
||||
const OUT = '_design-test/zf-check-elvars.scss';
|
||||
let t = fs.readFileSync(SRC, 'utf8');
|
||||
const before = t.length;
|
||||
t = t.replace(/@import\s+"~element-ui[^\n]*\n/, '// (校验副本:已移除 element-ui 源码 import)\n');
|
||||
if (t.length === before) { console.error('未找到 element-ui import,请检查'); process.exit(1); }
|
||||
fs.writeFileSync(OUT, t);
|
||||
console.log('已生成 ' + OUT);
|
||||
@@ -0,0 +1,214 @@
|
||||
// 从「画个标」共享图标库提取房子/房产相关素材(分层 + 精选)
|
||||
// 用法: node _design-test/extract-house.js
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const SRC_DIR = path.join(ROOT, 'js', 'data', 'sources');
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'js', 'data', 'manifest', 'icons.json'), 'utf8'));
|
||||
const metaById = {};
|
||||
(manifest.sources || []).forEach(s => { metaById[s.id] = s; });
|
||||
|
||||
// ── 分层词表(按 name 的 token 匹配,不做子串匹配)────────────────
|
||||
const TIERS = [
|
||||
{
|
||||
key: 'house',
|
||||
label: '房屋本体',
|
||||
desc: '房子 / 住宅 / 房产 —— 做「甄房」主图形最直接的候选',
|
||||
exact: ['house', 'houses', 'home', 'homes', 'household', 'homestead', 'villa', 'mansion', 'cabin',
|
||||
'cottage', 'chalet', 'hut', 'bungalow', 'dwelling', 'residence', 'residential', 'townhouse',
|
||||
'duplex', 'igloo', 'tent', 'houseroom', 'household', 'shelter',
|
||||
'real-estate', 'realestate', 'estate', 'property', 'properties', 'apartment', 'apartments',
|
||||
'condo', 'condominium', 'flat', 'housing', 'houselink', 'housekey', 'house-key']
|
||||
},
|
||||
{
|
||||
key: 'part',
|
||||
label: '门户构件',
|
||||
desc: '屋顶 / 门窗 / 钥匙 / 院落 —— 可做辅助图形或组合符号',
|
||||
exact: ['roof', 'roofing', 'chimney', 'door', 'doors', 'doorway', 'window', 'windows', 'gate', 'gates',
|
||||
'fence', 'fencing', 'stairs', 'staircase', 'stair', 'elevator', 'key', 'keys', 'keyhole', 'keycard',
|
||||
'lock', 'locks', 'padlock', 'garage', 'mailbox', 'balcony', 'porch', 'terrace', 'ladder',
|
||||
'doorknob', 'doorbell', 'gateway', 'hedge', 'garden']
|
||||
},
|
||||
{
|
||||
key: 'building',
|
||||
label: '楼宇建筑',
|
||||
desc: '楼宇 / 公共建筑 / 城市 —— 适合「房产平台」类意象',
|
||||
exact: ['building', 'buildings', 'skyscraper', 'skyscrapers', 'tower', 'towers', 'office', 'hotel',
|
||||
'motel', 'bank', 'store', 'shop', 'shopfront', 'warehouse', 'factory', 'industry', 'industrial',
|
||||
'city', 'town', 'urban', 'architecture', 'architectural', 'landmark', 'monument', 'castle',
|
||||
'church', 'temple', 'mosque', 'school', 'university', 'college', 'hospital', 'library', 'museum',
|
||||
'stadium', 'bridge', 'barn', 'farm', 'silo', 'windmill', 'lighthouse', 'palace', 'fort', 'fortress',
|
||||
'pyramid', 'pagoda', 'gatehouse', 'observatory', 'terminal', 'station', 'airport']
|
||||
},
|
||||
{
|
||||
key: 'deal',
|
||||
label: '房产交易',
|
||||
desc: '买卖 / 租售 / 中介服务 —— 做辅助标识(打勾、成交、钥匙交接)',
|
||||
exact: ['realtor', 'realty', 'mortgage', 'sold', 'for-sale', 'forsale', 'sale', 'sell', 'rent',
|
||||
'rental', 'lease', 'agent', 'broker', 'handshake', 'appraisal', 'valuation', 'house-search',
|
||||
'home-search', 'property-search']
|
||||
},
|
||||
{
|
||||
key: 'brand',
|
||||
label: '品牌标志',
|
||||
desc: '房产 / 家居类知名品牌 Logo(仅作参考,有商标风险,勿直接商用)',
|
||||
exact: ['zillow', 'trulia', 'redfin', 'realtor', 'rightmove', 'airbnb', 'booking', 'trivago',
|
||||
'homeadvisor', 'houzz', 'idealista', 'immobiliare', 'century21', 'remax', 'coldwell',
|
||||
'keller-williams', 'opendoor', 'offerpad', 'homelight', 'ikea', 'wayfair', 'homedepot',
|
||||
'lowes', 'leroy-merlin', 'castorama', 'homify', 'nest', 'smart-home', 'matter', 'zigbee']
|
||||
}
|
||||
];
|
||||
|
||||
const TIER_INDEX = TIERS.map(t => ({ key: t.key, re: new RegExp('^(?:' + t.exact.join('|') + ')$', 'i') }));
|
||||
|
||||
const NEG_RE = /(^|-)(off|offline|alert|error|forbidden|blocked|cancel|remove|delete|minus|slash|no|not|without|broken|warning|question|unavailable|unlock|open|add|plus|edit|download|upload|share|upload|export|import|send|edit|list|grid|check|search|refresh|sync|cog|settings|gear|service|tools?|repair)\b/i;
|
||||
|
||||
// 「home-*」「house-*」这类带前缀的语义噪音(智能家居/家务类,与建筑无关)
|
||||
const NOISE_RE = /^(home|house)-(automation|assistant|assistantd|automation|wifi|pod|speaker|audio|appliance|appliances|theater|theatre|workout|button|sensor|monitor|screen|security|enterprise|assistant|hub|bridge|energy|battery|charging|charging-station|filming|sound|video|camera|lightning|lightning-bolt|thermometer|thermostat|music|movie|content|connect|network|server|storage|data|dns|domain|page|pages|work|study|education|learning|teaching|doctor|hospital|clinic|care|nurse|service|services|device|devices|interface|account|accounts|bank|banking|credit|loan|insurance|tax|budget|invoice|wallet|money|finance|business|office|work|cooking|cook|meal|food|chef|recipe|cleaning|clean|laundry|chores|chore|garden|plant|plants|watering|irrigation|pet|baby|kids|child|elderly|health|fitness|gym|yoga|sleep|snooze|reminder|schedule|calendar|clock|time|timer|alarm|doorbell|doorbell|lock|locker|key|keyboard|link|links|tilt|tilted|indicator|realtime|runtime|runtime)$/i;
|
||||
|
||||
function tierOf(name) {
|
||||
const norm = name.toLowerCase().replace(/_/g, '-');
|
||||
if (NOISE_RE.test(norm)) return null;
|
||||
for (const t of TIER_INDEX) if (t.re.test(norm)) return t.key;
|
||||
const parts = norm.split(/[-_ ]+/).filter(Boolean);
|
||||
for (const p of parts) {
|
||||
for (const t of TIER_INDEX) if (t.re.test(p)) return t.key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const RE_NAME = /"name":"((?:[^"\\]|\\.)*)"/;
|
||||
const RE_SVG = /"svg":"((?:[^"\\]|\\.)*)"/;
|
||||
const RE_ID = /"id":"((?:[^"\\]|\\.)*)"/;
|
||||
const RE_TYPE = /"type":"((?:[^"\\]|\\.)*)"/;
|
||||
const RE_CAT = /"categoryCn":"((?:[^"\\]|\\.)*)"/;
|
||||
|
||||
const files = fs.readdirSync(SRC_DIR).filter(f => f.endsWith('.js') && !f.startsWith('dicebear-'));
|
||||
const raw_list = [];
|
||||
const seenSvg = new Map();
|
||||
|
||||
for (const f of files) {
|
||||
const id = f.replace(/\.js$/, '');
|
||||
const meta = metaById[id] || {};
|
||||
if (meta.workspaces && meta.workspaces.length && !meta.workspaces.includes('logo') && !meta.workspaces.includes('avatar')) continue;
|
||||
const raw = fs.readFileSync(path.join(SRC_DIR, f), 'utf8');
|
||||
const MARK = '{"id":"';
|
||||
let pos = 0;
|
||||
while (true) {
|
||||
const start = raw.indexOf(MARK, pos);
|
||||
if (start === -1) break;
|
||||
let end = raw.indexOf('},{"id":"', start);
|
||||
if (end === -1) end = raw.indexOf(']}', start);
|
||||
if (end === -1) end = raw.length;
|
||||
const objStr = raw.slice(start, end);
|
||||
pos = end;
|
||||
const nm = RE_NAME.exec(objStr);
|
||||
if (!nm) continue;
|
||||
const name = nm[1];
|
||||
const tier = tierOf(name);
|
||||
if (!tier) continue;
|
||||
const sm = RE_SVG.exec(objStr);
|
||||
if (!sm || sm[1].length < 20) continue;
|
||||
const svg = sm[1];
|
||||
if (seenSvg.has(svg)) continue;
|
||||
const im = RE_ID.exec(objStr);
|
||||
seenSvg.set(svg, 1);
|
||||
raw_list.push({
|
||||
id: im ? im[1] : '',
|
||||
name: name,
|
||||
tier: tier,
|
||||
source: id,
|
||||
sourceName: meta.nameCn || meta.name || id,
|
||||
type: (RE_TYPE.exec(objStr) || [, ''])[1] || meta.type || 'icon',
|
||||
license: meta.license || '',
|
||||
categoryCn: (RE_CAT.exec(objStr) || [, ''])[1],
|
||||
svg: svg.replace(/\\"/g, '"').replace(/\\\\/g, '\\')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── 精选:同名图标最多保留 N 个不同来源(保证风格可选、又不爆炸)──────
|
||||
const CORE = new Set(['house', 'home', 'building', 'real-estate', 'estate', 'property', 'apartment',
|
||||
'villa', 'cottage', 'cabin', 'mansion', 'townhouse', 'residence', 'residential', 'housing', 'flat',
|
||||
'condo', 'roof', 'door', 'window', 'gate', 'key', 'keyhole', 'lock', 'garage', 'mailbox', 'fence',
|
||||
'stairs', 'office', 'hotel', 'bank', 'school', 'hospital', 'warehouse', 'factory', 'shop', 'store',
|
||||
'city', 'tower', 'skyscraper', 'castle', 'church', 'landmark', 'architecture']);
|
||||
const MAX_PER_NAME_CORE = 16;
|
||||
const MAX_PER_NAME_OTHER = 4;
|
||||
// 精选硬上限(保证风格多样又不过量)
|
||||
const TIER_CAP = { house: 560, part: 320, building: 360, deal: 90, brand: 170 };
|
||||
const PER_SOURCE_CAP = 52;
|
||||
|
||||
// 常见主流图标集优先级(做 LOGO 时通常最干净的几何线性风格)
|
||||
const PREFERRED = ['lucide', 'lucide-lab', 'tabler', 'phosphor', 'material-symbols', 'material-symbols-light',
|
||||
'heroicons', 'mdi', 'mdi-light', 'remix', 'solar', 'mingcute', 'iconamoon', 'iconoir', 'hugeicons',
|
||||
'carbon', 'fluent', 'basil', 'uil', 'icon-park-outline', 'icon-park', 'feather', 'ionicons', 'octicon',
|
||||
'boxicons', 'bxs', 'bx', 'bootstrap', 'fa7-solid', 'fa7-regular', 'fluent-mdl2', 'antd', 'tdesign',
|
||||
'semi-icons', 'line-md', 'gravity-ui', 'mage', 'clarity', 'jam', 'proicons', 'prime', 'cuida',
|
||||
'pixelarticons', 'lets-icons', 'unjs', 'typicons', 'fontisto', 'foundation', 'devicon', 'streamline',
|
||||
'gis', 'maki', 'gameicons', 'openmoji', 'noto', 'twemoji', 'emojione', 'fluent-emoji',
|
||||
'simple-icons', 'cib', 'bxl', 'fa7-brands', 'logos', 'selfhst', 'svgl'];
|
||||
const PRANK = Object.fromEntries(PREFERRED.map((s, i) => [s, i]));
|
||||
|
||||
const byName = new Map();
|
||||
for (const it of raw_list) {
|
||||
if (NEG_RE.test(it.name)) continue;
|
||||
const k = it.tier + '|' + it.name.toLowerCase();
|
||||
if (!byName.has(k)) byName.set(k, []);
|
||||
byName.get(k).push(it);
|
||||
}
|
||||
|
||||
const staged = [];
|
||||
for (const [k, arr] of byName) {
|
||||
const name = k.split('|')[1];
|
||||
const max = CORE.has(name) ? MAX_PER_NAME_CORE : MAX_PER_NAME_OTHER;
|
||||
// 优先主流图标集 -> 单色线性 -> id 短
|
||||
arr.sort((a, b) => {
|
||||
const ap = PRANK[a.source] === undefined ? 999 : PRANK[a.source];
|
||||
const bp = PRANK[b.source] === undefined ? 999 : PRANK[b.source];
|
||||
if (ap !== bp) return ap - bp;
|
||||
if ((a.type !== 'icon') !== (b.type !== 'icon')) return (a.type !== 'icon') - (b.type !== 'icon');
|
||||
return a.id.length - b.id.length;
|
||||
});
|
||||
staged.push(...arr.slice(0, max));
|
||||
}
|
||||
|
||||
// 排序优先级:核心名精确命中 > 名字短 > 来源稳定
|
||||
staged.sort((a, b) => {
|
||||
const an = a.name.toLowerCase(), bn = b.name.toLowerCase();
|
||||
const ac = CORE.has(an) ? 0 : 1, bc = CORE.has(bn) ? 0 : 1;
|
||||
if (ac !== bc) return ac - bc;
|
||||
if (an.length !== bn.length) return an.length - bn.length;
|
||||
return a.source.localeCompare(b.source);
|
||||
});
|
||||
|
||||
const picked = [];
|
||||
const tierCount = {};
|
||||
const srcCount = {};
|
||||
for (const it of staged) {
|
||||
const cap = TIER_CAP[it.tier] || 200;
|
||||
if ((tierCount[it.tier] || 0) >= cap) continue;
|
||||
if ((srcCount[it.source] || 0) >= PER_SOURCE_CAP) continue;
|
||||
tierCount[it.tier] = (tierCount[it.tier] || 0) + 1;
|
||||
srcCount[it.source] = (srcCount[it.source] || 0) + 1;
|
||||
picked.push(it);
|
||||
}
|
||||
|
||||
// 详情字段瘦身
|
||||
const slim = picked.map(it => ({
|
||||
id: it.id, name: it.name, tier: it.tier, source: it.source, sourceName: it.sourceName,
|
||||
type: it.type, license: it.license, categoryCn: it.categoryCn, svg: it.svg
|
||||
}));
|
||||
|
||||
const stat = {};
|
||||
slim.forEach(i => stat[i.tier] = (stat[i.tier] || 0) + 1);
|
||||
const srcStat = {};
|
||||
slim.forEach(i => srcStat[i.source] = (srcStat[i.source] || 0) + 1);
|
||||
|
||||
const outFile = path.join(__dirname, 'house-icons.json');
|
||||
fs.writeFileSync(outFile, JSON.stringify({ total: slim.length, tiers: TIERS, list: slim }));
|
||||
console.log('原始命中(svg 去重后):', raw_list.length);
|
||||
console.log('精选后:', slim.length, JSON.stringify(stat));
|
||||
console.log('命中源数:', Object.keys(srcStat).length);
|
||||
console.log('输出:', outFile, (fs.statSync(outFile).size / 1024).toFixed(0) + 'KB');
|
||||
@@ -0,0 +1,14 @@
|
||||
// 临时脚本:提取 feather:pen-tool 图标 svg,输出到 _design-test/
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
|
||||
const code = fs.readFileSync('js/data/sources/feather.js', 'utf8');
|
||||
const sb = {};
|
||||
sb.window = sb; sb.self = sb; sb.globalThis = sb;
|
||||
vm.runInNewContext(code, sb, { filename: 'feather.js' });
|
||||
const icons = sb.window['HGB_Source_feather'].icons;
|
||||
const icon = icons.find(i => i.id === 'pen-tool' || i.name === 'pen-tool');
|
||||
if (!icon) { console.error('pen-tool 未找到'); process.exit(1); }
|
||||
console.log('id=' + icon.id + ' name=' + icon.name + ' categoryCn=' + (icon.categoryCn || ''));
|
||||
console.log('svg=' + icon.svg);
|
||||
fs.writeFileSync('_design-test/pen-tool-svg.txt', icon.svg, 'utf8');
|
||||
@@ -0,0 +1,67 @@
|
||||
// 临时脚本:按品牌意象关键词从图标数据文件提取候选(画个标 logo 测试用)
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const OUT = '_design-test';
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync('js/data/manifest/icons.json', 'utf8'));
|
||||
const srcMap = {};
|
||||
manifest.sources.forEach(s => { srcMap[s.id] = s; });
|
||||
|
||||
// 候选源:线条主库 + 实心/多色特色
|
||||
const TARGETS = ['feather', 'lucide', 'tabler', 'bootstrap', 'remix', 'heroicons', 'iconoir', 'phosphor', 'fluent', 'fluent-mdl2'];
|
||||
|
||||
// 品牌意象词表:画个标 = 画(绘) + 标(标志)
|
||||
const THEMES = {
|
||||
design: ['pen', 'pencil', 'palette', 'brush', 'paint', 'edit', 'ruler', 'compass', 'layout', 'draft', 'design', 'sketch', 'ink'],
|
||||
brand: ['flag', 'tag', 'badge', 'bookmark', 'award', 'crown', 'star', 'sparkle', 'label', 'logo', 'target', 'certificate'],
|
||||
symbol: ['hexagon', 'diamond', 'triangle', 'shield', 'anchor', 'gem', 'bolt', 'zap', 'infinity', 'sun', 'moon', 'leaf', 'flower'],
|
||||
tech: ['rocket', 'cpu', 'cloud', 'code', 'terminal', 'wifi', 'database', 'globe'],
|
||||
commerce: ['bag', 'cart', 'coin', 'wallet', 'briefcase', 'chart', 'store', 'gift'],
|
||||
misc: ['key', 'lock', 'mail', 'phone', 'calendar', 'clock', 'home', 'book', 'heart', 'camera', 'map', 'plane']
|
||||
};
|
||||
|
||||
function loadSource(sid) {
|
||||
const file = path.join(ROOT, 'js/data/sources', sid + '.js');
|
||||
if (!fs.existsSync(file)) return null;
|
||||
const code = fs.readFileSync(file, 'utf8');
|
||||
const sb = {};
|
||||
sb.window = sb; sb.self = sb; sb.globalThis = sb;
|
||||
vm.runInNewContext(code, sb, { filename: sid + '.js' });
|
||||
const src = sb.window['HGB_Source_' + sid];
|
||||
return src ? src.icons : null;
|
||||
}
|
||||
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (const sid of TARGETS) {
|
||||
const cfg = srcMap[sid];
|
||||
if (!cfg) continue;
|
||||
const icons = loadSource(sid);
|
||||
if (!icons) { console.log('SKIP(无数据文件): ' + sid); continue; }
|
||||
for (const [theme, kws] of Object.entries(THEMES)) {
|
||||
for (const kw of kws) {
|
||||
let hits = icons.filter(ic => (ic.name || '').toLowerCase().includes(kw) && !seen.has(sid + ':' + ic.id));
|
||||
hits = hits.slice(0, 1); // 每源每词取 1 个,控制候选量
|
||||
for (const h of hits) {
|
||||
seen.add(sid + ':' + h.id);
|
||||
out.push({
|
||||
theme, source: sid, id: h.id, name: h.name,
|
||||
categoryCn: h.categoryCn || '', type: h.type || cfg.type,
|
||||
svg: h.svg, license: cfg.license, licenseNote: cfg.licenseNote,
|
||||
coordText: cfg.coordText, style: cfg.style || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(` 加载 ${sid}(${cfg.count} 图标)→ 命中 ${out.filter(o => o.source === sid).length}`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(OUT, 'candidates.json'), JSON.stringify(out, null, 1), 'utf8');
|
||||
console.log(`\n候选池共 ${out.length} 个图标 → ${OUT}/candidates.json`);
|
||||
const byTheme = {};
|
||||
out.forEach(o => { byTheme[o.theme] = (byTheme[o.theme] || 0) + 1; });
|
||||
console.log('按主题分布:' + JSON.stringify(byTheme));
|
||||
@@ -0,0 +1,79 @@
|
||||
// 临时脚本 v2:意象白名单 + 词边界匹配 + 黑名单过滤,重选候选池
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const OUT = '_design-test';
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync('js/data/manifest/icons.json', 'utf8'));
|
||||
const srcMap = {};
|
||||
manifest.sources.forEach(s => { srcMap[s.id] = s; });
|
||||
|
||||
const TARGETS = ['feather', 'lucide', 'tabler', 'bootstrap', 'remix', 'heroicons', 'iconoir', 'phosphor'];
|
||||
|
||||
// 意象白名单:品牌感强的核心词(拆词匹配,词必须独立出现)
|
||||
const THEMES = {
|
||||
design: ['pencil', 'pen', 'palette', 'brush', 'paint', 'edit', 'ruler', 'compass', 'pen-tool', 'highlighter', 'marker', 'swatch', 'frame', 'crop', 'layers', 'grid', 'layout', 'design', 'sketch', 'ink', 'draft'],
|
||||
brand: ['flag', 'tag', 'badge', 'bookmark', 'award', 'crown', 'star', 'sparkle', 'label', 'target', 'certificate', 'ribbon', 'medal', 'trophy'],
|
||||
symbol: ['hexagon', 'diamond', 'triangle', 'shield', 'anchor', 'gem', 'bolt', 'zap', 'infinity', 'sun', 'moon', 'leaf', 'flower', 'mountain', 'wave', 'snowflake', 'crosshair', 'asterisk'],
|
||||
tech: ['rocket', 'cpu', 'cloud', 'code', 'terminal', 'wifi', 'database', 'globe', 'chip', 'robot', 'server', 'bug'],
|
||||
commerce: ['bag', 'cart', 'coin', 'wallet', 'briefcase', 'chart', 'store', 'gift', 'receipt', 'percent'],
|
||||
misc: ['key', 'lock', 'mail', 'phone', 'calendar', 'clock', 'home', 'book', 'heart', 'camera', 'map', 'plane', 'music', 'mic', 'coffee', 'cup']
|
||||
};
|
||||
// 黑名单:语义负面/错误/禁用类,绝不做 LOGO
|
||||
const BLACK = ['off', 'alert', 'error', 'warning', 'forbidden', 'ban', 'question', 'slash', 'offline', 'disconnected', 'exclamation', 'trash', 'remove', 'delete', 'block', 'stop'];
|
||||
|
||||
function words(name) {
|
||||
return (name || '').toLowerCase().split(/[-_ ]+/).filter(Boolean);
|
||||
}
|
||||
|
||||
function loadSource(sid) {
|
||||
const file = path.join(ROOT, 'js/data/sources', sid + '.js');
|
||||
if (!fs.existsSync(file)) return null;
|
||||
const code = fs.readFileSync(file, 'utf8');
|
||||
const sb = {};
|
||||
sb.window = sb; sb.self = sb; sb.globalThis = sb;
|
||||
vm.runInNewContext(code, sb, { filename: sid + '.js' });
|
||||
const src = sb.window['HGB_Source_' + sid];
|
||||
return src ? src.icons : null;
|
||||
}
|
||||
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (const sid of TARGETS) {
|
||||
const cfg = srcMap[sid];
|
||||
if (!cfg) continue;
|
||||
const icons = loadSource(sid);
|
||||
if (!icons) { console.log('SKIP: ' + sid); continue; }
|
||||
for (const [theme, kws] of Object.entries(THEMES)) {
|
||||
for (const kw of kws) {
|
||||
const kwWords = words(kw);
|
||||
const hits = icons.filter(ic => {
|
||||
const ws = words(ic.name);
|
||||
if (!ws.length) return false;
|
||||
if (ws.some(w => BLACK.includes(w))) return false;
|
||||
// 白名词须完全包含(词集合 ⊇ 白名词集合)
|
||||
return kwWords.every(w => ws.includes(w)) && !seen.has(sid + ':' + ic.id);
|
||||
});
|
||||
const pick = hits.slice(0, 1); // 每源每意象取 1 个,控制量
|
||||
for (const h of pick) {
|
||||
seen.add(sid + ':' + h.id);
|
||||
out.push({
|
||||
theme, source: sid, id: h.id, name: h.name,
|
||||
categoryCn: h.categoryCn || '', type: h.type || cfg.type,
|
||||
svg: h.svg, license: cfg.license, licenseNote: cfg.licenseNote,
|
||||
coordText: cfg.coordText, style: cfg.style || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(` 加载 ${sid} → 命中 ${out.filter(o => o.source === sid).length}`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(OUT, 'candidates2.json'), JSON.stringify(out, null, 1), 'utf8');
|
||||
console.log(`\n候选池 v2 共 ${out.length} 个 → candidates2.json`);
|
||||
const byTheme = {};
|
||||
out.forEach(o => { byTheme[o.theme] = (byTheme[o.theme] || 0) + 1; });
|
||||
console.log('主题分布:' + JSON.stringify(byTheme));
|
||||
@@ -0,0 +1,113 @@
|
||||
// 临时脚本:从候选池选 100 个生成「画个标」logo 设计稿 HTML
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const ROOT = process.cwd();
|
||||
const OUT = '_design-test';
|
||||
|
||||
const cands = JSON.parse(fs.readFileSync(path.join(OUT, 'candidates2.json'), 'utf8'));
|
||||
|
||||
// 主题配额(共 100)
|
||||
const QUOTA = { design: 25, brand: 20, symbol: 18, tech: 12, commerce: 10, misc: 15 };
|
||||
// 源优先级:经典线条在前,大库补充
|
||||
const SRC_ORDER = ['feather', 'lucide', 'heroicons', 'iconoir', 'tabler', 'bootstrap', 'remix', 'phosphor', 'fluent', 'fluent-mdl2'];
|
||||
|
||||
// 按主题分组
|
||||
const byTheme = {};
|
||||
cands.forEach(c => { (byTheme[c.theme] = byTheme[c.theme] || []).push(c); });
|
||||
|
||||
// 跨源轮选:每主题按源优先级轮流取,配额内
|
||||
const picked = [];
|
||||
for (const [theme, n] of Object.entries(QUOTA)) {
|
||||
const pool = byTheme[theme] || [];
|
||||
// 每源分组
|
||||
const perSrc = {};
|
||||
pool.forEach(c => { (perSrc[c.source] = perSrc[c.source] || []).push(c); });
|
||||
const rounds = Math.ceil(n / SRC_ORDER.length);
|
||||
let taken = 0;
|
||||
for (let r = 0; r < rounds && taken < n; r++) {
|
||||
for (const sid of SRC_ORDER) {
|
||||
const arr = perSrc[sid];
|
||||
if (arr && arr.length > r) {
|
||||
picked.push(arr[r]);
|
||||
taken++;
|
||||
if (taken >= n) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 配额未满则从剩余补
|
||||
if (taken < n) {
|
||||
const rest = pool.filter(c => !picked.includes(c));
|
||||
for (const c of rest) { if (taken >= n) break; picked.push(c); taken++; }
|
||||
}
|
||||
}
|
||||
console.log('选中 ' + picked.length + ' 个');
|
||||
const srcStat = {};
|
||||
picked.forEach(c => { srcStat[c.source] = (srcStat[c.source] || 0) + 1; });
|
||||
console.log('来源分布:' + JSON.stringify(srcStat));
|
||||
|
||||
// 品牌配色(12 组,深底白图标)
|
||||
const PALETTES = [
|
||||
['#FF5A5F', '#FF2D55'], ['#FF9500', '#E66A00'], ['#FFD60A', '#E6B800'],
|
||||
['#34C759', '#1F9D4F'], ['#00C7BE', '#009B94'], ['#0A84FF', '#0055D4'],
|
||||
['#5E5CE6', '#3B36C7'], ['#FF375F', '#D40035'], ['#AF52DE', '#8E2BC7'],
|
||||
['#30B0C7', '#1B7F94'], ['#8E8E93', '#48484A'], ['#111111', '#000000']
|
||||
];
|
||||
// 徽章形状轮换:圆 / 圆角方 / 方
|
||||
const SHAPES = ['round', 'squircle', 'square'];
|
||||
|
||||
let cards = '';
|
||||
picked.forEach((c, i) => {
|
||||
const [c1, c2] = PALETTES[i % PALETTES.length];
|
||||
const shape = SHAPES[i % SHAPES.length];
|
||||
const grad = `linear-gradient(135deg, ${c1} 0%, ${c2} 100%)`;
|
||||
const chip = `background:${c1};`;
|
||||
cards += `
|
||||
<div class="card" style="--grad:${grad};">
|
||||
<div class="badge ${shape}" style="background:var(--grad);">
|
||||
<svg viewBox="0 0 24 24" width="56" height="56" fill="none" stroke="#fff" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">${c.svg}</svg>
|
||||
</div>
|
||||
<div class="name">画个标</div>
|
||||
<div class="en">DrawLogo</div>
|
||||
<div class="meta">
|
||||
<span class="tag" style="${chip}">${c.theme}</span>
|
||||
<span class="src" title="${c.source}:${c.id}">${c.source}:${c.id}</span>
|
||||
</div>
|
||||
<div class="lic">${c.license}${c.licenseNote ? ' · ' + c.licenseNote : ''}</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>画个标 · 100 个 LOGO 候选设计稿(外部调取测试)</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: "PingFang SC", "Microsoft YaHei", sans-serif; background: #0e0e12; color: #eee; padding: 32px; }
|
||||
h1 { font-size: 22px; margin-bottom: 4px; }
|
||||
.sub { color: #888; font-size: 13px; margin-bottom: 24px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; }
|
||||
.card { background: #1a1a22; border: 1px solid #2a2a35; border-radius: 14px; padding: 18px 14px 14px; display: flex; flex-direction: column; align-items: center; text-align: center; }
|
||||
.badge { width: 96px; height: 96px; display: flex; align-items: center; justify-content: center; margin-bottom: 12px; }
|
||||
.badge.round { border-radius: 50%; }
|
||||
.badge.squircle { border-radius: 26px; }
|
||||
.badge.square { border-radius: 6px; }
|
||||
.badge svg { display: block; }
|
||||
.name { font-size: 18px; font-weight: 700; letter-spacing: 2px; }
|
||||
.en { font-size: 11px; color: #777; letter-spacing: 3px; margin-top: 2px; }
|
||||
.meta { margin-top: 10px; display: flex; align-items: center; gap: 6px; }
|
||||
.tag { color: #fff; font-size: 10px; padding: 2px 8px; border-radius: 20px; }
|
||||
.src { font-size: 10px; color: #777; }
|
||||
.lic { font-size: 10px; color: #555; margin-top: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>画个标 · 100 个 LOGO 候选设计稿</h1>
|
||||
<div class="sub">数据来源:画个标共享图标库(manifest/icons.json)· 指令四「跨项目调取」测试 · 每稿含来源与协议标注</div>
|
||||
<div class="grid">${cards}</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
fs.writeFileSync(path.join(OUT, '100-logos.html'), html, 'utf8');
|
||||
fs.writeFileSync(path.join(OUT, 'picked.json'), JSON.stringify(picked.map(c => ({ theme: c.theme, source: c.source, id: c.id, name: c.name, license: c.license })), null, 1), 'utf8');
|
||||
console.log('✅ 生成 ' + OUT + '/100-logos.html(' + picked.length + ' 稿)');
|
||||
@@ -0,0 +1,178 @@
|
||||
// 采集可用字体 → _design-test/font-specimens.json
|
||||
// 来源:① 本地素材库 OFL 字体(可商用) ② 本机已安装系统字体(仅预览)
|
||||
// 说明:本机 Node / 浏览器均无外网,Google Fonts 无法拉取,故不含 Web 字体。
|
||||
// 用法: node _design-test/harvest-fonts.js
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
// ── ① 本地素材库字体(js/data/posters/*.js,全部 OFL-1.1,可商用)──────
|
||||
const LOCAL = [
|
||||
{ id: 'league-spartan', family: 'League Spartan', cn: 'League Spartan · 几何无衬线', cat: '几何/无衬线', w: '700' },
|
||||
{ id: 'fontsource-montserrat', family: 'Montserrat', cn: 'Montserrat · 几何无衬线', cat: '几何/无衬线', w: '800' },
|
||||
{ id: 'fontsource-poppins', family: 'Poppins', cn: 'Poppins · 圆几何', cat: '几何/无衬线', w: '700' },
|
||||
{ id: 'fontsource-inter', family: 'Inter', cn: 'Inter · 界面无衬线', cat: '几何/无衬线', w: '700' },
|
||||
{ id: 'fontsource-space-grotesk', family: 'Space Grotesk', cn: 'Space Grotesk · 科技无衬线', cat: '科技/等宽', w: '700' },
|
||||
{ id: 'fontsource-cinzel', family: 'Cinzel', cn: 'Cinzel · 罗马大写碑体', cat: '衬线', w: '700' },
|
||||
{ id: 'fontsource-playfair', family: 'Playfair Display', cn: 'Playfair Display · 高对比衬线', cat: '衬线', w: '700' },
|
||||
{ id: 'fontsource-lora', family: 'Lora', cn: 'Lora · 书卷衬线', cat: '衬线', w: '700' },
|
||||
{ id: 'fontsource-oswald', family: 'Oswald', cn: 'Oswald · 窄体', cat: '窄体/粗黑', w: '500' },
|
||||
{ id: 'fontsource-bebas-neue', family: 'Bebas Neue', cn: 'Bebas Neue · 高瘦大写字', cat: '窄体/粗黑', w: '400' },
|
||||
{ id: 'fontsource-jetbrains-mono', family: 'JetBrains Mono', cn: 'JetBrains Mono · 等宽', cat: '科技/等宽', w: '700' },
|
||||
{ id: 'fontsource-caveat', family: 'Caveat', cn: 'Caveat · 手写', cat: '手写/笔刷', w: '700' }
|
||||
];
|
||||
|
||||
// ── ② 本机已安装字体(浏览器实测存在;仅作预览,商用需自行确认授权)────
|
||||
// [字体名, 分类, 一句话特点]
|
||||
const SYSTEM = [
|
||||
// 几何 / 无衬线
|
||||
['Helvetica', '几何/无衬线', '经典瑞士无衬线,Z 的中性标准'],
|
||||
['Arial', '几何/无衬线', '最通用的无衬线'],
|
||||
['Roboto', '几何/无衬线', 'Google 系统无衬线'],
|
||||
['Segoe UI', '几何/无衬线', 'Windows 系统无衬线'],
|
||||
['Tahoma', '几何/无衬线', '小字清晰的无衬线'],
|
||||
['Verdana', '几何/无衬线', '宽字距、易读'],
|
||||
['Trebuchet MS', '几何/无衬线', '带性格的旧金山体'],
|
||||
['Calibri', '几何/无衬线', '圆角现代无衬线'],
|
||||
['Candara', '几何/无衬线', '优雅的人文无衬线'],
|
||||
['Corbel', '几何/无衬线', '开阔的人文无衬线'],
|
||||
['Century Gothic', '几何/无衬线', '纯几何圆体'],
|
||||
['Gill Sans MT', '几何/无衬线', '英式人文无衬线'],
|
||||
['Franklin Gothic Book', '几何/无衬线', '美式哥特体(细)'],
|
||||
['Franklin Gothic Medium', '几何/无衬线', '美式哥特体(中)'],
|
||||
['Franklin Gothic Demi', '几何/无衬线', '美式哥特体(半粗)'],
|
||||
['Microsoft Sans Serif', '几何/无衬线', 'Windows 经典 UI 体'],
|
||||
['Tw Cen MT', '几何/无衬线', '二十世纪几何体'],
|
||||
['Berlin Sans FB', '几何/无衬线', '有切角性格的展示无衬线'],
|
||||
['Bahnschrift', '几何/无衬线', 'DIN 血统可变窄体'],
|
||||
['Bahnschrift SemiBold', '几何/无衬线', 'DIN 血统(半粗)'],
|
||||
['Arial Narrow', '窄体/粗黑', '压缩无衬线'],
|
||||
['Arial Black', '窄体/粗黑', '超重无衬线'],
|
||||
['Lucida Sans', '几何/无衬线', '宽圆无衬线'],
|
||||
['Lucida Sans Unicode', '几何/无衬线', '宽圆无衬线(大字符集)'],
|
||||
['Microsoft YaHei', '几何/无衬线', '微软雅黑(拉丁部分)'],
|
||||
// 窄体 / 粗黑 / 展示
|
||||
['Impact', '窄体/粗黑', '极致粗窄,Z 一坨重量'],
|
||||
['Haettenschweiler', '窄体/粗黑', '超窄粗体'],
|
||||
['Britannic Bold', '窄体/粗黑', '英式粗展示体'],
|
||||
['Agency FB', '窄体/粗黑', '宽方现代展示体'],
|
||||
['Wide Latin', '窄体/粗黑', '超宽衬线大字'],
|
||||
['Goudy Stout', '窄体/粗黑', '极重衬线展示体'],
|
||||
// 衬线
|
||||
['Times New Roman', '衬线', '最经典衬线'],
|
||||
['Georgia', '衬线', '屏幕衬线,笔画厚重'],
|
||||
['Garamond', '衬线', '法式古典衬线'],
|
||||
['Book Antiqua', '衬线', 'Palatino 血统'],
|
||||
['Bookman Old Style', '衬线', '厚重书本衬线'],
|
||||
['Cambria', '衬线', '屏幕阅读衬线'],
|
||||
['Constantia', '衬线', '人文衬线'],
|
||||
['Palatino Linotype', '衬线', '人文主义衬线'],
|
||||
['Century Schoolbook', '衬线', '教科书衬线'],
|
||||
['Century', '衬线', '古典几何衬线'],
|
||||
['Calisto MT', '衬线', '过渡期衬线'],
|
||||
['Bodoni MT', '衬线', '高对比 Didone,Z 斜画极细'],
|
||||
['Elephant', '衬线', '超重衬线'],
|
||||
['Engravers MT', '衬线', '刻版罗马大写'],
|
||||
['Goudy Old Style', '衬线', '老式衬线'],
|
||||
['High Tower Text', '衬线', '古籍感衬线'],
|
||||
['Modern No. 20', '衬线', 'Didone 复古'],
|
||||
['Perpetua', '衬线', ' Gill 的衬线体'],
|
||||
['Perpetua Titling MT', '衬线', '大写标题衬线'],
|
||||
['Poor Richard', '衬线', '美式老式衬线'],
|
||||
['Castellar', '衬线', '碑体大写(带衬脚)'],
|
||||
['Colonna MT', '衬线', '大写碑体'],
|
||||
['Imprint MT Shadow', '衬线', '带阴影的碑体'],
|
||||
['Niagara Engraved', '衬线', '镂空雕刻体'],
|
||||
['Sylfaen', '衬线', '乔治亚衬线'],
|
||||
['MS Gothic', '衬线', '日式黑体(拉丁窄)'],
|
||||
['Yu Gothic', '几何/无衬线', '日式无衬线'],
|
||||
// slab / 方正
|
||||
['Rockwell', 'slab/方正', '几何板衬线,Z 方脚'],
|
||||
['Rockwell Extra Bold', 'slab/方正', '超重板衬线'],
|
||||
['Cooper Black', 'slab/方正', '圆胖粗衬线'],
|
||||
// 装饰 / ArtDeco / 趣味
|
||||
['Broadway', '装饰/趣味', 'ArtDeco 舞台体'],
|
||||
['Showcard Gothic', '装饰/趣味', '手绘招牌体'],
|
||||
['Stencil', '装饰/趣味', '模板喷字'],
|
||||
['Onyx', '装饰/趣味', '超窄高对比'],
|
||||
['Playbill', '装饰/趣味', '西部木牌体'],
|
||||
['Algerian', '装饰/趣味', '异域装饰体'],
|
||||
['Snap ITC', '装饰/趣味', '弹跳趣味体'],
|
||||
['Juice ITC', '装饰/趣味', '手绘卡通体'],
|
||||
['Magneto', '装饰/趣味', 'ArtDeco 霓虹体'],
|
||||
['Jokerman', '装饰/趣味', '花哨趣味体'],
|
||||
['Curlz MT', '装饰/趣味', '卷曲花体'],
|
||||
['Chiller', '装饰/趣味', '惊悚手写体'],
|
||||
['Pristina', '装饰/趣味', '细笔装饰体'],
|
||||
['Harrington', '装饰/趣味', 'ArtDeco 装饰体'],
|
||||
['Forte', '装饰/趣味', '笔刷装饰体'],
|
||||
['Gigi', '装饰/趣味', '少女感装饰体'],
|
||||
['Maiandra GD', '装饰/趣味', '圆角人文展示体'],
|
||||
['Tempus Sans ITC', '装饰/趣味', '轻快展示体'],
|
||||
['Papyrus', '装饰/趣味', '莎草纸手写体'],
|
||||
['Old English Text MT', '装饰/趣味', '哥特黑体'],
|
||||
['Viner Hand ITC', '手写/笔刷', '硬笔手写'],
|
||||
// 手写 / 笔刷
|
||||
['Brush Script MT', '手写/笔刷', '经典笔刷体'],
|
||||
['Segoe Script', '手写/笔刷', '系统手写体'],
|
||||
['Segoe Print', '手写/笔刷', '系统手写印刷体'],
|
||||
['Freestyle Script', '手写/笔刷', '随性笔刷'],
|
||||
['French Script MT', '手写/笔刷', '法式连笔'],
|
||||
['Edwardian Script ITC', '手写/笔刷', '爱德华花体'],
|
||||
['Palace Script MT', '手写/笔刷', '宫廷花体'],
|
||||
['Script MT Bold', '手写/笔刷', '粗花体'],
|
||||
['Vladimir Script', '手写/笔刷', '自由花体'],
|
||||
['Bradley Hand ITC', '手写/笔刷', '手写便签体'],
|
||||
['Kristen ITC', '手写/笔刷', '儿童手写体'],
|
||||
['Lucida Handwriting', '手写/笔刷', '手写连笔'],
|
||||
['Kunstler Script', '手写/笔刷', '德式装饰花体'],
|
||||
['Ink Free', '手写/笔刷', '钢笔随手写'],
|
||||
['Parchment', '手写/笔刷', '羊皮纸手写'],
|
||||
['Vivaldi', '手写/笔刷', '斜体花体'],
|
||||
['Comic Sans MS', '手写/笔刷', '手绘马克笔'],
|
||||
// 等宽 / 科技
|
||||
['Consolas', '科技/等宽', '编程等宽'],
|
||||
['Cascadia Code', '科技/等宽', '微软编程等宽'],
|
||||
['Cascadia Mono', '科技/等宽', '微软等宽'],
|
||||
['Lucida Console', '科技/等宽', '经典等宽'],
|
||||
['Courier New', '科技/等宽', '打字机等宽']
|
||||
];
|
||||
|
||||
function pickLocalFile(files, want) {
|
||||
const has = (re) => files.find(f => re.test(f));
|
||||
return has(new RegExp('-latin-' + want + '-normal\\.woff2$'))
|
||||
|| has(/-latin-400-normal\.woff2$/)
|
||||
|| has(/-latin-\d+-normal\.woff2$/)
|
||||
|| has(/-latin-.*normal\.woff2$/)
|
||||
|| files.find(function (x) { return /\.woff2$/.test(x); }) || files[0];
|
||||
}
|
||||
|
||||
const out = [];
|
||||
let missing = 0;
|
||||
for (const f of LOCAL) {
|
||||
const p = path.join(ROOT, 'js', 'data', 'posters', f.id + '.js');
|
||||
if (!fs.existsSync(p)) { console.log(' ✗ 缺源文件', f.id); missing++; continue; }
|
||||
const t = fs.readFileSync(p, 'utf8');
|
||||
const files = [...t.matchAll(/"file":"posters\/[^"]+\/([^"]+\.(?:woff2|woff|ttf|otf))"/g)].map(m => m[1]);
|
||||
const latin = files.filter(x => /-latin-/.test(x));
|
||||
const pick = pickLocalFile(latin.length ? latin : files, f.w);
|
||||
if (!pick) { console.log(' ✗ 无 woff2', f.id); missing++; continue; }
|
||||
out.push({
|
||||
slug: f.id, family: f.family, cn: f.cn, cat: f.cat, weight: f.w,
|
||||
kind: 'ofl', license: 'OFL-1.1 · 可商用',
|
||||
fmt: (pick.split('.').pop() || 'woff2').toLowerCase(),
|
||||
src: '../js/data/posters/' + f.id + '/files/' + pick
|
||||
});
|
||||
console.log(' ✓', f.family, '->', pick);
|
||||
}
|
||||
for (const [family, cat, cn] of SYSTEM) {
|
||||
out.push({ slug: 'sys-' + family.toLowerCase().replace(/[^a-z0-9]+/g, '-'), family, cn, cat, weight: '', kind: 'system', license: '系统字体 · 商用需自行授权', src: '' });
|
||||
}
|
||||
console.log('本地 OFL 字体:', LOCAL.length - missing, '| 系统字体:', SYSTEM.length);
|
||||
|
||||
const cats = {};
|
||||
out.forEach(f => { cats[f.cat] = (cats[f.cat] || 0) + 1; });
|
||||
fs.writeFileSync(path.join(__dirname, 'font-specimens.json'),
|
||||
JSON.stringify({ total: out.length, cats, fonts: out }, null, 0));
|
||||
console.log('合计', out.length, '款 ·', JSON.stringify(cats));
|
||||
@@ -0,0 +1,62 @@
|
||||
# 甄房知识图谱健康检查(只读)
|
||||
# 检查项:孤立实体 / 悬空关系 / 前缀合规(不得出现 XiaoHu:)
|
||||
# 用法: python _design-test/kg-health.py [关键词]
|
||||
import os, re, sys, pymysql
|
||||
|
||||
ZF = r'e:\KF_7\ZhenOS-ZF'
|
||||
env = {}
|
||||
for ln in open(os.path.join(ZF, '.env.local'), encoding='utf-8'):
|
||||
m = re.match(r'^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$', ln)
|
||||
if m:
|
||||
env[m.group(1)] = m.group(2).strip().strip('"').strip("'")
|
||||
|
||||
conn = pymysql.connect(host='rm-0jlbgr2rv6dj3t6jngo.mysql.rds.aliyuncs.com', port=3306,
|
||||
user=env['DB_USERNAME'], password=env['DB_PASSWORD'],
|
||||
charset='utf8mb4', connect_timeout=12)
|
||||
cur = conn.cursor()
|
||||
cur.execute('USE zhenos_kg')
|
||||
|
||||
def one(sql):
|
||||
cur.execute(sql); return cur.fetchone()[0]
|
||||
|
||||
print('=== 甄房知识图谱健康检查 ===')
|
||||
print(' 实体总数 ', one('SELECT COUNT(*) FROM kg_entities'))
|
||||
print(' 关系总数 ', one('SELECT COUNT(*) FROM kg_relations'))
|
||||
print(' 孤立实体(无任何关系)', one(
|
||||
'SELECT COUNT(*) FROM kg_entities e WHERE NOT EXISTS('
|
||||
'SELECT 1 FROM kg_relations r WHERE r.from_entity=e.entity_name OR r.to_entity=e.entity_name)'))
|
||||
print(' 悬空关系 from ', one(
|
||||
'SELECT COUNT(*) FROM kg_relations WHERE from_entity NOT IN (SELECT entity_name FROM kg_entities)'))
|
||||
print(' 悬空关系 to ', one(
|
||||
'SELECT COUNT(*) FROM kg_relations WHERE to_entity NOT IN (SELECT entity_name FROM kg_entities)'))
|
||||
print(' 非 ZhenosSH: 前缀实体', one(
|
||||
"SELECT COUNT(*) FROM kg_entities WHERE entity_name NOT LIKE 'ZhenosSH:%'"))
|
||||
print(" 旧前缀 XiaoHu: ", one("SELECT COUNT(*) FROM kg_entities WHERE entity_name LIKE 'XiaoHu:%'"))
|
||||
print(' dev_knowledge 条数 ', one('SELECT COUNT(*) FROM dev_knowledge'))
|
||||
print(' 其中 source=AI_SYNC ', one("SELECT COUNT(*) FROM dev_knowledge WHERE source='AI_SYNC'"))
|
||||
print(' 实体中 AI_SYNC ', one("SELECT COUNT(*) FROM kg_entities WHERE source='AI_SYNC'"))
|
||||
print(' 关系中 AI_SYNC 模块 ', one("SELECT COUNT(*) FROM kg_relations WHERE module IN ('ruoyi-ui','zhenfang-uni')"))
|
||||
|
||||
cur.execute("SELECT entity_type, entity_name FROM kg_entities WHERE source='AI_SYNC' ORDER BY entity_type, entity_name")
|
||||
_rows = cur.fetchall()
|
||||
if _rows:
|
||||
print('\n source=AI_SYNC 的新增实体(%d 个):' % len(_rows))
|
||||
for t, n in _rows:
|
||||
print(' %-12s %s' % (t, n))
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
kw = sys.argv[1]
|
||||
cur.execute("SELECT entity_type, entity_name FROM kg_entities WHERE entity_name LIKE %s ORDER BY entity_type",
|
||||
('%' + kw + '%',))
|
||||
rows = cur.fetchall()
|
||||
print('\n 含「%s」的实体 %d 个:' % (kw, len(rows)))
|
||||
for t, n in rows:
|
||||
print(' %-14s %s' % (t, n))
|
||||
cur.execute("SELECT type, title FROM dev_knowledge WHERE title LIKE %s OR tags LIKE %s",
|
||||
('%' + kw + '%', '%' + kw + '%'))
|
||||
rows = cur.fetchall()
|
||||
print(' 含「%s」的知识条目 %d 条:' % (kw, len(rows)))
|
||||
for t, n in rows:
|
||||
print(' %-12s %s' % (t, n))
|
||||
|
||||
conn.close()
|
||||
@@ -0,0 +1,28 @@
|
||||
# 查看 zhenos_kg 中某实体的完整字段(只读)
|
||||
# 用法: python _design-test/kg-inspect.py "ZhenosSH:zhenfang-uni"
|
||||
import os, re, sys, pymysql
|
||||
|
||||
ZF = r'e:\KF_7\ZhenOS-ZF'
|
||||
env = {}
|
||||
for ln in open(os.path.join(ZF, '.env.local'), encoding='utf-8'):
|
||||
m = re.match(r'^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$', ln)
|
||||
if m:
|
||||
env[m.group(1)] = m.group(2).strip().strip('"').strip("'")
|
||||
|
||||
name = sys.argv[1] if len(sys.argv) > 1 else ''
|
||||
conn = pymysql.connect(host='rm-0jlbgr2rv6dj3t6jngo.mysql.rds.aliyuncs.com', port=3306,
|
||||
user=env['DB_USERNAME'], password=env['DB_PASSWORD'],
|
||||
charset='utf8mb4', connect_timeout=12)
|
||||
cur = conn.cursor()
|
||||
cur.execute('USE zhenos_kg')
|
||||
cur.execute('SHOW COLUMNS FROM kg_entities')
|
||||
cols = [c[0] for c in cur.fetchall()]
|
||||
cur.execute('SELECT * FROM kg_entities WHERE entity_name=%s', (name,))
|
||||
rows = cur.fetchall()
|
||||
if not rows:
|
||||
print('未找到实体:', name)
|
||||
for r in rows:
|
||||
print('=' * 60)
|
||||
for c, v in zip(cols, r):
|
||||
print('%s: %s' % (c, v))
|
||||
conn.close()
|
||||
@@ -0,0 +1,64 @@
|
||||
# 只读探测甄房知识图谱库(zhenos_kg)——不做任何写入
|
||||
# 用法: python _design-test/kg-probe.py
|
||||
import os, re, sys, pymysql
|
||||
|
||||
ZF = r'e:\KF_7\ZhenOS-ZF'
|
||||
env = {}
|
||||
with open(os.path.join(ZF, '.env.local'), encoding='utf-8') as f:
|
||||
for ln in f:
|
||||
m = re.match(r'^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$', ln)
|
||||
if m:
|
||||
env[m.group(1)] = m.group(2).strip().strip('"').strip("'")
|
||||
|
||||
user = env.get('DB_USERNAME')
|
||||
pwd = env.get('DB_PASSWORD')
|
||||
if not user or not pwd:
|
||||
sys.exit('未在 .env.local 取到 DB_USERNAME / DB_PASSWORD')
|
||||
|
||||
HOST = 'rm-0jlbgr2rv6dj3t6jngo.mysql.rds.aliyuncs.com'
|
||||
PORT = 3306
|
||||
|
||||
try:
|
||||
conn = pymysql.connect(host=HOST, port=PORT, user=user, password=pwd, charset='utf8mb4',
|
||||
connect_timeout=12, read_timeout=30)
|
||||
except Exception as e:
|
||||
sys.exit('连接失败(可能是 RDS IP 白名单未放行本机):%s' % e)
|
||||
|
||||
cur = conn.cursor()
|
||||
cur.execute("SHOW DATABASES")
|
||||
dbs = [r[0] for r in cur.fetchall()]
|
||||
print('可见数据库:', ', '.join(dbs))
|
||||
print('zhenos_kg 存在:', 'zhenos_kg' in dbs)
|
||||
if 'zhenos_kg' not in dbs:
|
||||
conn.close(); sys.exit(0)
|
||||
|
||||
cur.execute("USE zhenos_kg")
|
||||
cur.execute("SHOW TABLES")
|
||||
tabs = [r[0] for r in cur.fetchall()]
|
||||
print('表:', ', '.join(tabs))
|
||||
for t in ('kg_entities', 'kg_relations', 'dev_knowledge'):
|
||||
if t in tabs:
|
||||
cur.execute("SELECT COUNT(*) FROM `%s`" % t)
|
||||
print(' %-16s %d 行' % (t, cur.fetchone()[0]))
|
||||
|
||||
if 'kg_entities' in tabs:
|
||||
cur.execute("SELECT entity_type, COUNT(*) FROM kg_entities GROUP BY entity_type ORDER BY 2 DESC")
|
||||
print('实体类型分布:', ', '.join('%s=%d' % r for r in cur.fetchall()))
|
||||
cur.execute("SELECT COUNT(*) FROM kg_entities WHERE entity_name LIKE 'ZhenosSH:%%'")
|
||||
print("前缀 ZhenosSH: 实体数:", cur.fetchone()[0])
|
||||
cur.execute("SELECT COUNT(*) FROM kg_entities WHERE entity_name LIKE 'XiaoHu:%%'")
|
||||
print("前缀 XiaoHu: 实体数(应为 0):", cur.fetchone()[0])
|
||||
cur.execute("SELECT entity_name FROM kg_entities WHERE entity_name LIKE '%%ui%%' OR entity_name LIKE '%%样式%%' OR entity_name LIKE '%%主题%%' LIMIT 15")
|
||||
rows = cur.fetchall()
|
||||
print('与 UI/样式/主题 相关的已有实体:')
|
||||
for r in rows:
|
||||
print(' ', r[0])
|
||||
if not rows:
|
||||
print(' (无)')
|
||||
|
||||
if 'dev_knowledge' in tabs:
|
||||
cur.execute("SHOW COLUMNS FROM dev_knowledge")
|
||||
print('dev_knowledge 列:', ', '.join(c[0] for c in cur.fetchall()))
|
||||
|
||||
conn.close()
|
||||
print('\n只读探测完成,未做任何写入。')
|
||||
@@ -0,0 +1,60 @@
|
||||
# 读取 zhenos_kg 的三张表结构 + 既有样本,供生成符合约定的同步 SQL
|
||||
# 只读,不写入
|
||||
# 用法: python _design-test/kg-schema.py
|
||||
import os, re, pymysql
|
||||
|
||||
ZF = r'e:\KF_7\ZhenOS-ZF'
|
||||
env = {}
|
||||
with open(os.path.join(ZF, '.env.local'), encoding='utf-8') as f:
|
||||
for ln in f:
|
||||
m = re.match(r'^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$', ln)
|
||||
if m:
|
||||
env[m.group(1)] = m.group(2).strip().strip('"').strip("'")
|
||||
|
||||
conn = pymysql.connect(host='rm-0jlbgr2rv6dj3t6jngo.mysql.rds.aliyuncs.com', port=3306,
|
||||
user=env['DB_USERNAME'], password=env['DB_PASSWORD'],
|
||||
charset='utf8mb4', connect_timeout=12)
|
||||
cur = conn.cursor()
|
||||
cur.execute("USE zhenos_kg")
|
||||
|
||||
for t in ('kg_entities', 'kg_relations', 'dev_knowledge'):
|
||||
cur.execute("SHOW CREATE TABLE `%s`" % t)
|
||||
print('=' * 70)
|
||||
print(cur.fetchone()[1])
|
||||
|
||||
cur.execute("SELECT * FROM kg_entities WHERE entity_name='ZhenosSH:ruoyi-ui'")
|
||||
cur.execute("SHOW COLUMNS FROM kg_entities")
|
||||
cols = [c[0] for c in cur.fetchall()]
|
||||
cur.execute("SELECT * FROM kg_entities WHERE entity_name='ZhenosSH:ruoyi-ui'")
|
||||
row = cur.fetchone()
|
||||
print('=' * 70)
|
||||
print('样本实体 ZhenosSH:ruoyi-ui:')
|
||||
for c, v in zip(cols, row):
|
||||
s = str(v)
|
||||
print(' %-20s %s' % (c, s[:300]))
|
||||
|
||||
cur.execute("SELECT * FROM kg_entities WHERE entity_type='ConfigFile' LIMIT 2")
|
||||
print('-' * 70)
|
||||
for r in cur.fetchall():
|
||||
print(' ConfigFile 样本:', dict(zip(cols, [str(x)[:80] for x in r])))
|
||||
|
||||
cur.execute("SHOW COLUMNS FROM kg_relations")
|
||||
rcols = [c[0] for c in cur.fetchall()]
|
||||
cur.execute("SELECT * FROM kg_relations WHERE source_name LIKE 'ZhenosSH:ruoyi-ui%%' LIMIT 4")
|
||||
print('-' * 70)
|
||||
print('ruoyi-ui 的关系样本:')
|
||||
for r in cur.fetchall():
|
||||
print(' ', dict(zip(rcols, [str(x)[:60] for x in r])))
|
||||
|
||||
cur.execute("SHOW COLUMNS FROM dev_knowledge")
|
||||
kcols = [c[0] for c in cur.fetchall()]
|
||||
cur.execute("SELECT type,title,tags,entity_type,entity_name,module,priority,status,source FROM dev_knowledge WHERE type='pitfall' LIMIT 2")
|
||||
print('-' * 70)
|
||||
print('dev_knowledge pitfall 样本:')
|
||||
for r in cur.fetchall():
|
||||
print(' ', dict(zip(['type','title','tags','entity_type','entity_name','module','priority','status','source'],
|
||||
[str(x)[:90] for x in r])))
|
||||
cur.execute("SELECT DISTINCT type FROM dev_knowledge")
|
||||
print(' dev_knowledge 已有 type:', [r[0] for r in cur.fetchall()])
|
||||
|
||||
conn.close()
|
||||
@@ -0,0 +1,38 @@
|
||||
# 把 H5 tabbar 的「选中态」图标从旧品牌橙重着色为甄房交互蓝
|
||||
# 图标为单色 + alpha 通道,按 alpha 保留、RGB 整体替换即可无损改色
|
||||
# 用法: python _design-test/recolor-tabbar.py (预演)
|
||||
# python _design-test/recolor-tabbar.py --write (真写)
|
||||
import os, sys, collections
|
||||
from PIL import Image
|
||||
|
||||
D = r'e:\KF_7\ZhenOS-ZF\zhenfang-uni\src\static\tabbar'
|
||||
TARGET = (0x00, 0x70, 0xF2) # #0070F2 甄房交互蓝
|
||||
WRITE = '--write' in sys.argv
|
||||
|
||||
def dominant(im):
|
||||
c = collections.Counter()
|
||||
for px in im.getdata():
|
||||
if px[3] > 200:
|
||||
c[px[:3]] += 1
|
||||
return c.most_common(1)[0][0] if c else None
|
||||
|
||||
for f in sorted(os.listdir(D)):
|
||||
if not f.endswith('.png') or '_blue' not in f:
|
||||
continue
|
||||
p = os.path.join(D, f)
|
||||
im = Image.open(p).convert('RGBA')
|
||||
before = dominant(im)
|
||||
if before == TARGET:
|
||||
print(' %-22s 已是目标色,跳过' % f)
|
||||
continue
|
||||
px = im.load()
|
||||
for y in range(im.height):
|
||||
for x in range(im.width):
|
||||
r, g, b, a = px[x, y]
|
||||
px[x, y] = (TARGET[0], TARGET[1], TARGET[2], a)
|
||||
after = dominant(im)
|
||||
print(' %-22s #%02X%02X%02X -> #%02X%02X%02X' % (f, *before, *after))
|
||||
if WRITE:
|
||||
im.save(p)
|
||||
|
||||
print('\n' + ('已写入' if WRITE else '预演,未写入(加 --write 生效)'))
|
||||
@@ -0,0 +1,65 @@
|
||||
// 扫描 ZF/ruoyi-ui 中残留的小虎租房旧色值,输出「色值 -> 出现次数 / 文件数」清单
|
||||
// 仅统计,不修改任何文件
|
||||
// 用法: node _design-test/scan-old-colors.js
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = 'e:/KF_7/ZhenOS-ZF/ruoyi-ui/src';
|
||||
const EXT = new Set(['.vue', '.scss', '.css', '.js', '.ts']);
|
||||
|
||||
// 小虎租房旧色板(品牌橙 + 暖中性 + 语义 + 业务梯度)
|
||||
const CAND = new Set([
|
||||
'#ff7a1a', '#e86a0c', '#d96408', '#ff8a3d', '#ffc48a', '#ffe8d1', '#fff6ec', '#f6ead8',
|
||||
'#faf7f2', '#e9d3b0', '#efe7dc', '#8a7f73', '#c9beb0', '#2a2118', '#3a322a', '#756a5e',
|
||||
'#9a8e80', '#b8ac9e', '#2f2820', '#4a4036', '#ffb31a', '#fff6e0', '#d9481c', '#fdeee8',
|
||||
'#2e9e5b', '#ecf7f0', '#f08060', '#fbbf24', '#1f8a5b', '#fbf5ea', '#d3dce6', '#99a9bf'
|
||||
]);
|
||||
|
||||
const stats = new Map(); // hex -> { n, files:Set }
|
||||
const rgbStats = new Map();
|
||||
|
||||
function walk(dir) {
|
||||
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = path.join(dir, e.name);
|
||||
if (e.isDirectory()) { walk(p); continue; }
|
||||
if (!EXT.has(path.extname(e.name).toLowerCase())) continue;
|
||||
const txt = fs.readFileSync(p, 'utf8');
|
||||
const rel = path.relative(ROOT, p).replace(/\\/g, '/');
|
||||
// 十六进制
|
||||
const re = /#([0-9a-fA-F]{6})\b/g;
|
||||
let m;
|
||||
while ((m = re.exec(txt))) {
|
||||
const hex = '#' + m[1].toLowerCase();
|
||||
if (!CAND.has(hex)) continue;
|
||||
if (!stats.has(hex)) stats.set(hex, { n: 0, files: new Set() });
|
||||
const s = stats.get(hex); s.n++; s.files.add(rel);
|
||||
}
|
||||
// rgb()/rgba() 三元组
|
||||
const re2 = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g;
|
||||
while ((m = re2.exec(txt))) {
|
||||
const key = [m[1], m[2], m[3]].join(',');
|
||||
const hex = '#' + [m[1], m[2], m[3]].map(v => (+v).toString(16).padStart(2, '0')).join('');
|
||||
if (!CAND.has(hex) && !['255,122,26', '255,179,26', '255,138,61'].includes(key)) continue;
|
||||
if (!rgbStats.has(key)) rgbStats.set(key, { n: 0, files: new Set(), hex });
|
||||
const s = rgbStats.get(key); s.n++; s.files.add(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(ROOT);
|
||||
|
||||
const rows = [...stats.entries()].map(([hex, s]) => ({ hex, n: s.n, f: s.files.size }))
|
||||
.sort((a, b) => b.n - a.n);
|
||||
const rgbRows = [...rgbStats.entries()].map(([k, s]) => ({ k, hex: s.hex, n: s.n, f: s.files.size }))
|
||||
.sort((a, b) => b.n - a.n);
|
||||
|
||||
console.log('=== 十六进制旧色值(' + rows.length + ' 种)===');
|
||||
let sum = 0, filesMax = 0;
|
||||
rows.forEach(r => { sum += r.n; filesMax = Math.max(filesMax, r.f); console.log(' ' + r.hex + ' ' + String(r.n).padStart(4) + ' 次 ' + String(r.f).padStart(3) + ' 文件'); });
|
||||
console.log(' 小计 ' + sum + ' 次');
|
||||
|
||||
console.log('\n=== rgb()/rgba() 旧三元组(' + rgbRows.length + ' 种)===');
|
||||
let sum2 = 0;
|
||||
rgbRows.forEach(r => { sum2 += r.n; console.log(' rgb(' + r.k + ') ' + String(r.n).padStart(4) + ' 次 ' + String(r.f).padStart(3) + ' 文件'); });
|
||||
console.log(' 小计 ' + sum2 + ' 次');
|
||||
|
||||
console.log('\n合计 ' + (sum + sum2) + ' 处');
|
||||
@@ -0,0 +1,44 @@
|
||||
// 临时静态服务器(仅用于本机预览 _design-test 产物)
|
||||
// 用法: node _design-test/serve.js [port]
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const PORT = Number(process.argv[2] || 55501);
|
||||
const MIME = {
|
||||
'.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8',
|
||||
'.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.woff2': 'font/woff2'
|
||||
};
|
||||
|
||||
http.createServer((req, res) => {
|
||||
// 供浏览器栅格化后回传 PNG 落盘(仅允许写入 _design-test/logo/ 下)
|
||||
if (req.method === 'POST' && req.url.indexOf('/__save') === 0) {
|
||||
const rel = decodeURIComponent((req.url.split('path=')[1] || ''));
|
||||
const base = path.join(ROOT, '_design-test', 'logo');
|
||||
const dest = path.join(base, rel);
|
||||
if (!dest.startsWith(base)) { res.writeHead(403).end('forbidden'); return; }
|
||||
const chunks = [];
|
||||
req.on('data', c => chunks.push(c));
|
||||
req.on('end', () => {
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.writeFileSync(dest, Buffer.concat(chunks));
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('ok ' + rel + ' ' + Buffer.concat(chunks).length + 'B');
|
||||
});
|
||||
return;
|
||||
}
|
||||
let p = decodeURIComponent(req.url.split('?')[0]);
|
||||
if (p === '/') p = '/_design-test/zhenfang-logo-picker.html';
|
||||
let file = path.join(ROOT, p);
|
||||
if (!fs.existsSync(file) && fs.existsSync(path.join(ROOT, '_design-test', p))) {
|
||||
file = path.join(ROOT, '_design-test', p); // 允许直接用 /xxx.html 访问 _design-test 下的产物
|
||||
}
|
||||
if (!file.startsWith(ROOT)) { res.writeHead(403).end('forbidden'); return; }
|
||||
fs.readFile(file, (err, buf) => {
|
||||
if (err) { res.writeHead(404).end('not found: ' + p); return; }
|
||||
res.writeHead(200, { 'Content-Type': MIME[path.extname(file).toLowerCase()] || 'application/octet-stream' });
|
||||
res.end(buf);
|
||||
});
|
||||
}).listen(PORT, () => console.log('serving ' + ROOT + ' at http://localhost:' + PORT + '/'));
|
||||
@@ -0,0 +1,849 @@
|
||||
// 甄房业务系统 · 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, '&').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]) => `
|
||||
<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);
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,70 @@
|
||||
/* 甄房业务系统 · 设计令牌 v1.1(定稿 2026-09-19)
|
||||
* 色彩体系:SAP Fiori Morning Horizon(官方实测值)
|
||||
* 引入顺序:本文件应在项目根样式之前引入
|
||||
* 规则:界面实现一律使用 var(--zf-*) ,禁止直接写十六进制色值
|
||||
* 来源:@sap-theming/theming-base-content → sap_horizon/css_variables.css
|
||||
*/
|
||||
:root {
|
||||
/* ── 品牌层 ── */
|
||||
--zf-brand-ink: #0C2340;
|
||||
--zf-brand-ink-deep: #071527;
|
||||
--zf-brand-gold: #C6A15B;
|
||||
--zf-brand-gold-soft: #E3CB99;
|
||||
|
||||
/* ── 交互层 ── */
|
||||
--zf-primary: #0070F2;
|
||||
--zf-primary-hover: #0064D9;
|
||||
--zf-primary-active-bg: #FFFFFF;
|
||||
--zf-primary-active-ink: #0064D9;
|
||||
--zf-link: #0064D9;
|
||||
|
||||
/* ── 表面层 ── */
|
||||
--zf-bg-base: #FFFFFF;
|
||||
--zf-bg-sub: #F5F6F7;
|
||||
--zf-bg-hover: #EAECEE;
|
||||
--zf-bg-active: #DEE2E5;
|
||||
--zf-bg-selected: #EBF8FF;
|
||||
|
||||
/* ── 边框 ── */
|
||||
--zf-border: #E5E5E5;
|
||||
--zf-border-field: #556B81;
|
||||
--zf-border-strong: #758CA4;
|
||||
|
||||
/* ── 文字 ── */
|
||||
--zf-text-1: #131E29;
|
||||
--zf-text-2: #556B82;
|
||||
--zf-text-3: #5F7590;
|
||||
--zf-text-placeholder: #5F7590;
|
||||
--zf-text-inverse: #FFFFFF;
|
||||
--zf-text-inverse-2: #BBC1CA;
|
||||
|
||||
/* ── 语义色 ── */
|
||||
--zf-success: #256F3A;
|
||||
--zf-error: #AA0808;
|
||||
--zf-warning: #E76500;
|
||||
--zf-info: #0070F2;
|
||||
--zf-neutral: #788FA6;
|
||||
|
||||
/* ── 形状与层级 ── */
|
||||
--zf-radius-s: 4px;
|
||||
--zf-radius: 8px;
|
||||
--zf-radius-l: 12px;
|
||||
--zf-radius-tile: 16px;
|
||||
--zf-radius-pill: 999px;
|
||||
--zf-shadow-1: 0 1px 2px rgba(34,53,72,.08);
|
||||
--zf-shadow-2: 0 2px 8px rgba(34,53,72,.12);
|
||||
|
||||
/* ── 字号(官方 rem 值,根字号 16px)── */
|
||||
--zf-fs-caption: 11px;
|
||||
--zf-fs-body-s: 12px;
|
||||
--zf-fs-body: 14px;
|
||||
--zf-fs-title-s: 16px;
|
||||
--zf-fs-title: 20px;
|
||||
--zf-fs-display: 24px;
|
||||
|
||||
/* ── 间距(4pt 基数)── */
|
||||
--zf-sp-1: 4px; --zf-sp-2: 8px; --zf-sp-3: 12px; --zf-sp-4: 16px; --zf-sp-5: 20px; --zf-sp-6: 24px; --zf-sp-7: 32px; --zf-sp-8: 40px;
|
||||
|
||||
/* ── 字族 ── */
|
||||
--zf-font: "72", "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
// 甄房业务系统 · 三套色彩体系对比页
|
||||
// 结构:一套 UI 演示结构 + 三套 CSS 变量作用域([data-theme]),保证对比公平
|
||||
// 对比度由页面运行时真实计算(非手写)
|
||||
// 用法: node _design-test/theme/build-theme-compare.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 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)';
|
||||
|
||||
// 品牌标识(框 + Z),c1=框色 c2=Z色
|
||||
const mark = (c1, c2, size) =>
|
||||
'<svg viewBox="0 0 64 64" width="' + size + '" height="' + size + '">' +
|
||||
'<g transform="' + FG + '"><path d="' + H04 + '" fill="none" stroke="' + c1 +
|
||||
'" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></g>' +
|
||||
'<path d="' + FZ.final + '" fill="' + c2 + '"/></svg>';
|
||||
|
||||
// ── 三套色彩令牌 ────────────────────────────────────────────────
|
||||
// 说明:品牌墨蓝 #0C2340 在三套方案中都用于「品牌/导航区」,
|
||||
// 交互色各自采用该体系的官方主色 —— 即「品牌色与交互色分离」。
|
||||
const THEMES = [
|
||||
{
|
||||
id: 'carbon', name: '方案 A · Carbon 严谨派', sys: 'IBM Carbon Design System',
|
||||
score: { dense: 9.5, comfort: 9.5, a11y: 10, brand: 8.5, cost: 7.5, proven: 9.0 },
|
||||
radius: '2px', font: '"IBM Plex Sans", "PingFang SC", "Microsoft YaHei", sans-serif',
|
||||
shadow: 'none',
|
||||
desc: '以中性灰主导、蓝色只承担「行动色」的经典企业级体系。四套官方主题(White / Gray 10 / Gray 90 / Gray 100)与分层模型,为数据密集型界面而生。',
|
||||
tone: { brand:'#0C2340', primary:'#0F62FE', bg:'#FFFFFF', bg2:'#F4F4F4' },
|
||||
tags: [
|
||||
['品牌 / 导航底', '#0C2340', '甄房墨蓝(品牌区、侧栏、登录页)'],
|
||||
['交互主色', '#0F62FE', 'Blue 60 · 官方 primary action'],
|
||||
['主色悬停', '#0043CE', 'Blue 70'],
|
||||
['主色按下', '#002D9C', 'Blue 80'],
|
||||
['主色浅底', '#EDF5FF', 'Blue 10 · 高亮/选中浅底'],
|
||||
['页面底色', '#FFFFFF', '$background(White 主题)'],
|
||||
['分层底色', '#F4F4F4', 'Gray 10 · $layer-01'],
|
||||
['悬停底色', '#E8E8E8', 'Gray 10 hover'],
|
||||
['选中底色', '#E0E0E0', 'Gray 20 · $layer-selected-01'],
|
||||
['边框', '#E0E0E0', 'Gray 20'],
|
||||
['边框-强', '#8D8D8D', 'Gray 50'],
|
||||
['一级文字', '#161616', 'Gray 100'],
|
||||
['二级文字', '#525252', 'Gray 70'],
|
||||
['三级文字', '#6F6F6F', 'Gray 60'],
|
||||
['成功', '#198038', 'Green 60'],
|
||||
['警告', '#F1C21B', 'Yellow 30'],
|
||||
['错误', '#DA1E28', 'Red 60'],
|
||||
['信息', '#0F62FE', 'Blue 60'],
|
||||
['品牌金(点缀)', '#C6A15B', '仅 logo / 关键数字,占比 ≤5%']
|
||||
],
|
||||
pros: ['中性灰主导(官方原文 “Neutral Gray family is dominant”),长时间盯表格最不累',
|
||||
'明确要求「避免使用中间调」,界面层次靠灰阶深浅而非色块,密度最高',
|
||||
'无障碍是一等公民:官方每个令牌都标注对比度要求(如 $layer-selected-inverse 标 4.5:1 AA)',
|
||||
'四套官方主题 + 分层模型,深色模式与高对比模式有现成规范'],
|
||||
cons: ['圆角基本为 0(2px),偏工程感,国内用户可能觉得「硬」',
|
||||
'品牌金几乎没有落点,品牌温度靠墨蓝承担',
|
||||
'与 ElementUI 的默认圆角/间距差异大,改造成本三套中最高'],
|
||||
fit: ['数据密度极高的作业台(房源批量检核、佣金结算)',
|
||||
'需要长时间连续录入/核对的岗位',
|
||||
'对无障碍有硬性要求的场景(政企、大客户交付)'],
|
||||
// 生产落地必改项:[用途, 原值, 建议改为, 理由]
|
||||
fixes: [
|
||||
['警告色用法', '#F1C21B 作文字色', '黄只作底色(#F1C21B 底 + #161616 字)',
|
||||
'Yellow 30 在白底上仅 1.68:1;Carbon 官方本就把黄色用于警报条底色而非文字色'],
|
||||
['圆角', '2px', '按国内习惯可放宽到 4px',
|
||||
'不是超标项,属外观取舍;Carbon 的 0–2px 方角在国内后台会显「硬」']
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'fiori', name: '方案 B · Fiori 商务派', sys: 'SAP Fiori · Morning Horizon',
|
||||
final: true, // ✓ 已定稿(2026-09-19)
|
||||
score: { dense: 9.0, comfort: 8.8, a11y: 9.5, brand: 8.0, cost: 7.0, proven: 9.5 },
|
||||
radius: '8px', font: '"72", "PingFang SC", "Microsoft YaHei", sans-serif',
|
||||
shadow: '0 1px 2px rgba(34,53,72,.08)',
|
||||
desc: 'ERP 领域事实标准。语义令牌分层最完整:表面 S0–S8、文字 T1–T9、边框、语义色、强调色五组,并给出可推导的对比度规则。',
|
||||
tone: { brand:'#0C2340', primary:'#0070F2', bg:'#FFFFFF', bg2:'#F5F6F7' },
|
||||
tags: [
|
||||
['品牌 / Shell Bar', '#0C2340', '甄房墨蓝(Shell Bar、应用头)'],
|
||||
['交互主色', '#0070F2', 'Blue 7 · S2 Secondary / T4 CTA'],
|
||||
['主色悬停', '#0057D2', 'Blue 8 · T7 Selected'],
|
||||
['主色按下', '#0040B0', '同体系推导(Blue 8 再压一档)'],
|
||||
['主色浅底', '#EBF4FE', 'S5 Selected Surface(官方 rgba(0,112,242,.08) 在白底上的合成值)'],
|
||||
['页面底色', '#FFFFFF', 'S0 Base Background'],
|
||||
['次级底色', '#F5F6F7', 'S4 Quaternary · Grey 1'],
|
||||
['边框', '#D5DADD', 'Grey 3'],
|
||||
['边框-强', '#A9B4BE', 'Grey 4'],
|
||||
['一级文字', '#223548', 'T1 · Grey 9'],
|
||||
['二级文字', '#475E75', 'T2 · Grey 7'],
|
||||
['三级文字', '#5B738B', 'T3 · Grey 6'],
|
||||
['成功', '#256F3A', 'Positive(同体系参考值)'],
|
||||
['警告', '#E76500', 'Critical · Mango(同体系参考值)'],
|
||||
['错误', '#D20A0A', 'Red 7 · Negative(官方确认)'],
|
||||
['信息', '#0070F2', 'Blue 7'],
|
||||
['品牌金(点缀)', '#C6A15B', '仅 logo / 关键数字,占比 ≤5%']
|
||||
],
|
||||
pros: ['唯一为 ERP 事务场景设计的体系,表格/表单/Shell 的层级划分最成熟',
|
||||
'「相隔 4 级 = 3:1、相隔 5 级 = 4.5:1」——对比度可推导,选色不用猜',
|
||||
'选中态用 8% 蓝底(而非实色块),长时间使用不刺眼',
|
||||
'品牌化路径最清晰:官方 Quick Theming 就是「替换 logo + 主品牌色」'],
|
||||
cons: ['Shell Bar + 应用头 + 页面头三层结构,垂直空间占用大,密度略低于 Carbon',
|
||||
'蓝白对比偏冷、现代感一般,年轻经纪人可能觉得「像老系统」',
|
||||
'部分语义色官方文档未完整公开,落地前需核对准确值'],
|
||||
fit: ['交易全流程、佣金结算这类强流程、强状态的模块',
|
||||
'与财务/ERP 打通、需要符合企业级规范的场景',
|
||||
'多角色、多层级权限的后台(选中态与层级表达最清楚)'],
|
||||
fixes: [] // 12/12 全部通过,无需改动
|
||||
},
|
||||
{
|
||||
id: 'antd', name: '方案 C · AntD 亲和派', sys: 'Ant Design 5',
|
||||
score: { dense: 8.0, comfort: 7.5, a11y: 8.0, brand: 8.5, cost: 9.5, proven: 9.0 },
|
||||
radius: '6px', font: '-apple-system, "PingFang SC", "Microsoft YaHei", sans-serif',
|
||||
shadow: '0 1px 2px rgba(0,0,0,.06)',
|
||||
desc: '国内中后台事实标准。12 色 × 10 阶色板 + 算法生成,可在品牌色基础上自动产出整条色阶;中文文档与团队熟悉度最高。',
|
||||
tone: { brand:'#0C2340', primary:'#1677FF', bg:'#FFFFFF', bg2:'#F5F5F5' },
|
||||
tags: [
|
||||
['品牌 / 侧栏', '#0C2340', '甄房墨蓝(侧栏、品牌区)'],
|
||||
['交互主色', '#1677FF', 'colorPrimary · blue-6(官方默认)'],
|
||||
['主色悬停', '#4096FF', 'blue-5'],
|
||||
['主色按下', '#0958D9', 'blue-7'],
|
||||
['主色浅底', '#E6F4FF', 'blue-1 · 选中行浅底'],
|
||||
['页面底色', '#FFFFFF', '官方容器色'],
|
||||
['次级底色', '#F5F5F5', '官方中性灰'],
|
||||
['边框', '#D9D9D9', '官方边框色'],
|
||||
['一级文字', '#1F1F1F', 'rgba(0,0,0,.88) 合成值'],
|
||||
['二级文字', '#595959', 'rgba(0,0,0,.65) 合成值'],
|
||||
['三级文字', '#8C8C8C', 'rgba(0,0,0,.45) 合成值'],
|
||||
['成功', '#52C41A', 'colorSuccess'],
|
||||
['警告', '#FAAD14', 'colorWarning'],
|
||||
['错误', '#FF4D4F', 'colorError'],
|
||||
['信息', '#1677FF', 'colorInfo'],
|
||||
['品牌金(点缀)', '#C6A15B', '仅 logo / 关键数字,占比 ≤5%']
|
||||
],
|
||||
pros: ['团队最熟、中文文档最全,与现有 Vue2 + ElementUI 生态迁移成本最低',
|
||||
'色板算法可由品牌色直接生成整条色阶(12 阶),扩展与派生最省事',
|
||||
'色彩语义与国内用户预期一致(成功绿/警告黄/错误红),零学习成本',
|
||||
'品牌金在三套中最好安排(圆角卡片 + 金色数字自然)'],
|
||||
cons: ['默认色饱和偏高:#FF4D4F 错误色、#52C41A 成功色长时间看刺激较强',
|
||||
'官方定位偏「轻盈」,高数据密度下需要额外收紧间距与层级',
|
||||
'中性灰层级不如 Carbon/Fiori 严谨,容易出现「灰得不够分明」'],
|
||||
fit: ['经纪人日常高频作业(房源/客源/跟进),要求上手快、认知零负担',
|
||||
'迭代节奏快、需要大量复用生态组件的中后台',
|
||||
'团队以国内前端为主、希望降低长期维护成本的项目'],
|
||||
// 生产落地必改项:[用途, 原值, 建议改为, 理由]
|
||||
fixes: [
|
||||
['主色(按钮底 / 链接字)', '#1677FF', '#0958D9(blue-7)',
|
||||
'官方默认主色在白底上实测仅 4.10:1,差 0.4 未达 AA 4.5:1 —— 「第 6 阶满足 4.5:1」只对部分色相成立;下移一阶到 blue-7 即 6.16:1,肉眼几乎无差别'],
|
||||
['成功色', '#52C41A', '#389E0D(green-7)',
|
||||
'#52C41A 白底仅 2.27:1,连图形类 3:1 都不到;图标与文字改 green-7(3.46:1),浅底仍可保留亮绿'],
|
||||
['警告色', '#FAAD14', '文字与描边改 #AD6800(gold-8)',
|
||||
'#FAAD14 白底仅 1.90:1;沿用「亮色只作底、深色作字」的做法,gold-8 为 4.41:1,几乎直接满足正文标准'],
|
||||
['错误色(正文)', '#FF4D4F', '正文与链接改 #CF1322(red-7)',
|
||||
'#FF4D4F 作正文仅 3.27:1;作为图形勉强及格,但表格里的错误提示是正文级文字,改 red-7 后 5.57:1;描边与底色可继续用亮红'],
|
||||
['三级文字', '#8C8C8C', '#6E6E6E',
|
||||
'rgba(0,0,0,.45) 的合成值白底 3.36:1,未达 AA;加深到 #6E6E6E 即 5.10:1'],
|
||||
['灰阶层级', '仅三级文字灰', '补一级 #BFBFBF 专用于占位符 / 禁用',
|
||||
'AntD 只有三级文字灰,导致占位符被迫复用 #8C8C8C —— 既不够浅(有对比度不达标风险)又会被误读为正文;补一级浅灰后三级文字才能安心加深']
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// 加权评分(权重:密度25 舒适20 无障碍15 品牌15 成本15 验证10)
|
||||
const W = { dense: .25, comfort: .20, a11y: .15, brand: .15, cost: .15, proven: .10 };
|
||||
THEMES.forEach(t => { t.total = Object.keys(W).reduce((s, k) => s + t.score[k] * W[k], 0); });
|
||||
|
||||
const CSS = `
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:#0a0d13;color:#e9edf5;font-family:"PingFang SC","Microsoft YaHei",system-ui,sans-serif;
|
||||
padding:40px 24px 90px;line-height:1.6}
|
||||
.wrap{max-width:1500px;margin:0 auto}
|
||||
h1{font-size:27px;letter-spacing:.2px}h1 b{color:#E3CB99}
|
||||
.lead{color:#8b94a7;font-size:13.5px;margin:10px 0 26px;line-height:1.95}
|
||||
h2{font-size:19px;margin:46px 0 6px;padding-left:11px;border-left:4px solid #C6A15B}
|
||||
h2 span{font-size:11px;font-weight:400;color:#8b94a7;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}
|
||||
|
||||
/* ── 调研结论 ── */
|
||||
table.cmp{width:100%;border-collapse:collapse;font-size:12.5px;margin-top:14px}
|
||||
table.cmp th,table.cmp td{border:1px solid rgba(255,255,255,.09);padding:9px 11px;text-align:left}
|
||||
table.cmp th{background:rgba(255,255,255,.045);font-weight:600;color:#cfd6e4}
|
||||
table.cmp td.num{text-align:center;font-variant-numeric:tabular-nums}
|
||||
table.cmp tr.win td{background:rgba(198,161,91,.10)}
|
||||
table.cmp td.tot{font-weight:700;color:#E3CB99}
|
||||
.badge{display:inline-block;font-size:10px;padding:1px 7px;border-radius:20px;margin-left:7px;
|
||||
border:1px solid rgba(198,161,91,.45);color:#E3CB99}
|
||||
.src{font-size:11px;color:#6d768a;margin-top:8px;line-height:1.9}
|
||||
.src a{color:#8ab4f8;text-decoration:none}
|
||||
|
||||
/* ── 方案卡 ── */
|
||||
.plan{border:1px solid rgba(255,255,255,.09);border-radius:16px;padding:22px;margin-top:18px;
|
||||
background:linear-gradient(180deg,rgba(255,255,255,.03),rgba(255,255,255,.012))}
|
||||
.plan .hd{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
|
||||
.plan .hd h3{margin:0;font-size:18px;color:#fff}
|
||||
.plan .hd .sys{font-size:11.5px;color:#8b94a7}
|
||||
.plan .hd .sc{margin-left:auto;font-size:13px;color:#E3CB99;font-weight:700}
|
||||
.plan .hd .finalmark{font-size:11px;font-weight:700;padding:2px 9px;border-radius:20px;
|
||||
background:rgba(94,224,138,.13);border:1px solid rgba(94,224,138,.5);color:#5ee08a}
|
||||
.plan.final{border-color:rgba(94,224,138,.42);box-shadow:0 0 0 1px rgba(94,224,138,.16) inset}
|
||||
.plan .d{font-size:12.5px;color:#9aa4b8;margin:9px 0 16px;line-height:1.9}
|
||||
.pc{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}
|
||||
.pc>div{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);border-radius:11px;padding:13px 15px}
|
||||
.pc h4{font-size:12.5px;margin-bottom:8px}
|
||||
.pc ul{list-style:none;font-size:12px;color:#98a1b3;line-height:1.95}
|
||||
.pc ul li{padding-left:15px;position:relative}
|
||||
.pc ul li:before{position:absolute;left:0;top:0}
|
||||
.pc .good li:before{content:'+';color:#5ee08a}
|
||||
.pc .bad li:before{content:'−';color:#f0a55c}
|
||||
.pc .fit li:before{content:'·';color:#8ab4f8}
|
||||
|
||||
/* ── 生产落地必改项 ── */
|
||||
table.fix{margin-top:10px;font-size:12px}
|
||||
table.fix th:nth-child(1),table.fix td:nth-child(1){width:132px}
|
||||
table.fix th:nth-child(2),table.fix td:nth-child(2){width:150px}
|
||||
table.fix th:nth-child(3),table.fix td:nth-child(3){width:250px}
|
||||
table.fix code{background:rgba(255,255,255,.06);padding:1px 5px;border-radius:4px;font-size:11px}
|
||||
table.fix td:nth-child(2) code{color:#f0a55c}
|
||||
table.fix td:nth-child(3) code{color:#5ee08a}
|
||||
.fixok{font-size:12.5px;color:#5ee08a;background:rgba(94,224,138,.07);
|
||||
border:1px solid rgba(94,224,138,.28);border-radius:9px;padding:10px 13px;margin-top:10px}
|
||||
.sumline{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}
|
||||
.sumline div{flex:1;min-width:230px;background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.08);
|
||||
border-radius:10px;padding:10px 13px;font-size:12px;color:#98a1b3}
|
||||
.sumline div b{color:#e9edf5}
|
||||
.sumline div em{font-style:normal;font-weight:700}
|
||||
.sumline div em.ok{color:#5ee08a}.sumline div em.no{color:#f0a55c}
|
||||
|
||||
/* ── 令牌表 ── */
|
||||
.tok{display:grid;grid-template-columns:repeat(auto-fill,minmax(232px,1fr));gap:7px;margin-top:12px}
|
||||
.tk{display:flex;align-items:center;gap:9px;background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.07);
|
||||
border-radius:9px;padding:7px 10px;font-size:11.5px}
|
||||
.tk i{width:22px;height:22px;border-radius:6px;flex:none;border:1px solid rgba(255,255,255,.16)}
|
||||
.tk .n{flex:1;min-width:0}
|
||||
.tk .n b{display:block;font-weight:600;font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.tk .n code{color:#7f8899;font-size:10px;letter-spacing:.2px}
|
||||
.tk .r{font-size:9.5px;padding:1px 6px;border-radius:20px;border:1px solid;flex:none;white-space:nowrap}
|
||||
.tk .r.pass{color:#5ee08a;border-color:rgba(94,224,138,.4)}
|
||||
.tk .r.fail{color:#f0a55c;border-color:rgba(240,165,92,.4)}
|
||||
|
||||
/* ── UI 演示(三套作用域)── */
|
||||
.demo{border-radius:14px;overflow:hidden;margin-top:14px;border:1px solid rgba(255,255,255,.12);background:var(--bg);
|
||||
font-family:var(--font);color:var(--text1);font-size:12.5px}
|
||||
.demo[data-theme=carbon]{--brand:#0C2340;--primary:#0F62FE;--primary-h:#0043CE;--primary-a:#002D9C;--primary-s:#EDF5FF;
|
||||
--bg:#FFFFFF;--bg2:#F4F4F4;--hover:#E8E8E8;--sel:#E0E0E0;--bd:#E0E0E0;--bd-s:#8D8D8D;--text1:#161616;--text2:#525252;--text3:#6F6F6F;
|
||||
--ok:#198038;--warn:#F1C21B;--err:#DA1E28;--radius:2px;--sh:none;--gold:#C6A15B;
|
||||
--font:"IBM Plex Sans","PingFang SC","Microsoft YaHei",sans-serif}
|
||||
.demo[data-theme=fiori]{--brand:#0C2340;--primary:#0070F2;--primary-h:#0057D2;--primary-a:#0040B0;--primary-s:rgba(0,112,242,.08);
|
||||
--bg:#FFFFFF;--bg2:#F5F6F7;--hover:rgba(0,112,242,.06);--sel:rgba(0,112,242,.10);--bd:#D5DADD;--bd-s:#A9B4BE;--text1:#223548;--text2:#475E75;--text3:#5B738B;
|
||||
--ok:#256F3A;--warn:#E76500;--err:#D20A0A;--radius:8px;--sh:0 1px 2px rgba(34,53,72,.08);--gold:#C6A15B;
|
||||
--font:"72","PingFang SC","Microsoft YaHei",sans-serif}
|
||||
.demo[data-theme=antd]{--brand:#0C2340;--primary:#1677FF;--primary-h:#4096FF;--primary-a:#0958D9;--primary-s:#E6F4FF;
|
||||
--bg:#FFFFFF;--bg2:#F5F5F5;--hover:#F5F5F5;--sel:#E6F4FF;--bd:#D9D9D9;--bd-s:#BFBFBF;--text1:#1F1F1F;--text2:#595959;--text3:#8C8C8C;
|
||||
--ok:#52C41A;--warn:#FAAD14;--err:#FF4D4F;--radius:6px;--sh:0 1px 2px rgba(0,0,0,.06);--gold:#C6A15B;
|
||||
--font:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif}
|
||||
|
||||
.app{display:grid;grid-template-columns:196px 1fr;min-height:540px}
|
||||
.app .side{background:var(--brand);padding:14px 0;display:flex;flex-direction:column;gap:2px}
|
||||
.app .side .brand{display:flex;align-items:center;gap:9px;padding:2px 14px 16px;color:#fff}
|
||||
.app .side .brand b{font-size:15px;font-weight:600;letter-spacing:1px}
|
||||
.app .side .brand em{font-size:8px;letter-spacing:1.4px;opacity:.62;font-style:normal;display:block}
|
||||
.app .side a{display:flex;align-items:center;gap:9px;color:rgba(255,255,255,.72);text-decoration:none;
|
||||
font-size:12.5px;padding:9px 14px;border-left:3px solid transparent}
|
||||
.app .side a.on{background:rgba(255,255,255,.10);color:#fff;border-left-color:var(--gold);font-weight:600}
|
||||
.app .side a .d{width:6px;height:6px;border-radius:2px;background:currentColor;opacity:.55;flex:none}
|
||||
.app .main{display:flex;flex-direction:column;min-width:0}
|
||||
.app .top{display:flex;align-items:center;gap:14px;padding:11px 18px;border-bottom:1px solid var(--bd);background:var(--bg)}
|
||||
.app .top .crumb{font-size:12.5px;color:var(--text2)}
|
||||
.app .top .crumb b{color:var(--text1);font-weight:600}
|
||||
.app .top .usr{margin-left:auto;font-size:12px;color:var(--text2);display:flex;align-items:center;gap:8px}
|
||||
.app .top .usr .av{width:24px;height:24px;border-radius:50%;background:var(--primary-s);color:var(--primary);
|
||||
display:flex;align-items:center;justify-content:center;font-size:10.5px;font-weight:600}
|
||||
.app .body{padding:14px 18px 20px;background:var(--bg2);flex:1;display:flex;flex-direction:column;gap:12px}
|
||||
|
||||
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
|
||||
.kpi{background:var(--bg);border:1px solid var(--bd);border-radius:var(--radius);padding:11px 13px;box-shadow:var(--sh)}
|
||||
.kpi .t{font-size:11px;color:var(--text3)}
|
||||
.kpi .v{font-size:20px;font-weight:600;color:var(--text1);margin-top:3px;font-variant-numeric:tabular-nums}
|
||||
.kpi .v em{font-size:11px;font-style:normal;color:var(--text3);font-weight:400;margin-left:3px}
|
||||
.kpi .d{font-size:10.5px;margin-top:2px}
|
||||
.kpi .d.up{color:var(--ok)}.kpi .d.dn{color:var(--err)}
|
||||
.kpi.gold .v{color:var(--gold)}
|
||||
|
||||
.card{background:var(--bg);border:1px solid var(--bd);border-radius:var(--radius);box-shadow:var(--sh)}
|
||||
.filters{display:flex;flex-wrap:wrap;gap:10px;align-items:flex-end;padding:13px}
|
||||
.f{display:flex;flex-direction:column;gap:4px}
|
||||
.f label{font-size:11px;color:var(--text3)}
|
||||
.f input,.f select{font-family:inherit;font-size:12.5px;color:var(--text1);background:var(--bg);
|
||||
border:1px solid var(--bd);border-radius:var(--radius);padding:6px 9px;min-width:132px}
|
||||
.f input::placeholder{color:var(--text3)}
|
||||
.f input:focus,.f select:focus{outline:2px solid var(--primary);outline-offset:-1px;border-color:var(--primary)}
|
||||
.f.err input{border-color:var(--err)}
|
||||
.f .msg{font-size:10.5px;color:var(--err)}
|
||||
.btns{display:flex;gap:8px;margin-left:auto}
|
||||
.btn{font-family:inherit;font-size:12.5px;border-radius:var(--radius);padding:6px 15px;cursor:default;
|
||||
border:1px solid var(--bd);background:var(--bg);color:var(--text1);white-space:nowrap}
|
||||
.btn.pri{background:var(--primary);border-color:var(--primary);color:#fff;font-weight:500}
|
||||
.btn.pri.h{background:var(--primary-h);border-color:var(--primary-h)}
|
||||
.btn.pri.a{background:var(--primary-a);border-color:var(--primary-a)}
|
||||
.btn.dan{background:var(--err);border-color:var(--err);color:#fff}
|
||||
.btn.gho{border-color:var(--primary);color:var(--primary);background:transparent}
|
||||
.btn.tx{border-color:transparent;background:transparent;color:var(--primary);padding:6px 6px}
|
||||
.btn.dis{background:var(--bg2);border-color:var(--bd);color:var(--text3);opacity:.75}
|
||||
|
||||
.tbl{width:100%;border-collapse:collapse;font-size:12.5px;font-variant-numeric:tabular-nums}
|
||||
.tbl th{text-align:left;font-weight:600;font-size:11.5px;color:var(--text2);background:var(--bg2);
|
||||
padding:9px 11px;border-bottom:1px solid var(--bd);white-space:nowrap}
|
||||
.tbl td{padding:9px 11px;border-bottom:1px solid var(--bd);color:var(--text1)}
|
||||
.tbl tbody tr:nth-child(even){background:var(--bg2)}
|
||||
.tbl tbody tr.hov{background:var(--hover)}
|
||||
.tbl tbody tr.sel{background:var(--sel)}
|
||||
.tbl tbody tr.sel td:first-child{box-shadow:inset 2px 0 0 var(--primary)}
|
||||
.tbl .code{color:var(--primary);font-weight:500}
|
||||
.tbl .sub{color:var(--text3);font-size:11px;display:block}
|
||||
.tbl .op a{color:var(--primary);text-decoration:none;margin-right:9px;font-size:12px}
|
||||
.tbl .op a.dan{color:var(--err)}
|
||||
.tag{display:inline-block;font-size:10.5px;padding:1px 8px;border-radius:20px;border:1px solid;white-space:nowrap}
|
||||
.tag.ok{color:var(--ok);border-color:var(--ok);background:color-mix(in srgb,var(--ok) 9%,transparent)}
|
||||
.tag.wn{color:var(--warn);border-color:var(--warn);background:color-mix(in srgb,var(--warn) 12%,transparent)}
|
||||
.tag.er{color:var(--err);border-color:var(--err);background:color-mix(in srgb,var(--err) 9%,transparent)}
|
||||
.tag.pri{color:var(--primary);border-color:var(--primary);background:var(--primary-s)}
|
||||
.tag.nu{color:var(--text2);border-color:var(--bd-s);background:transparent}
|
||||
.tag.gold{color:#8a6a2f;border-color:var(--gold);background:color-mix(in srgb,var(--gold) 14%,transparent)}
|
||||
|
||||
.pager{display:flex;align-items:center;gap:7px;padding:11px 13px;font-size:12px;color:var(--text2)}
|
||||
.pg{min-width:26px;height:26px;display:flex;align-items:center;justify-content:center;border:1px solid var(--bd);
|
||||
border-radius:var(--radius);background:var(--bg);color:var(--text1)}
|
||||
.pg.on{background:var(--primary);border-color:var(--primary);color:#fff;font-weight:600}
|
||||
|
||||
.tabs{display:flex;gap:2px;border-bottom:1px solid var(--bd);padding:0 13px}
|
||||
.tabs a{font-size:12.5px;padding:9px 13px;color:var(--text2);text-decoration:none;border-bottom:2px solid transparent}
|
||||
.tabs a.on{color:var(--primary);border-bottom-color:var(--primary);font-weight:600}
|
||||
|
||||
.alerts{display:grid;grid-template-columns:1fr 1fr;gap:9px}
|
||||
.al{display:flex;gap:8px;align-items:flex-start;font-size:12px;padding:9px 12px;border-radius:var(--radius);border:1px solid}
|
||||
.al b{font-weight:600}
|
||||
.al.i{border-color:var(--primary);background:var(--primary-s);color:var(--text1)}
|
||||
.al.s{border-color:var(--ok);background:color-mix(in srgb,var(--ok) 8%,transparent);color:var(--text1)}
|
||||
.al.w{border-color:var(--warn);background:color-mix(in srgb,var(--warn) 11%,transparent);color:var(--text1)}
|
||||
.al.e{border-color:var(--err);background:color-mix(in srgb,var(--err) 8%,transparent);color:var(--text1)}
|
||||
|
||||
/* 元素级对照 */
|
||||
.elcmp{margin-top:12px;border:1px solid rgba(255,255,255,.09);border-radius:12px;overflow:hidden}
|
||||
table.el{width:100%;border-collapse:collapse;font-size:12px}
|
||||
table.el th,table.el td{border-bottom:1px solid rgba(255,255,255,.08);padding:8px 12px;vertical-align:middle}
|
||||
table.el th{background:rgba(255,255,255,.04);color:#cfd6e4;font-size:11.5px;text-align:left}
|
||||
table.el td:first-child{color:#8b94a7;font-size:11.5px;width:118px}
|
||||
table.el td .demo{display:inline-block;margin:0;padding:9px 12px;border:none;border-radius:8px;min-width:0}
|
||||
`;
|
||||
|
||||
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
// ── UI 演示结构(只写一次,三套主题注入同一份)──
|
||||
const demoBody = () => `
|
||||
<div class="app">
|
||||
<aside class="side">
|
||||
<div class="brand">${mark('#FFFFFF', '#FFFFFF', 30)}<span><b>甄房</b><em>ZHENFANG</em></span></div>
|
||||
<a class="on"><span class="d"></span>工作台</a>
|
||||
<a><span class="d"></span>房源管理</a>
|
||||
<a><span class="d"></span>客源管理</a>
|
||||
<a><span class="d"></span>交易管理</a>
|
||||
<a><span class="d"></span>钥匙管理</a>
|
||||
<a><span class="d"></span>我的佣金</a>
|
||||
<a><span class="d"></span>系统设置</a>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<div class="top">
|
||||
<div class="crumb">房源管理 / <b>二手房房源</b></div>
|
||||
<div class="usr"><span class="av">张</span>张伟 · 滨江门店</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="kpis">
|
||||
<div class="kpi"><div class="t">在售房源</div><div class="v">1,284<em>套</em></div><div class="d up">↑ 3.2% 较上周</div></div>
|
||||
<div class="kpi"><div class="t">本周新增客源</div><div class="v">96<em>位</em></div><div class="d up">↑ 12 位</div></div>
|
||||
<div class="kpi"><div class="t">待跟进</div><div class="v">23<em>条</em></div><div class="d dn">↓ 5 条</div></div>
|
||||
<div class="kpi gold"><div class="t">本月预估佣金</div><div class="v">86.4<em>万</em></div><div class="d up">↑ 8.6%</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card filters">
|
||||
<div class="f"><label>房源编码</label><input placeholder="SH-ES-..."></div>
|
||||
<div class="f"><label>所属楼盘</label><select><option>滨江壹号</option><option>钱江新城</option></select></div>
|
||||
<div class="f"><label>委托起始日</label><input value="2026-09-01"></div>
|
||||
<div class="f err"><label>维护人</label><input value="张"><span class="msg">请输入完整姓名</span></div>
|
||||
<div class="btns">
|
||||
<button class="btn pri">查询</button>
|
||||
<button class="btn">重置</button>
|
||||
<button class="btn gho">高级筛选</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="tabs">
|
||||
<a class="on">全部 1284</a><a>有效 892</a><a>维护中 236</a><a>已成交 121</a><a>暂缓 35</a>
|
||||
</div>
|
||||
<table class="tbl">
|
||||
<thead><tr>
|
||||
<th>房源编码</th><th>楼盘 / 房号</th><th>面积</th><th>售价</th><th>状态</th><th>维护人</th><th>操作</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr><td><span class="code">SH-ES-202609-00037</span></td>
|
||||
<td>滨江壹号 3幢 1802<span class="sub">滨江区 · 钱塘江畔</span></td>
|
||||
<td>128.5 ㎡</td><td>1,285 万</td><td><span class="tag ok">有效</span></td><td>张伟</td>
|
||||
<td class="op"><a>查看</a><a>编辑</a></td></tr>
|
||||
<tr class="hov"><td><span class="code">SH-ES-202609-00041</span></td>
|
||||
<td>钱江新城 7幢 2201<span class="sub">上城区 · 万象城</span></td>
|
||||
<td>156.0 ㎡</td><td>1,860 万</td><td><span class="tag pri">待检核</span></td><td>李敏</td>
|
||||
<td class="op"><a>查看</a><a>编辑</a></td></tr>
|
||||
<tr class="sel"><td><span class="code">SH-ES-202609-00052</span></td>
|
||||
<td>滨江壹号 5幢 901<span class="sub">滨江区 · 钱塘江畔</span></td>
|
||||
<td>98.2 ㎡</td><td>920 万</td><td><span class="tag gold">独家</span></td><td>王强</td>
|
||||
<td class="op"><a>查看</a><a>编辑</a></td></tr>
|
||||
<tr><td><span class="code">SH-ES-202609-00058</span></td>
|
||||
<td>绿城桂语 2幢 1503<span class="sub">西湖区 · 文三路</span></td>
|
||||
<td>142.6 ㎡</td><td>1,420 万</td><td><span class="tag wn">维护中</span></td><td>张伟</td>
|
||||
<td class="op"><a>查看</a><a>编辑</a></td></tr>
|
||||
<tr><td><span class="code">SH-ES-202609-00063</span></td>
|
||||
<td>万科未来 1幢 608<span class="sub">余杭区 · 未来科技城</span></td>
|
||||
<td>89.0 ㎡</td><td>610 万</td><td><span class="tag er">已下架</span></td><td>赵磊</td>
|
||||
<td class="op"><a>查看</a><a class="dan">删除</a></td></tr>
|
||||
<tr><td><span class="code">SH-ES-202609-00071</span></td>
|
||||
<td>滨江壹号 2幢 1101<span class="sub">滨江区 · 钱塘江畔</span></td>
|
||||
<td>110.4 ㎡</td><td>1,080 万</td><td><span class="tag nu">草稿</span></td><td>李敏</td>
|
||||
<td class="op"><a>查看</a><a>编辑</a></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pager">共 1,284 条
|
||||
<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>
|
||||
<span style="margin-left:auto"></span>
|
||||
<span style="font-size:11.5px">每页 20 条</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alerts">
|
||||
<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>
|
||||
|
||||
<div class="card" style="padding:13px;display:flex;gap:9px;flex-wrap:wrap;align-items:center">
|
||||
<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 dan">危险操作</button>
|
||||
<button class="btn tx">文字按钮</button>
|
||||
<button class="btn dis">禁用</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// 元素级对照(每行一个元素,三列并排)
|
||||
const EL_ROWS = [
|
||||
['主按钮', t => '<div class="demo" data-theme="' + t + '"><button class="btn pri">查询房源</button></div>'],
|
||||
['次要按钮', t => '<div class="demo" data-theme="' + t + '"><button class="btn gho">高级筛选</button></div>'],
|
||||
['危险按钮', t => '<div class="demo" data-theme="' + t + '"><button class="btn dan">删除房源</button></div>'],
|
||||
['状态标签', t => '<div class="demo" data-theme="' + t + '"><span class="tag ok">有效</span> <span class="tag wn">维护中</span> <span class="tag er">已下架</span> <span class="tag pri">待检核</span> <span class="tag gold">独家</span></div>'],
|
||||
['表单', t => '<div class="demo" data-theme="' + t + '"><div class="f" style="width:150px"><label>房源编码</label><input placeholder="请输入"></div></div>'],
|
||||
['表单错误态', t => '<div class="demo" data-theme="' + t + '"><div class="f err" style="width:150px"><label>维护人</label><input value="张"><span class="msg">请输入完整姓名</span></div></div>'],
|
||||
['表头 / 行', t => '<div class="demo" data-theme="' + t + '"><table class="tbl" style="width:290px"><thead><tr><th>房源编码</th><th>状态</th></tr></thead><tbody><tr><td><span class="code">SH-...037</span></td><td><span class="tag ok">有效</span></td></tr><tr class="sel"><td><span class="code">SH-...052</span></td><td><span class="tag gold">独家</span></td></tr></tbody></table></div>'],
|
||||
['分页 / 页签', t => '<div class="demo" data-theme="' + t + '"><div class="tabs" style="padding:0"><a class="on">全部</a><a>有效</a></div><div class="pager" style="padding:9px 0"><span class="pg on">1</span><span class="pg">2</span><span class="pg">3</span></div></div>'],
|
||||
['提示条', t => '<div class="demo" data-theme="' + t + '"><div class="al w"><span>⚠</span><span>钥匙编号不一致,请核对。</span></div></div>']
|
||||
];
|
||||
|
||||
const posOf = t => THEMES.findIndex(x => x.id === t) + 2; // 第 1 列是元素名
|
||||
|
||||
const html = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8">
|
||||
<title>甄房业务系统 · 色彩体系选型对比(Carbon / Fiori / Ant Design)</title>
|
||||
<style>${CSS}</style></head><body><div class="wrap">
|
||||
|
||||
<h1>甄房业务系统 · <b>色彩体系选型对比</b></h1>
|
||||
<div class="lead">
|
||||
面向「经纪人高频长时使用」的业务平台,从全球主流 SaaS / ERP 设计系统中筛选出综合评分最高的三套,
|
||||
各自结合已定稿的 LOGO(框内 Forte Z + 甄房墨蓝 <code>#0C2340</code>)开发完整界面色彩方案。<br>
|
||||
三套方案的界面结构完全相同,只替换色彩令牌 —— 保证对比公平。<b>所有对比度数值由页面运行时真实计算</b>,非人工填写。
|
||||
</div>
|
||||
|
||||
<h2>一、调研结论<span>数据来源:三家官方设计系统文档</span></h2>
|
||||
<p class="note">筛选维度与权重:数据密度适配 25% | 长时使用舒适度 20% | 无障碍可推导性 15% | 品牌融合度 15% | 落地迁移成本 15% | 行业验证广度 10%。<br>
|
||||
注意:「无障碍」一列评的是<b>体系提供了多少可推导的对比度规则</b>,与默认色盘的实际通过率并不同向 —— Carbon 规则最硬,但它把 Yellow 30 定位成警报条底色,被当文字色用就会不达标;AntD 规则最少,默认色盘实测反而是问题最多的。所以下面每一套都给出<b>实测通过率</b>与<b>生产落地必改项</b>。</p>
|
||||
<table class="cmp">
|
||||
<thead><tr><th>体系</th><th>定位与代表产品</th><th class="num">密度25%</th><th class="num">舒适20%</th><th class="num">无障碍15%</th><th class="num">品牌15%</th><th class="num">成本15%</th><th class="num">验证10%</th><th class="num">加权总分</th></tr></thead>
|
||||
<tbody>
|
||||
${THEMES.map(t => `<tr><td><b>${t.sys}</b></td><td style="color:#98a1b3">${
|
||||
t.id === 'carbon' ? 'IBM 全线产品 · 数据密集型企业级标杆'
|
||||
: t.id === 'fiori' ? 'SAP S/4HANA、Ariba 等 ERP 全线'
|
||||
: '阿里/蚂蚁中后台生态 · 国内事实标准'}</td>
|
||||
${['dense','comfort','a11y','brand','cost','proven'].map(k => '<td class="num">' + t.score[k].toFixed(1) + '</td>').join('')}
|
||||
<td class="num tot">${t.total.toFixed(2)}</td></tr>`).join('')}
|
||||
</tbody></table>
|
||||
<div class="src">
|
||||
依据:Carbon 官方明确「Neutral Gray family is dominant」并规定「避免使用中间调」,Blue 60 <code>#0f62fe</code> 为全局 primary action,四套官方主题(White / Gray 10 / Gray 90 / Gray 100);<br>
|
||||
Fiori Horizon 提供 S0–S8 表面、T1–T9 文字、边框、语义、强调五组语义令牌,并给出「相隔 4 级 = 3:1、相隔 5 级 = 4.5:1」的可推导对比度规则;<br>
|
||||
Ant Design 5 以 <code>colorPrimary #1677ff</code> 为种子令牌,色板由算法生成 12 色 × 10 阶,第 6 阶满足 WCAG 4.5:1。<br>
|
||||
参考:<a href="https://carbondesignsystem.com/elements/color/tokens/">Carbon Color Tokens</a> ·
|
||||
<a href="https://experience.sap.com/fiori-design-web/v1-110/theming/">SAP Fiori Theming</a> ·
|
||||
<a href="https://ant-design.antgroup.com/docs/spec/colors-cn">Ant Design 色彩</a>
|
||||
</div>
|
||||
|
||||
<h2>二、三套方案的完整界面</h2>
|
||||
<p class="note">
|
||||
核心策略:<b>品牌色与交互色分离</b> —— 三套方案都把甄房墨蓝 <code>#0C2340</code> 固定在品牌/导航区(侧栏、Shell Bar、登录页),
|
||||
交互色各自沿用体系的官方主色(<code>#0F62FE</code> / <code>#0070F2</code> / <code>#1677FF</code>)。
|
||||
这样既保住品牌识别,又拿到该体系已验证的交互反馈与无障碍保障。
|
||||
</p>
|
||||
<div class="sumline" id="wcag-summary"></div>
|
||||
${THEMES.map(t => `
|
||||
<div class="plan${t.final ? ' final' : ''}">
|
||||
<div class="hd"><h3>${t.name}</h3><span class="sys">${t.sys}</span>${t.final ? '<span class="finalmark">✓ 已定稿</span>' : ''}<span class="sc">加权 ${t.total.toFixed(2)} / 10</span></div>
|
||||
<div class="d">${t.desc}</div>
|
||||
<div class="demo" data-theme="${t.id}">${demoBody()}</div>
|
||||
|
||||
<h3>色彩令牌</h3>
|
||||
<div class="tok">
|
||||
${t.tags.map(([n, v, r]) => '<div class="tk"><i style="background:' + v + '"></i><span class="n"><b>' + n + '</b><code>' + v + '</code></span><span class="r" style="color:#7f8899;border-color:rgba(255,255,255,.14)">' + r + '</span></div>').join('')}
|
||||
</div>
|
||||
|
||||
<h3>对比度实测(WCAG,AA 正文 4.5:1 / AA 大字与图形 3:1)</h3>
|
||||
<div class="tok" id="wcag-${t.id}"></div>
|
||||
|
||||
<div class="pc">
|
||||
<div><h4 style="color:#5ee08a">优点</h4><ul class="good">${t.pros.map(x => '<li>' + x + '</li>').join('')}</ul></div>
|
||||
<div><h4 style="color:#f0a55c">代价与不足</h4><ul class="bad">${t.cons.map(x => '<li>' + x + '</li>').join('')}</ul></div>
|
||||
</div>
|
||||
<div class="pc" style="grid-template-columns:1fr"><div><h4 style="color:#8ab4f8">适用场景</h4><ul class="fit">${t.fit.map(x => '<li>' + x + '</li>').join('')}</ul></div></div>
|
||||
|
||||
<h3>生产落地必改项<span style="font-size:11px;font-weight:400;color:#8b94a7;margin-left:8px">依据上表实测结果</span></h3>
|
||||
${t.fixes.length
|
||||
? '<table class="cmp fix"><thead><tr><th>用途</th><th>原值</th><th>建议改为</th><th>理由</th></tr></thead><tbody>' +
|
||||
t.fixes.map(f => '<tr><td><b>' + f[0] + '</b></td><td><code>' + f[1] + '</code></td><td><code>' + f[2] +
|
||||
'</code></td><td style="color:#98a1b3">' + f[3] + '</td></tr>').join('') + '</tbody></table>'
|
||||
: '<div class="fixok">本方案 12 项对比度检查全部达标,无需为无障碍做任何色值改动 —— 三套中唯一「开箱即合规」的方案。</div>'}
|
||||
</div>`).join('')}
|
||||
|
||||
<h2>三、主要界面元素横向对照<span>同一元素 · 三套色彩并排</span></h2>
|
||||
<div class="elcmp"><table class="el">
|
||||
<thead><tr><th>元素</th>${THEMES.map(t => '<th>' + t.name.split('·')[1].trim() + '</th>').join('')}</tr></thead>
|
||||
<tbody>
|
||||
${EL_ROWS.map(([name, fn]) => '<tr><td>' + name + '</td>' + THEMES.map(t => '<td>' + fn(t.id) + '</td>').join('') + '</tr>').join('')}
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h2>四、高频长时使用的统一优化层<span>三套方案共用</span></h2>
|
||||
<p class="note">这是针对「经纪人每天连续使用 6-8 小时」这一前提,在三套体系之上叠加的四条约束。三套方案均已落实。</p>
|
||||
<table class="cmp">
|
||||
<thead><tr><th>优化项</th><th>做法</th><th>解决什么</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><b>深色品牌侧栏</b></td><td>侧栏统一用 <code>#0C2340</code> 而非浅灰</td><td>减少整屏亮度、形成稳定视觉锚点;浅灰侧栏在长时间使用中会与内容区「糊在一起」,找不到导航边界</td></tr>
|
||||
<tr><td><b>斑马纹极低对比</b></td><td>条纹底色与白底差值控制在 2–3%(<code>#F4F4F4</code>/<code>#F5F6F7</code>/<code>#F5F5F5</code>)</td><td>帮助横向追行不串行,但不会产生条纹眩光;这是数据表最高频的疲劳源</td></tr>
|
||||
<tr><td><b>语义色降饱和</b></td><td>状态标签用「描边+浅底」而非实心色块,浅底透明度压到 8–12%</td><td>一屏内可能有几十个状态标签,实心高饱和色块是最大的刺眼来源</td></tr>
|
||||
<tr><td><b>金色只做点</b></td><td>品牌金 <code>#C6A15B</code> 只用于 logo、关键数字、选中指示条,占比 ≤5%</td><td>保住品牌质感,同时避免金色大面积铺陈显廉价</td></tr>
|
||||
</tbody></table>
|
||||
|
||||
<h2>五、选型建议</h2>
|
||||
<p class="note"><b style="color:#5ee08a">本次结论:已定稿 方案 B · Fiori 商务派</b>(2026-09-19)。定稿理由:三套中唯一 12/12 对比度开箱合规、语义令牌分层最完整、选中态用 8% 蓝底而非实色块(长时使用最不刺眼),且官方 Quick Theming 就是「替换 logo + 主品牌色」,与已定稿的甄房墨蓝最契合。下表保留完整选型逻辑,供后续复盘与其他产品线参考。</p>
|
||||
<table class="cmp">
|
||||
<thead><tr><th>如果……</th><th>建议</th><th>理由</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>团队人少、迭代快,希望最快落地并降低长期维护成本</td><td><b>方案 C · AntD 亲和派</b></td><td>迁移成本最低、中文生态最全;代价是要按「生产落地必改项」把 6 处色值各下移一阶(改完视觉几乎无差别,但差别在无障碍上)</td></tr>
|
||||
<tr><td>需要严谨的层级与无障碍,且能接受工程感外观</td><td><b>方案 A · Carbon 严谨派</b></td><td>灰阶分层最严谨、对比度有官方硬约束;数据密度最高。仅「黄色被当文字色」与「圆角偏硬」两处需按建议调整</td></tr>
|
||||
<tr><td>交易与佣金等强流程模块、或需与财务/ERP 打通</td><td><b>方案 B · Fiori 商务派</b></td><td>状态与选中态表达最清楚,对比度规则可推导,符合企业级规范;三套中唯一 12/12 开箱合规</td></tr>
|
||||
<tr><td>想要「一次到位」但不想赌</td><td><b>混合:Carbon 骨架 + AntD 语义色</b></td><td>用 Carbon 的灰阶分层与密度打底,语义色采用国内用户熟悉的 AntD 色相并降饱和(沿用 C 的必改项色值);这是我推荐的落地路径</td></tr>
|
||||
</tbody></table>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
// ── 令牌数据(由 Node 侧注入)──
|
||||
var THEMES = ${JSON.stringify(THEMES.map(t => ({ id: t.id, name: t.name, sys: t.sys, total: t.total })))};
|
||||
// ── 真实计算 WCAG 对比度 ──────────────────────────────────────
|
||||
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);
|
||||
}
|
||||
function blend(fg, bg, alpha){
|
||||
var f = toRgb(fg), b = toRgb(bg);
|
||||
return '#' + f.map(function(x,i){ return Math.round(x*alpha + b[i]*(1-alpha)).toString(16).padStart(2,'0'); }).join('');
|
||||
}
|
||||
var PAIRS = [
|
||||
['一级文字 / 白底', 'text1', '#FFFFFF', 4.5],
|
||||
['二级文字 / 白底', 'text2', '#FFFFFF', 4.5],
|
||||
['三级文字 / 白底', 'text3', '#FFFFFF', 4.5],
|
||||
['一级文字 / 次级底', 'text1', 'bg2', 4.5],
|
||||
['白字 / 交互主色(按钮)', '#FFFFFF', 'primary', 4.5],
|
||||
['主色文字 / 白底(链接)', 'primary', '#FFFFFF', 4.5],
|
||||
['成功 / 白底', 'ok', '#FFFFFF', 3],
|
||||
['警告 / 白底', 'warn', '#FFFFFF', 3],
|
||||
['错误 / 白底', 'err', '#FFFFFF', 3],
|
||||
['错误 / 白底(正文)', 'err', '#FFFFFF', 4.5],
|
||||
['白字 / 品牌墨蓝(侧栏)', '#FFFFFF', 'brand', 4.5],
|
||||
['侧栏二级字 / 品牌墨蓝', null, 'brand', 4.5, '#B9C2D0']
|
||||
];
|
||||
var SUMMARY = [];
|
||||
THEMES.forEach(function(t){
|
||||
var el = document.getElementById('wcag-' + t.id);
|
||||
if (!el) return;
|
||||
var dummy = document.querySelector('.demo[data-theme="' + t.id + '"]');
|
||||
var cs = getComputedStyle(dummy);
|
||||
var val = function(k){ return k.charAt(0) === '#' ? k : cs.getPropertyValue('--' + k).trim(); };
|
||||
var out = [], bad = [];
|
||||
PAIRS.forEach(function(p){
|
||||
var fg = p[4] ? p[4] : val(p[1]); // p[4] 为直接色值覆盖(可选)
|
||||
var bg = val(p[2]);
|
||||
if (bg.indexOf('rgba') === 0) { bg = blend(bg, '#FFFFFF', parseFloat(bg.split(',')[3])); }
|
||||
var r = ratio(fg, bg);
|
||||
var req = p[3];
|
||||
if (r < req) bad.push(p[0] + ' ' + r.toFixed(2) + ':1');
|
||||
out.push('<div class="tk"><i style="background:' + bg + ';border-color:rgba(255,255,255,.2)"></i>' +
|
||||
'<span class="n"><b>' + p[0] + '</b><code>' + r.toFixed(2) + ':1 要求 ≥' + req + ':1</code></span>' +
|
||||
'<span class="r ' + (r >= req ? 'pass' : 'fail') + '">' + (r >= req ? '通过' : '未达标') + '</span></div>');
|
||||
});
|
||||
el.innerHTML = out.join('');
|
||||
SUMMARY.push({ id: t.id, name: t.name, sys: t.sys, total: t.total, pass: PAIRS.length - bad.length, n: PAIRS.length, bad: bad });
|
||||
});
|
||||
var sumEl = document.getElementById('wcag-summary');
|
||||
if (sumEl) {
|
||||
sumEl.innerHTML = SUMMARY.map(function(s){
|
||||
return '<div><b>' + s.sys + '</b> WCAG 实测 <em class="' + (s.bad.length ? 'no' : 'ok') + '">' +
|
||||
s.pass + ' / ' + s.n + ' 通过</em>' + (s.bad.length ? ',需修正:' + s.bad.join('、') : ',全部达标') + '</div>';
|
||||
}).join('');
|
||||
}
|
||||
</script>
|
||||
</body></html>`;
|
||||
|
||||
fs.writeFileSync(path.join(HERE, 'index.html'), html);
|
||||
console.log('已生成 theme/index.html ·', THEMES.length, '套方案');
|
||||
THEMES.forEach(t => console.log(' ', t.name, '加权', t.total.toFixed(2)));
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,95 @@
|
||||
// 登录页流星轨迹验证
|
||||
// 目的:确认「流星进入视口期间始终不透明」,且「透明度开始变化时整条流星已在视口外」
|
||||
// 用法: node _design-test/verify-meteor.js [视口宽] [视口高] [试验次数]
|
||||
const VW = Number(process.argv[2] || 1280)
|
||||
const VH = Number(process.argv[3] || 720)
|
||||
const N = Number(process.argv[4] || 20000)
|
||||
|
||||
// 动画时间轴(与 login.vue 的 @keyframes meteor-shower 一致)
|
||||
// 0% 透明度 0 → 0.5% 变 1 → 60% 仍为 1 → 66.7% 变 0 → 100% 保持 0
|
||||
// 位移在 0%~60% 内从起点线性走到终点,60% 之后停在终点
|
||||
const FLY_END = 0.60 // 飞行结束(到达终点)的进度
|
||||
const FADE_IN_END = 0.005 // 淡入完成的进度(0.5%)
|
||||
const FADE_OUT_START = 0.60 // 淡出开始的进度
|
||||
|
||||
const inside = (x, y) => x >= 0 && x <= VW && y >= 0 && y <= VH
|
||||
|
||||
// 判断「头部 + 拖尾」这条线段是否与视口相交(沿线采样)
|
||||
function streakVisible(hx, hy, ux, uy, tail) {
|
||||
for (let i = 0; i <= 40; i++) {
|
||||
const s = (i / 40) * tail
|
||||
if (inside(hx - ux * s, hy - uy * s)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function headAt(c) { return Math.min(c / FLY_END, 1) } // 进度 -> 路径参数
|
||||
|
||||
function build(variant) {
|
||||
const tail = 90 + Math.random() * 70
|
||||
const margin = tail + 60
|
||||
const theta = (25 + Math.random() * 30) * Math.PI / 180
|
||||
const ux = -Math.cos(theta), uy = Math.sin(theta)
|
||||
|
||||
if (variant === 'old') {
|
||||
// 旧实现:起终点都取在视口内部
|
||||
const sx = VW * (0.50 + Math.random() * 0.50)
|
||||
const sy = VH * (Math.random() * 0.35)
|
||||
const ex = VW * (Math.random() * 0.30)
|
||||
const ey = VH * (0.85 + Math.random() * 0.20)
|
||||
return { sx, sy, tx: ex - sx, ty: ey - sy, ux: (sx - ex), uy: (sy - ey), tail, margin }
|
||||
}
|
||||
// 新实现
|
||||
let sx, sy
|
||||
if (Math.random() < 0.5) { sx = VW * (0.45 + Math.random() * 0.65); sy = -margin }
|
||||
else { sx = VW + margin; sy = VH * (0.10 + Math.random() * 0.40) }
|
||||
const t = Math.min((VH + margin - sy) / uy, (-margin - sx) / ux)
|
||||
return { sx, sy, tx: ux * t, ty: uy * t, ux, uy, tail, margin }
|
||||
}
|
||||
|
||||
function analyze(variant) {
|
||||
let worstFadeWhileVisible = 0 // 可见期间透明度掉到 1 以下的最大程度(0 = 完美)
|
||||
let maxVisibleAtFlyEnd = 0 // 飞行结束时仍可见的比例
|
||||
let minEnter = 1, maxEnter = 0, minExit = 1, maxExit = 0
|
||||
const visAt = []
|
||||
for (let n = 0; n < N; n++) {
|
||||
const m = build(variant)
|
||||
const ulen = Math.hypot(m.ux, m.uy) || 1
|
||||
const ux = m.ux / ulen, uy = m.uy / ulen // 单位化的「拖尾指向」
|
||||
let enter = -1, exit = -1
|
||||
for (let i = 0; i <= 600; i++) {
|
||||
const c = i / 600
|
||||
const f = headAt(c)
|
||||
const hx = m.sx + m.tx * f, hy = m.sy + m.ty * f
|
||||
const vis = streakVisible(hx, hy, ux, uy, m.tail)
|
||||
if (vis && enter < 0) enter = c
|
||||
if (vis) exit = c
|
||||
}
|
||||
if (enter < 0) continue
|
||||
visAt.push(enter)
|
||||
if (enter < minEnter) minEnter = enter
|
||||
if (enter > maxEnter) maxEnter = enter
|
||||
if (exit < minExit) minExit = exit
|
||||
if (exit > maxExit) maxExit = exit
|
||||
// 可见窗口内透明度是否恒为 1
|
||||
if (enter < FADE_IN_END) worstFadeWhileVisible = Math.max(worstFadeWhileVisible, FADE_IN_END - enter)
|
||||
if (exit > FADE_OUT_START) worstFadeWhileVisible = Math.max(worstFadeWhileVisible, exit - FADE_OUT_START)
|
||||
maxVisibleAtFlyEnd = Math.max(maxVisibleAtFlyEnd, Math.max(0, exit - FADE_OUT_START))
|
||||
}
|
||||
visAt.sort((a, b) => a - b)
|
||||
const med = visAt[Math.floor(visAt.length / 2)] || 0
|
||||
return { minEnter, maxEnter, medEnter: med, minExit, maxExit, worstFadeWhileVisible, maxVisibleAtFlyEnd, count: visAt.length }
|
||||
}
|
||||
|
||||
for (const v of ['old', 'new']) {
|
||||
const r = analyze(v)
|
||||
const f1 = n => (n * 100).toFixed(1)
|
||||
console.log((v === 'old' ? '【旧】' : '【新】') + ' 实现 视口 ' + VW + 'x' + VH + ' 有效样本 ' + r.count)
|
||||
console.log(' 可见区间:进入 ' + f1(r.minEnter) + '%~' + f1(r.maxEnter) + '% 离开 ' +
|
||||
f1(r.minExit) + '%~' + f1(r.maxExit) + '%')
|
||||
console.log(' 可见期间透明度不足的最大窗口:' + f1(r.worstFadeWhileVisible) + '% of 周期' +
|
||||
(r.worstFadeWhileVisible <= 0.0001 ? ' ✓ 无(可见时恒为不透明)' : ' ← 会在屏内淡入/淡出'))
|
||||
console.log(' 飞行结束时仍未出屏的量:' + f1(r.maxVisibleAtFlyEnd) + '%' +
|
||||
(r.maxVisibleAtFlyEnd <= 0.0001 ? ' ✓ 整条流星已出屏' : ' ← 未出屏就已淡出'))
|
||||
console.log('')
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
@import 'e:/KF_7/ZhenOS-ZF/ruoyi-ui/src/assets/styles/variables.scss';
|
||||
@import 'e:/KF_7/ZhenOS-ZF/ruoyi-ui/src/assets/styles/mixin.scss';
|
||||
@import 'e:/KF_7/ZhenOS-ZF/ruoyi-ui/src/assets/styles/theme.scss';
|
||||
@import 'e:/KF_7/ZhenOS-ZF/ruoyi-ui/src/assets/styles/sidebar.scss';
|
||||
@import 'e:/KF_7/ZhenOS-ZF/ruoyi-ui/src/assets/styles/btn.scss';
|
||||
@import 'e:/KF_7/ZhenOS-ZF/ruoyi-ui/src/assets/styles/ruoyi.scss';
|
||||
@import 'e:/KF_7/ZhenOS-ZF/ruoyi-ui/src/assets/styles/element-ui.scss';
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user