@@ -3,111 +3,705 @@
*
* 模版是**内容不是代码**:这里产出的一律是无脚本、无事件处理器、无外链的静态 HTML;
* 数据由基座在推送时填(`data-slot` 取值、`data-rows` 逐条展开)。
* 容器是沙箱 iframe(`sandbox=""`),因此**样式必须自带** —— 页面不继承工作台的 CSS。
* 容器是沙箱 iframe(`sandbox=""`),因此**样式必须自带** —— 页面不继承工作台的 CSS,
* 也**不能引外部字体**(净化门拦 `src`/`href` 的 `//`),所以只用系统字族。
*
* ── 版面取正式汇报件(2026-07-31 重做,对齐 PRD 原型 `jc-report`)────────────
*
* 从前这里是「一个色条 + 几张灰卡」:能读,但不像一份**交得出去的件**。
* 人在这一屏上要签字、要拿去对账,它就该长成一份汇报件的样子:
*
* 题头(图标徽章 · 类型行 · 标题 · 状态章)
* → 元信息一行(编号 / 客户 / 编制 …,竖线分隔,不做格子)
* → 关键指标带(判据摊上台面,洗蓝渐变格)
* → 编号章节(一 / 二 / 三,方徽 + 标题 + 细线 + 右侧结论签)
* → 落款(署名 + 时刻)
*
* **色彩纪律:结构用品牌蓝,数据才用语义色。** 整屏只有一处渐变(题头徽章),
* 绿/橙/红只出现在「这个数达标没达标」上 —— 满屏彩色等于没有重点。
*
* 图标一律**内联 SVG**(lucide 线形,1.75 描边):沙箱里取不到图标字体,
* 而 emoji 当图标是另一回事 —— 它在不同系统上是不同的画,还带情绪。
*
* 六个活动共用这一套外壳:汇报面貌一致,不是每个活动各自发明一套。
*/
export type Tone = 'brand' | 'ok' | 'warn' | 'danger' | 'case' | 'archive' ;
/** 主色一档一个 —— 题头徽章与结构线取它 */
const ACCENT : Readonly < Record < Tone , string > > = {
brand : '#3370ff' ,
ok : '#0aa294 ' ,
warn : '#ff8 800 ' ,
danger : '#e5484d ' ,
ok : '#278717 ' ,
warn : '#de7 802 ' ,
danger : '#d83931 ' ,
case : '#7f3bf5' ,
archive : '#46586a' ,
} ;
/** 一套克制的排版;所有尺寸用相对单位,窄栏也不横向溢出 */
const BASE_CSS = ( accent : string ) : string = > `
:root { color-scheme: light; }
/** 题头徽章的渐变收尾色 —— 与 PRD 的 `--ai-grad` 同一手法(蓝 → 紫) */
const ACCENT_B : Readonly < Record < Tone , string > > = {
brand : '#7f3bf5' ,
ok : '#0aa294' ,
warn : '#f54a45' ,
danger : '#a3231d' ,
case : '#3370ff' ,
archive : '#6a7590' ,
} ;
/* ==================== 图标 ==================== */
/**
* 内联 lucide 线形图标 —— **只收这套模版真正用得上的**。
*
* 不做成「全量图标库」:模版是随包冻结的字符串,塞一百个图标就是给每一份报告
* 都背上一百个 path。要新图标就往这里加一条,加的时候顺手想想是不是真的需要。
*/
const ICONS : Readonly < Record < string , string > > = {
'file-text' :
'<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M16 13H8"/><path d="M16 17H8"/><path d="M10 9H8"/>' ,
'list-checks' :
'<path d="M3 5h1l1.5 1.5L8 4"/><path d="M3 12h1l1.5 1.5L8 10"/><path d="M3 19h1l1.5 1.5L8 17"/><path d="M12 5h9"/><path d="M12 12h9"/><path d="M12 19h9"/>' ,
database :
'<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.7 4 3 9 3s9-1.3 9-3V5"/><path d="M3 12c0 1.7 4 3 9 3s9-1.3 9-3"/>' ,
search : '<circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>' ,
coins :
'<circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/><path d="M7 6h1v4"/><path d="m16.71 13.88.7.71-2.82 2.82"/>' ,
handshake :
'<path d="m11 17 2 2a1 1 0 1 0 3-3"/><path d="m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.9-3.9a2 2 0 0 1 0-2.8L17 5"/><path d="M21 3 9.6 14.4a2 2 0 0 1-2.8 0l-2.4-2.4a2 2 0 0 1 0-2.8L7 6"/>' ,
archive :
'<rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/>' ,
'shield-check' : '<path d="M20 13c0 5-3.5 7.5-8 9-4.5-1.5-8-4-8-9V6l8-3 8 3Z"/><path d="m9 12 2 2 4-4"/>' ,
'alert-triangle' :
'<path d="m21.7 18-8-14a2 2 0 0 0-3.5 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>' ,
'circle-check' : '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>' ,
mail : '<rect width="20" height="16" x="2" y="4" rx="2"/><path d="m2 7 8.9 5.3a2 2 0 0 0 2.2 0L22 7"/>' ,
workflow :
'<rect width="8" height="8" x="3" y="3" rx="2"/><path d="M7 11v4a2 2 0 0 0 2 2h4"/><rect width="8" height="8" x="13" y="13" rx="2"/>' ,
package :
'<path d="m7.5 4.27 9 5.15"/><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>' ,
gauge : '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>' ,
users :
'<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>' ,
send : '<path d="M22 2 11 13"/><path d="M22 2 15 22l-4-9-9-4Z"/>' ,
'mail-open' :
'<path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0z"/><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"/>' ,
building :
'<path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"/><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"/><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"/><path d="M10 6h4"/><path d="M10 10h4"/><path d="M10 14h4"/>' ,
user : '<circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/>' ,
history : '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>' ,
boxes :
'<path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/>' ,
sheet :
'<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M8 13h2"/><path d="M14 13h2"/><path d="M8 17h2"/><path d="M14 17h2"/>' ,
'git-branch' : '<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>' ,
} ;
/** 一枚图标 —— 认不出的名字回一个空盒,不摆一个错的图标充数 */
function ico ( name : string , size = 14 ) : string {
const path = ICONS [ name ] ;
if ( path === undefined ) return '' ;
return ` <svg class="ic" width=" ${ size } " height=" ${ size } " viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"> ${ path } </svg> ` ;
}
/* ==================== 骨架样式 ==================== */
/** 版面取正式汇报件;尺寸一律相对单位,窄栏也不横向溢出 */
const BASE_CSS = ( a : string , b : string ) : string = > `
/*
* ── 版面**照 PRD 目标件的数值抄**(2026-08-01 重做)────────────────────
*
* ~/coding/JCAI_PRD/docs/活动执行页/ 那两份是这个活动「办完该长什么样」的答案。
* 上一版我把「块」都搬齐了,却用了自己那套外壳:题头下多一段灰字摘要、
* 多一条元信息带、章节头是彩色方块编号 + 大标题、字号用 clamp 响应式。
* 结果是**块齐了、版面是两套**,用户一眼就说「跟我给你的完全不一样」。
*
* 所以这一版把数值逐条抄过来:题头 16/20/13、章节头 11px/800/.12em + 细线 + 右签、
* 正文 12px、表头 10.5px、落款 13/20/15。**字号一律固定** ——
* clamp 会让同一份件在不同宽度下长得不一样,而目标件只有一个样子。
*/
:root {
color-scheme: light;
--a: ${ a } ; --a-b: ${ b } ;
--a-wash: color-mix(in srgb, ${ a } 7%, #fff);
--a-soft: color-mix(in srgb, ${ a } 20%, #fff);
--brand: #3370ff; --brand-soft: #e1eaff; --brand-wash: #f0f4ff;
--ink: #1f2329; --ink-2: #41464e; --ink-3: #646a73; --ink-4: #8f959e;
--line: #dee0e3; --line-soft: #e8eaed; --soft: #f8f9fb; --cardline: #e1e6ec;
--ok: #278717; --ok-soft: #e8f8e4; --ok-line: #bfedb4;
--warn: #de7802; --warn-accent: #ff8800; --warn-soft: #fff4e5;
--danger: #d83931; --danger-soft: #feeae9; --danger-line: #f9c1be;
--teal: #0aa294;
--mono: ui-monospace, "SF Mono", SFMono-Regular, "JetBrains Mono", Menlo, Consolas, monospace;
--shadow-brand: 0 6px 16px rgba(51,112,255,.22);
}
* { box-sizing: border-box; }
/*
* **报告本体就是工作区** —— 不在里面再画一张居中的小卡片。
* 容器(.jc-view-frame)已经是 100% 宽、白底、带圆角的那张卡。
*/
body {
margin: 0; padding: 18px ;
font: 13px/1.6 -apple-system, "PingFang SC", "Noto Sans SC", "Segoe UI ", sans-serif;
color: #1f2933 ; background: #f7f8fa ;
margin: 0; padding: 0 ;
font: 13px/1.6 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC ", sans-serif;
color: var(--ink) ; background: #fff ;
-webkit-font-smoothing: antialiased;
}
.rp { width: 100%; background: #fff; overflow: hidden; }
.ic { flex: none; vertical-align: -.14em; }
/* ── 题头:徽章 + 类型行 + 标题 + 状态;下缘粗细双线 = 正式件 ── */
.hd { position: relative; overflow: hidden; display: flex; align-items: flex-start; gap: 12px;
padding: 16px 20px 13px; border-bottom: 2px solid var(--ink); }
.hd::after { content: ""; position: absolute; left: 0; right: 0; bottom: -4px; height: 1px; background: var(--line); }
.hd::before { content: ""; position: absolute; right: -70px; top: -96px; width: 240px; height: 240px; border-radius: 999px;
background: radial-gradient(circle, color-mix(in srgb, var(--a) 10%, transparent), transparent 70%); pointer-events: none; }
.hd > * { position: relative; z-index: 1; }
.hd .bdg { flex: none; width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center;
color: #fff; background: linear-gradient(135deg, var(--a), var(--a-b)); box-shadow: var(--shadow-brand); }
.hd .tt { flex: 1; min-width: 0; }
.hd .kick { font-size: 10px; font-weight: 800; letter-spacing: .08em; color: var(--ink-4); font-family: var(--mono); }
.hd h1 { margin: 4px 0 0; font-size: 18px; font-weight: 800; letter-spacing: -.01em; line-height: 1.3; overflow-wrap: anywhere; }
.hd h1 .ref { font-family: var(--mono); font-size: .88em; letter-spacing: -.02em; }
.hd .stamp { flex: none; margin-top: 2px; transform: rotate(-6deg); border: 2px solid currentColor;
border-radius: 9px; padding: 5px 9px 4px 14px; font-size: 14px; font-weight: 900;
letter-spacing: .3em; color: var(--ok); opacity: .9; }
.hd .stamp.bad { color: var(--danger); }
.hd .live { display: inline-flex; align-items: center; gap: 6px; flex: none; margin-top: 4px;
padding: 4px 12px; border-radius: 999px; font-size: 11px; font-weight: 700;
color: var(--a); background: var(--a-wash); border: 1px solid var(--a-soft); }
/* ── 正文区与章节头:**小标签 + 细线 + 右端小签**,没有编号方块 ── */
.bd { padding: 18px 20px 12px; }
.sh { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 9px; margin: 22px 0 11px; }
.s:first-child .sh { margin-top: 0; }
.sh b { flex: none; font-size: 11px; font-weight: 800; letter-spacing: .12em; color: var(--ink-3); }
.sh .ln { flex: 1; height: 1px; background: var(--line-soft); }
.sh .aside { flex: none; font-size: 10px; font-weight: 700; color: var(--ink-4); font-family: var(--mono);
background: var(--soft); border-radius: 999px; padding: 2px 9px; white-space: nowrap; }
.sh .aside.ok { color: var(--ok); background: var(--ok-soft); }
.sh .aside.bad { color: var(--danger); background: var(--danger-soft); }
/* ── 关键指标带(要用才用;两份目标件都没有)── */
.stats { display: flex; flex-wrap: wrap; gap: 1px; background: var(--line-soft); border: 1px solid var(--line-soft); border-radius: 10px; overflow: hidden; }
.stat { flex: 1 1 140px; min-width: 0; padding: 10px 14px 11px; background: #fff; }
.stat i { display: block; font-style: normal; font-size: 9.5px; font-weight: 800; letter-spacing: .09em; color: var(--ink-4); }
.stat b { display: block; margin-top: 3px; font-size: 14px; font-weight: 800; line-height: 1.5; overflow-wrap: anywhere; }
/* ── 来件原文:青色左脊 = 收进来的东西 ── */
.lt { border: 1px solid var(--cardline); border-left: 3px solid var(--teal); border-radius: 12px;
padding: 14px 16px 15px; background: #fff; box-shadow: 0 1px 2px rgba(31,35,41,.05); }
.lt .lh { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 11px; }
.lt .dir { display: inline-flex; align-items: center; gap: 5px; font-size: 10.5px; font-weight: 800;
padding: 3px 9px; border-radius: 999px; background: #e4f8f5; color: #0a7f74; }
.lt .lh .own { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 999px;
background: var(--brand-wash); border: 1px solid var(--brand-soft); font-size: 10.5px; color: #245bdb; }
.lt .lh .own b { font-weight: 600; }
.lt .lh .own i { font-style: normal; opacity: .55; }
.lt .lh .own:has(b:empty) { display: none; }
.lt .lh .at { margin-left: auto; font-family: var(--mono); font-size: 10.5px; color: var(--ink-4); }
.lt .lm { display: flex; flex-direction: column; gap: 4px; margin-bottom: 11px; padding-bottom: 11px;
border-bottom: 1px solid var(--line-soft); font-size: 11.5px; color: var(--ink-2); }
.lt .lm p { margin: 0; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.lt .lm i { flex: none; font-style: normal; width: 44px; font-size: 10px; font-weight: 800; letter-spacing: .06em; color: var(--ink-4); }
.lt .lm b { font-weight: 400; font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); overflow-wrap: anywhere; }
.lt h4 { margin: 0 0 9px; font-size: 14px; font-weight: 800; line-height: 1.45; overflow-wrap: anywhere; }
/* 正文原文照录:pre-wrap 保原始换行,不截断、不折叠 */
.lt .lb { margin: 0; font: inherit; font-size: 12.5px; line-height: 1.72; color: var(--ink-2);
white-space: pre-wrap; overflow-wrap: anywhere; }
.lt .atts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; padding-top: 11px; border-top: 1px dashed var(--line-soft); }
.lt .att { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px 5px 8px; border: 1px solid var(--brand-soft);
border-radius: 8px; background: var(--brand-wash); font-size: 11.5px; color: #245bdb; }
.lt .att b { font-family: var(--mono); font-weight: 400; font-size: 10.5px; }
.lt .att span { color: var(--ink-4); }
/* ── 要素卡:顶缘色棱 + 右上光晕 ── */
.facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 11px; }
.fact { position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; gap: 10px; min-width: 0;
padding: 14px; border: 1px solid var(--cardline); border-radius: 13px; background: #fff;
box-shadow: 0 1px 2px rgba(31,35,41,.05), 0 8px 20px -14px rgba(31,35,41,.35);
--t: var(--brand); --t-soft: rgba(51,112,255,.13); }
.fact::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 3px; background: var(--t); }
.fact::after { content: ""; position: absolute; right: -46px; top: -52px; width: 132px; height: 132px; border-radius: 999px;
background: radial-gradient(circle, var(--t-soft), transparent 70%); pointer-events: none; }
.fact > * { position: relative; z-index: 1; }
.fact.t-teal { --t: var(--teal); --t-soft: rgba(10,162,148,.15); }
.fact.t-ok { --t: var(--ok); --t-soft: rgba(39,135,23,.14); }
.fact.t-warn { --t: var(--warn-accent); --t-soft: rgba(255,136,0,.12); }
.fact .k { display: flex; align-items: center; gap: 6px; font-size: 9.5px; font-weight: 800; letter-spacing: .12em; color: var(--t); }
.fact .k i { flex: none; width: 19px; height: 19px; border-radius: 6px; display: inline-grid; place-items: center;
background: var(--t-soft); color: var(--t); }
.fact .v { font-size: 14px; font-weight: 800; line-height: 1.35; overflow-wrap: anywhere; }
.fact .v.mono { font-family: var(--mono); font-size: 11px; font-weight: 400; color: var(--ink-2);
background: var(--t-soft); border-radius: 8px; padding: 3px 9px; display: inline-block; }
.fact .n { margin-top: auto; font-size: 10.5px; line-height: 1.55; color: var(--ink-4); overflow-wrap: anywhere; }
.fact .n b { color: var(--ink-2); font-weight: 700; }
.fact .row { display: flex; align-items: center; gap: 10px; min-width: 0; }
.fact .row > .col { min-width: 0; flex: 1; }
.ava { flex: none; width: 38px; height: 38px; display: inline-flex; align-items: center; justify-content: center;
color: #fff; background: linear-gradient(135deg, var(--a), var(--a-b)); border-radius: 12px;
box-shadow: var(--shadow-brand), 0 0 0 3px rgba(51,112,255,.10); }
.ava.person { border-radius: 999px; color: #0a7f74; background: #e4f8f5; border: 1px solid #9fe0d9;
box-shadow: 0 0 0 3px rgba(10,162,148,.10); }
/* 大号计数 —— 照目标件:32px 等宽 */
.big { display: flex; align-items: baseline; gap: 6px; }
.big > b { font-family: var(--mono); font-size: 32px; font-weight: 800; line-height: .95; letter-spacing: -.04em; }
.big > span { font-size: 11px; color: var(--ink-4); }
/* 分段刻度条:一条数据一格,按值上色 */
.meter { display: flex; gap: 3px; margin: 0; padding: 0; list-style: none; min-height: 7px; }
.meter > li { flex: 1 1 0; min-width: 4px; height: 7px; border-radius: 2px; background: var(--line); }
.meter > li.v-a { background: var(--ok); }
.meter > li.v-b { background: var(--brand); }
.meter > li.v-c { background: var(--warn-accent); }
.meter > li.v-0 { background: var(--ok); }
.meter > li.v-1, .meter > li.v-2, .meter > li.v-3 { background: var(--danger); }
.meter:empty { height: 7px; border-radius: 3px; background: repeating-linear-gradient(45deg, var(--line-soft) 0 5px, transparent 5px 10px); border: 1px dashed var(--line); }
.mlg { display: flex; flex-wrap: wrap; gap: 6px 18px; margin-top: 8px; font-size: 11px; color: var(--ink-3); }
.mlg span { display: inline-flex; align-items: center; gap: 6px; }
.mlg i { width: 9px; height: 9px; border-radius: 2px; }
.mlg i.v-a { background: var(--ok); } .mlg i.v-b { background: var(--brand); } .mlg i.v-c { background: var(--warn-accent); }
.mlg b { font-weight: 800; color: var(--ink); font-family: var(--mono); }
/* ── 表 ── */
.tw { overflow-x: auto; border: 1px solid var(--line-soft); border-radius: 10px; }
table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 12px; }
th { text-align: left; font-weight: 800; font-size: 10.5px; letter-spacing: .03em; color: var(--ink-3);
padding: 8px 9px; background: var(--soft); border-bottom: 1px solid var(--line-soft); white-space: nowrap; }
th.req::after { content: "* "; margin-left: 1px; color: var(--danger); font-size: 10px; }
td { padding: 6px 9px; border-bottom: 1px solid var(--line-soft); color: var(--ink); line-height: 1.5; overflow-wrap: anywhere; }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover td { background: #f7f9fd; }
/* 序号列由 CSS 计数器排 —— 模版里没有循环变量 */
tbody { counter-reset: rn; }
td.rn { position: relative; width: 38px; padding-left: 13px; white-space: nowrap; font-family: var(--mono); font-size: 10.5px; color: var(--ink-4); }
td.rn::before { counter-increment: rn; content: counter(rn); }
td.num { white-space: nowrap; font-family: var(--mono); font-size: 11.5px; font-variant-numeric: tabular-nums; }
td.mono { font-family: var(--mono); font-size: 11px; color: var(--ink-2); overflow-wrap: break-word; }
/* 空格子自己说话:必填红、选填灰;缺必填的整行浅红 + 行首红条 */
td.req:empty::after, td.opt:empty::after {
display: inline-flex; align-items: center; font-size: 10px; font-weight: 700; line-height: 1;
padding: 3px 7px; border-radius: 5px; border: 1px dashed;
}
td.req:empty::after { content: "必填"; color: var(--danger); background: var(--danger-soft); border-color: #f3b6b3; }
td.opt:empty::after { content: "选填"; color: var(--ink-4); background: var(--soft); border-color: var(--line); }
tbody tr:has(td.req:empty) td { background: #fffbfb; }
tbody tr:has(td.req:empty):hover td { background: #fff5f5; }
tbody tr:has(td.req:empty) td.rn { color: var(--danger); font-weight: 800; }
tbody tr:has(td.req:empty) td.rn::after { content: ""; position: absolute; left: 0; top: 4px; bottom: 4px; width: 3px; border-radius: 0 3px 3px 0; background: var(--danger); }
/* 短标签出药丸,按值分档上色(data-class-of 投影) */
td.pill { white-space: nowrap; }
td.pill > span { display: inline-grid; place-items: center; width: 22px; height: 22px; border-radius: 7px;
font-size: 11px; font-weight: 800; font-family: var(--mono);
background: var(--soft); color: var(--ink-3); border: 1px solid var(--line); }
td.pill > span.v-a { background: var(--ok-soft); color: var(--ok); border-color: var(--ok-line); }
td.pill > span.v-b { background: var(--brand-wash); color: #245bdb; border-color: var(--brand-soft); }
td.pill > span.v-c { background: var(--warn-soft); color: var(--warn); border-color: #ffd9a8; }
td.pill > span.v-true { background: var(--ok-soft); color: var(--ok); border-color: var(--ok-line); width: auto; padding: 0 9px; }
td.pill > span.v-false { width: auto; padding: 0 9px; }
.mono { font-family: var(--mono); }
.tag { display: inline-block; padding: 1.5px 9px; border-radius: 999px; font-size: 11px; font-weight: 700;
background: var(--a-wash); color: var(--a); border: 1px solid var(--a-soft); }
/* ── 说明:蓝底一条,摆在数据旁边 ── */
.note { display: flex; gap: 8px; align-items: flex-start; margin: 11px 0 0; padding: 9px 12px; border-radius: 9px;
background: var(--brand-wash); font-size: 11.5px; line-height: 1.7; color: #245bdb; }
.note svg { flex: none; margin-top: 3px; }
.note b { font-weight: 700; }
/* 剔除行说明:虚线框、灰底 —— 与蓝底那条分得开(它说的是「没做什么」) */
.drop { display: flex; gap: 8px; align-items: flex-start; margin: 10px 0 0; padding: 9px 12px; border-radius: 9px;
border: 1px dashed var(--line); background: var(--soft); font-size: 11.5px; line-height: 1.6; color: var(--ink-3); }
.drop svg { flex: none; margin-top: 3px; color: var(--ink-4); }
.drop b, .drop code { color: var(--ink-2); font-weight: 700; }
.drop code { font-family: var(--mono); font-size: 10.5px; }
/* ── 分步 / 阶段 ── */
.steps { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 7px; }
.steps li { display: flex; gap: 9px; align-items: flex-start; font-size: 12px; line-height: 1.65; color: var(--ink-2); }
.steps .n { flex: none; width: 19px; height: 19px; margin-top: 1px; border-radius: 999px; display: grid;
place-items: center; font-size: 10.5px; font-weight: 800; color: var(--a);
background: var(--a-wash); border: 1px solid var(--a-soft); font-family: var(--mono); }
.steps .t { font-weight: 700; }
.steps .d { color: var(--ink-4); font-size: 11px; }
/* ── 按序求值的规则表 ── */
.rules { border: 1px solid var(--line-soft); border-radius: 12px; overflow: hidden; }
.rules > div { display: grid; grid-template-columns: 26px minmax(0, 1fr) minmax(0, 160px) minmax(0, 68px); gap: 10px;
align-items: center; padding: 10px 14px; font-size: 12px; color: var(--ink-2); }
.rules > div + div { border-top: 1px solid var(--line-soft); }
.rules > .h { background: var(--soft); font-size: 10px; font-weight: 800; letter-spacing: .1em; color: var(--ink-4); }
.rules .no { font-family: var(--mono); font-weight: 800; color: var(--ink-4); }
.rules .go { font-weight: 700; }
.rules .go code { font-family: var(--mono); font-size: 11px; color: var(--a); margin-right: 5px; }
.rules .cnt { font-family: var(--mono); font-weight: 800; text-align: right; }
/* ── 落地动作 ── */
.acts { margin: 0; padding: 0; list-style: none; border: 1px solid var(--line-soft); border-radius: 12px; overflow: hidden; }
.acts li { display: flex; align-items: flex-start; gap: 10px; padding: 11px 14px; font-size: 12px; line-height: 1.65; color: var(--ink-3); }
.acts li + li { border-top: 1px solid var(--line-soft); }
.acts .m { flex: none; width: 20px; height: 20px; margin-top: 1px; border-radius: 999px; display: grid; place-items: center;
color: var(--ok); background: var(--ok-soft); border: 1px solid var(--ok-line); }
.acts b { display: block; font-weight: 800; color: var(--ink); margin-bottom: 2px; }
/* ── 结论章 ── */
.verdict { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 11px 0 0;
padding: 11px 14px; border-radius: 10px; background: var(--ok-soft);
border: 1px solid var(--ok-line); color: var(--ok); font-size: 13px; font-weight: 800; }
.verdict.bad { background: var(--danger-soft); border-color: var(--danger-line); color: var(--danger); }
.verdict.wait { background: var(--warn-soft); border-color: #f7d9ab; color: var(--warn); }
.verdict span { font-weight: 500; font-size: 11.5px; color: var(--ink-2); }
/* ── 落款 ── */
.foot { display: flex; align-items: center; flex-wrap: wrap; gap: 12px;
padding: 13px 20px 15px; border-top: 2px solid var(--line); background: #fbfcfd;
font-size: 10.5px; color: var(--ink-4); }
.foot svg { color: var(--a); }
.foot b { font-size: 11.5px; font-weight: 700; color: var(--ink-2); }
.foot .ftag { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; padding: 6px 13px;
border-radius: 999px; font-size: 11.5px; font-weight: 800; color: var(--a);
background: #fff; border: 1px solid var(--a-soft); }
@media (max-width: 1040px) { .facts { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) {
.facts { grid-template-columns: 1fr; }
.hd, .bd, .foot { padding-left: 13px; padding-right: 13px; }
}
.wrap { max-width: 760px; margin: 0 auto; }
.hd { display: flex; align-items: stretch; gap: 12px; margin-bottom: 14px; }
.hd .bar { width: 4px; border-radius: 2px; background: ${ accent } ; flex: none; }
.hd .kick { font-size: 11px; letter-spacing: .08em; color: #6a7686; }
.hd h1 { margin: 2px 0 0; font-size: 17px; font-weight: 650; letter-spacing: .01em; }
.card { background: #fff; border: 1px solid #e6e9ee; border-radius: 10px; padding: 14px 16px; margin-bottom: 12px; }
.card h2 { margin: 0 0 10px; font-size: 12px; font-weight: 600; color: #6a7686; letter-spacing: .04em; }
.kv { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px 18px; }
.kv > div { min-width: 0; }
.kv em { display: block; font-style: normal; font-size: 11px; color: #8b95a3; margin-bottom: 2px; }
.kv b { font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
.tbl-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th { text-align: left; font-weight: 600; color: #6a7686; padding: 6px 8px; border-bottom: 1px solid #e6e9ee; white-space: nowrap; }
td { padding: 7px 8px; border-bottom: 1px solid #f0f2f5; overflow-wrap: anywhere; }
tr:last-child td { border-bottom: 0; }
.mono { font-family: "SF Mono", Menlo, Consolas, monospace; }
.tag { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 11px; font-weight: 600;
background: ${ accent } 1a; color: ${ accent } ; }
.note { font-size: 11px; color: #8b95a3; margin-top: 10px; line-height: 1.75; }
.steps { list-style: none; margin: 0; padding: 0; }
.steps li { position: relative; padding: 0 0 12px 20px; border-left: 2px solid #e6e9ee; }
.steps li:last-child { padding-bottom: 0; border-left-color: transparent; }
.steps li::before { content: ""; position: absolute; left: -5px; top: 5px; width: 8px; height: 8px;
border-radius: 50%; background: ${ accent } ; }
.steps .t { font-weight: 600; }
.steps .d { color: #8b95a3; font-size: 11.5px; }
` ;
/** 页头 + 卡片区 —— 每张模版都长这样,只换标题与内容 */
/* ==================== 外壳 ==================== */
/** 状态章:办结盖印 / 在办亮灯 —— 两者都不写就没有 */
export type Stamp =
| { readonly kind : 'stamp' ; readonly text : string ; readonly bad? : boolean }
| { readonly kind : 'live' ; readonly text : string } ;
/**
* 一份汇报件的外壳 —— 题头 + 元信息 + 正文 + 落款。
*
* `icon` 认 `ICONS` 里的名字(file-text / list-checks / database / search / coins /
* handshake / archive / shield-check / package / gauge / mail / workflow / users / send);
* 不给就按 tone 落一个默认的。
*/
export function viewShell ( input : {
title : string ;
kicker : string ;
tone : Tone ;
body : string ;
/**
* 标题后面那半截**由数据填**的取值路径 —— 一般是单号。
* 「询价受理需求」是件的名字,「MD14.1」才说得出是哪一单。
*/
titleRef? : string ;
/** 题头徽章的图标名 */
icon? : string ;
/** 右上角状态章 */
stamp? : Stamp ;
/** 落款一句(署名与出处);不给就落一句通用的 */
sign? : string ;
/**
* 落款右端那枚小签 —— 这份件在流里是什么位置(「A1 受理归一 · 交下游」)。
* **写位置,不写状态**:归档件与在办件是同一张模版。
*/
footTag? : string ;
} ) : string {
const accent = ACCENT [ input . tone ] ;
return ` <style> ${ BASE_CSS ( accent ) } </style>
<div class="wrap">
<div class="hd"><span class="bar"></span><div><div class="kick"> ${ input . kicker } </div><h1> ${ input . title } </h1></div></div>
${ input . body }
const a = ACCENT [ input . tone ] ;
const b = ACCENT_B [ input . tone ] ;
const badge = ico ( input . icon ? ? DEFAULT_ICON [ input . tone ] , 18 ) ;
const stamp =
input . stamp === undefined
? ''
: input . stamp . kind === 'stamp'
? ` <span class="stamp ${ input . stamp . bad === true ? ' bad' : '' } "> ${ input . stamp . text } </span> `
: ` <span class="live"> ${ ico ( 'gauge' , 12 ) } ${ input . stamp . text } </span> ` ;
return ` <style> ${ BASE_CSS ( a , b ) } </style>
<div class="rp">
<div class="hd">
<span class="bdg"> ${ badge } </span>
<div class="tt">
<div class="kick"> ${ input . kicker } </div>
<h1> ${ input . title } ${ input . titleRef === undefined ? '' : ` <span class="ref" data-slot=" ${ input . titleRef } "></span> ` } </h1>
</div>
${ stamp }
</div>
<div class="bd"> ${ input . body } </div>
<div class="foot"> ${ ico ( 'file-text' , 12 ) } <b> ${ input . sign ? ? '本页由活动按当下局面生成' } </b> · 放行前的一切都还改得动 ${
input . footTag === undefined ? '' : ` <span class="ftag"> ${ ico ( 'git-branch' , 12 ) } ${ input . footTag } </span> `
} </div>
</div> ` ;
}
/** 键值卡:`[标签, 取值路径]`;`mono` 用等宽显示编号类字段 */
export function kvCard ( title : string , items : ReadonlyArray < readonly [ string , string , ( 'mono' | 'tag' ) ? ] > ) : string {
const cells = items
. map ( ( [ label , path , style ] ) = > {
const cls = style === undefined ? '' : ` class=" ${ style === 'mono' ? 'mono' : 'tag' } " ` ;
return ` <div><em> ${ label } </em><b ${ cls } data-slot=" ${ path } "></b></div> ` ;
} )
. join ( '' ) ;
return ` <div class="card"><h2> ${ title } </h2><div class="kv"> ${ cells } </div></div> ` ;
const DEFAULT_ICON : Readonly < Record < Tone , string > > = {
brand : 'file-text' ,
ok : 'circle-check' ,
warn : 'alert-triangle' ,
danger : 'alert-triangle' ,
case : 'workflow' ,
archive : 'archive' ,
} ;
/* ==================== 件 ==================== */
/**
* 关键指标带 —— **判据摊上台面**。
* 摆的是人做判断要看的那三五个数,不是所有数;摆多了就没有「关键」可言。
*/
export function statsBand ( items : ReadonlyArray < readonly [ string , string ] > ) : string {
return ` <div class="stats"> ${ items
. map ( ( [ label , path ] ) = > ` <div class="stat"><i> ${ label } </i><b data-slot=" ${ path } "></b></div> ` )
. join ( '' ) } </div> ` ;
}
/** 逐条表:行数不写死,由 `data-rows` 展开 */
export function tableCard (
title : string ,
/**
* 章节头 —— **小号加粗标签 + 一条细线 + 右端小签**,照 PRD 目标件。
*
* 从前这里是「彩色方块编号(一/二/三)+ 大标题」。那是我自己发明的一套:
* 编号在一份两三屏的件里没有指路作用(人不会说「翻到第四节」),
* 而方块 + 大标题把每一节都拔成一个新的开始 —— 一份件被切成了五份。
* 目标件用的是**分隔而非分段**:一行小字压住,右端一枚 mono 小签说这一段的口径。
*/
export function section ( title : string , body : string , aside ? : { readonly text : string ; readonly tone ? : 'ok' | 'bad' } ) : string {
const pill =
aside === undefined
? ''
: ` <span class="aside ${ aside . tone === undefined ? '' : ` ${ aside . tone } ` } "> ${ aside . text } </span> ` ;
return ` <div class="s"><div class="sh"><b> ${ title } </b><span class="ln"></span> ${ pill } </div> ${ body } </div> ` ;
}
export type ColStyle =
| 'mono'
| 'num'
| 'pill'
| 'req'
| 'opt'
| 'req-mono'
| 'opt-mono'
| 'req-num'
| 'opt-num' ;
const COL_CLASS : Readonly < Record < ColStyle , string > > = {
mono : 'mono' ,
num : 'num' ,
pill : 'pill' ,
req : 'req' ,
opt : 'opt' ,
'req-mono' : 'req mono' ,
'opt-mono' : 'opt mono' ,
'req-num' : 'req num' ,
'opt-num' : 'opt num' ,
} ;
/**
* 逐条表:行数不写死,由 `data-rows` 展开。
*
* `numbered` 给一列序号 —— 由 CSS 计数器排,模版里没有循环变量。
* 有了行号,「第 9 条缺年用量」才指得出是哪一行。
*/
export function tableOnly (
rowsPath : string ,
columns : ReadonlyArray < readonly [ string , string , ( 'mono' ) ? ] > ,
note? : string ,
columns : ReadonlyArray < readonly [ string , string , ColStyle ? ] > ,
options ? : { readonly numbered? : boolean } ,
) : string {
const head = columns . map ( ( [ label ] ) = > ` <th> ${ label } </th> ` ) . join ( '' ) ;
const cells = columns
. map ( ( [ , path , style ] ) = > ` <td ${ style === 'mono' ? ' class="mono" ' : '' } data-slot=" ${ path } "></td> ` )
. join ( '' ) ;
return ` <div class="card"><h2 >${ title } </h2><div class="tbl-wrap"><table>
<thead><tr> ${ head } </tr></thead>
<tbody data-rows=" ${ rowsPath } "><tr> ${ cells } </tr></tbody>
</table></div> ${ note === undefined ? '' : ` <p class="note"> ${ note } </p> ` } </div> ` ;
const numbered = options ? . numbered === true ;
const head =
( numbered ? '<th class="rn"></th> ' : '' ) +
columns
. map ( ( [ label , , style ] ) = > ` <th ${ style !== undefined && style . startsWith ( 'req' ) ? ' class="req"' : '' } > ${ label } </th> ` )
. join ( '' ) ;
const cells =
( numbered ? '<td class="rn"></td>' : '' ) +
columns
. map ( ( [ , path , style ] ) = > {
// 药丸要有个套得住的壳:槽位填的是文本,`td` 自己当不了药丸;颜色按值分档
if ( style === 'pill' ) return ` <td class="pill"><span data-class-of=" ${ path } " data-slot=" ${ path } "></span></td> ` ;
const cls = style === undefined ? '' : ` class=" ${ COL_CLASS [ style ] } " ` ;
// 必填 / 选填这两档要**真的空**才标得出来,所以带 data-blank
const blank = style !== undefined && ( style . startsWith ( 'req' ) || style . startsWith ( 'opt' ) ) ? ' data-blank' : '' ;
return ` <td ${ cls } ${ blank } data-slot=" ${ path } "></td> ` ;
} )
. join ( '' ) ;
return ` <div class="tw"><table><thead><tr> ${ head } </tr></thead><tbody data-rows=" ${ rowsPath } "><tr> ${ cells } </tr></tbody></table></div> ` ;
}
/** 说明卡:把「为什么这么做」摆在数据旁边 —— 人要在数据上作判断 */
export function noteCard ( title : string , html : string ) : string {
return ` <div class="card"><h2> ${ title } </h2><p class="note" style="margin-top:0" >${ html } </p ></div> ` ;
/** 只要那一条 说明 */
export function noteOnly ( html : string ) : string {
return ` <div class="note"> ${ ico ( 'shield-check' , 13 ) } <span >${ html } </span ></div> ` ;
}
/** 阶段时间线:loop 按案例模型的阶段与里程碑组织汇报(AM 07) */
export function stagesCard ( title : string , rowsPath : string , note? : string ) : string {
return ` <div class="card"><h2> ${ title } </h2><ul class="steps" data-rows=" ${ rowsPath } ">
<li><span class="t" data-slot="name"></span><span class="d" data-slot="state"></span></li>
</ul> ${ note === undefined ? '' : ` <p class="note"> ${ note } </p> ` } </div> ` ;
export function stagesOnly ( rowsPath : string ) : string {
return ` <ul class="steps" data-rows=" ${ rowsPath } "><li><span class="n">·</span><span><span class="t" data-slot="name"></span> <span class="d" data-slot="state"></span></span></li></ul> ` ;
}
/**
* **来件原文** —— 这一单是被什么叫起来的。
*
* 归一出来的每一个字段都得回溯得到出处;把原文摆在归一结果**上面**,
* 人核对时不必切页签、不必去翻沟通记录 —— 一屏之内看得见「客户原话是这么写的」。
* 正文 `pre-wrap` 原样照录:不摘要、不折叠。摘要过的原文就不是原文了。
*/
export function letter ( input : {
/** 谁发来的(取值路径) */
from : string ;
/** 主题(取值路径) */
subject : string ;
/** 收件人(取值路径)—— 投给哪个信箱;不给就不摆这一行 */
to? : string ;
/** 正文(取值路径) */
body : string ;
/** 附件数组的取值路径 —— 每件取 `name` 与 `note` */
attachments? : string ;
/** 左上角那枚方向签,默认「收件」 */
direction? : string ;
} ) : string {
const atts =
input . attachments === undefined
? ''
: ` <div class="atts" data-rows=" ${ input . attachments } "><span class="att"> ${ ico ( 'sheet' , 12 ) } <b data-slot="name"></b><span data-slot="note"></span></span></div> ` ;
/*
* 入站时刻由基座填(`case:openedAt`,起单即入站)——
* 一封信摆在这儿而不说几时到的,人核 SLA 时得另开一屏去找。
*/
const to = input . to === undefined ? '' : ` <p><i>收件人</i><b data-slot=" ${ input . to } "></b></p> ` ;
/*
* 归属签 —— 这一单**怎么来的、归了谁**(`case:origin` / `case:owner`,基座填)。
* 活动写不出也不该写:让被指派的人自己说自己是谁,那不叫归属解析。
*/
return ` <div class="lt">
<div class="lh"><span class="dir"> ${ ico ( 'mail-open' , 12 ) } ${ input . direction ? ? '收件' } </span><span class="own"><b data-slot="case:origin" data-blank></b><i>→</i><b data-slot="case:owner" data-blank></b></span><code class="at" data-slot="case:openedAt" data-blank></code></div>
<div class="lm"><p><i>发件人</i><b data-slot=" ${ input . from } "></b></p> ${ to } </div>
<h4 data-slot=" ${ input . subject } "></h4>
<p class="lb" data-slot=" ${ input . body } "></p>
${ atts }
</div> ` ;
}
/**
* **分段刻度条** —— 一条数据一格,按值上色(`data-class-of`)。
*
* 原型里那条是「按条数分宽的分布条」,宽度得算,而模版不许算。
* 改成一条料一格:同类相邻自然连成一段,看着等价,而且每一格都对得上清单里的一行 ——
* 数得出来,不是一根示意的色条。
*
* `legend` 是图例:`[色档, 标签, 取值路径]`,把「A 5 条」这类计数摆在条子下面。
*/
export function meter (
rowsPath : string ,
valueField : string ,
legend? : ReadonlyArray < readonly [ string , string , string ] > ,
) : string {
/*
* 格子里**不填字**:只用 `data-class-of` 把值投影成颜色。
* 带上 `data-slot` 的下场实测见过 —— 九个格子里排着「0 0 0 0 0 0 0 0 1」,
* 一条刻度条成了一串数字,目标件那条是纯色块。
*/
const bar = ` <ul class="meter" data-rows=" ${ rowsPath } "><li data-class-of=" ${ valueField } "></li></ul> ` ;
if ( legend === undefined || legend . length === 0 ) return bar ;
const keys = legend
. map (
( [ tone , label , path ] ) = >
` <span><i class="v- ${ tone } "></i> ${ label } <b data-slot=" ${ path } "></b></span> ` ,
)
. join ( '' ) ;
return ` ${ bar } <div class="mlg"> ${ keys } </div> ` ;
}
/** 大号计数 —— 一屏之内最该被一眼看见的那个数(`unit` 是它的分母或量纲) */
export function bigCount ( path : string , unit : string ) : string {
return ` <span class="big"><b data-slot=" ${ path } "></b><span> ${ unit } </span></span> ` ;
}
/**
* **按序求值的规则表** —— 三条互斥条件与各自的去向,连着「本单是几」一起摆。
*
* 命中哪一条由编排层求值,这里**不画对勾替它宣布**;但把条件、去向、本单的计数
* 摆在同一行上,人自己一眼就对得出来 —— 这比一句「三条按序求值」有用得多。
*/
export function ruleTable (
rules : ReadonlyArray < { readonly no : string ; readonly when : string ; readonly node : string ; readonly to : string ; readonly count : string } > ,
) : string {
const head = '<div class="h"><span>序</span><span>条件(按序求值)</span><span>去向</span><span>本单</span></div>' ;
const rows = rules
. map (
( r ) = >
` <div><span class="no"> ${ r . no } </span><span> ${ r . when } </span> ` +
` <span class="go"><code> ${ r . node } </code> ${ r . to } </span><span class="cnt" data-slot=" ${ r . count } "></span></div> ` ,
)
. join ( '' ) ;
return ` <div class="rules"> ${ head } ${ rows } </div> ` ;
}
/** 要素卡的色调 —— 静态,不随数据变(数据判不了达标的事,别让颜色替它说) */
export type FactTone = 'brand' | 'teal' | 'ok' | 'warn' ;
/**
* **要素带** —— 这一单的几件事,一件一张卡。
*
* 与关键指标带的分别:指标带摆的是**数**(判据),要素带摆的是**谁与什么**
* (客户、联系人、属性、成品量)。人接手一单先认这几件,再看数对不对。
*/
export function factCards (
items : ReadonlyArray < {
/** 卡头那一行小字 */
readonly label : string ;
/** 主值的取值路径 */
readonly value : string ;
/** 卡脚一句(HTML,可含 `data-slot`) */
readonly note? : string ;
readonly icon? : string ;
readonly tone? : FactTone ;
/** 主值用等宽(邮箱、编码这类) */
readonly mono? : boolean ;
/**
* 左侧圆标:`org` 是圆角方(组织)、`person` 是圆(人)—— 方与圆一眼分得开。
* 标里放的是**图标不是首字母**:首字母得算,而模版不许算;
* 画一个算错的缩写,不如画一个说得清类别的图标。
*/
readonly avatar ? : 'org' | 'person' ;
/** 给了就把主值摆成大号计数,这里是它的分母或量纲(如「/ 10 行 BOM」) */
readonly big? : string ;
/** 主值下面再挂一件(刻度条这类) */
readonly extra? : string ;
} > ,
) : string {
return ` <div class="facts"> ${ items
. map ( ( f ) = > {
const value =
f . big === undefined
? ` <span class="v ${ f . mono === true ? ' mono' : '' } " data-slot=" ${ f . value } "></span> `
: bigCount ( f . value , f . big ) ;
const body =
f . avatar === undefined
? value
: ` <div class="row"><span class="ava ${ f . avatar === 'person' ? ' person' : '' } "> ` +
` ${ ico ( f . avatar === 'person' ? 'user' : 'building' , 17 ) } </span><div class="col"> ${ value } </div></div> ` ;
return (
` <div class="fact ${ f . tone === undefined || f . tone === 'brand' ? '' : ` t- ${ f . tone } ` } "> ` +
` <span class="k"><i> ${ ico ( f . icon ? ? 'file-text' , 11 ) } </i> ${ f . label } </span> ` +
` ${ body } ${ f . extra ? ? '' } ` +
` ${ f . note === undefined ? '' : ` <span class="n"> ${ f . note } </span> ` } </div> `
) ;
} )
.join('')}</div> ` ;
}
/**
* **落地动作** —— 放行那一刻系统替人做了什么,一条一行。
*
* 放行是草稿态与事实态之间的那道门。门后发生的事人看不见(合并、发事件、起下游单),
* 而他要为这些事负责 —— 所以得逐条写出来,不能只留一句「已放行」。
*/
export function actionList ( items : ReadonlyArray < { readonly title : string ; readonly detail : string } > ) : string {
return ` <ul class="acts"> ${ items
. map ( ( a ) = > ` <li><span class="m"> ${ ico ( 'circle-check' , 12 ) } </span><span><b> ${ a . title } </b> ${ a . detail } </span></li> ` )
. join ( '' ) } </ul> ` ;
}
/** 结论章 —— 一屏只该有一个 */
export function verdict ( text : string , tone : 'ok' | 'bad' | 'wait' , detail? : string ) : string {
const cls = tone === 'ok' ? '' : ` ${ tone } ` ;
const mark = tone === 'ok' ? 'circle-check' : tone === 'bad' ? 'alert-triangle' : 'gauge' ;
return ` <p class="verdict ${ cls } "> ${ ico ( mark , 15 ) } ${ text } ${ detail === undefined ? '' : ` <span> ${ detail } </span> ` } </p> ` ;
}