/** * 视图模版工具箱 —— 活动开发者写「呈现」时看到的世界(AM 07)。 * * 模版是**内容不是代码**:这里产出的一律是无脚本、无事件处理器、无外链的静态 HTML; * 数据由基座在推送时填(`data-slot` 取值、`data-rows` 逐条展开)。 * 容器是沙箱 iframe(`sandbox=""`),因此**样式必须自带** —— 页面不继承工作台的 CSS。 * * 六个活动共用这一套外壳:汇报面貌一致,不是每个活动各自发明一套。 */ export type Tone = 'brand' | 'ok' | 'warn' | 'danger' | 'case' | 'archive'; const ACCENT: Readonly> = { brand: '#3370ff', ok: '#0aa294', warn: '#ff8800', danger: '#e5484d', case: '#7f3bf5', archive: '#46586a', }; /** 一套克制的排版;所有尺寸用相对单位,窄栏也不横向溢出 */ const BASE_CSS = (accent: string): string => ` :root { color-scheme: light; } * { box-sizing: border-box; } body { margin: 0; padding: 18px; font: 13px/1.6 -apple-system, "PingFang SC", "Noto Sans SC", "Segoe UI", sans-serif; color: #1f2933; background: #f7f8fa; } .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 function viewShell(input: { title: string; kicker: string; tone: Tone; body: string; }): string { const accent = ACCENT[input.tone]; return `
${input.kicker}

${input.title}

${input.body}
`; } /** 键值卡:`[标签, 取值路径]`;`mono` 用等宽显示编号类字段 */ export function kvCard(title: string, items: ReadonlyArray): string { const cells = items .map(([label, path, style]) => { const cls = style === undefined ? '' : ` class="${style === 'mono' ? 'mono' : 'tag'}"`; return `
${label}
`; }) .join(''); return `

${title}

${cells}
`; } /** 逐条表:行数不写死,由 `data-rows` 展开 */ export function tableCard( title: string, rowsPath: string, columns: ReadonlyArray, note?: string, ): string { const head = columns.map(([label]) => `${label}`).join(''); const cells = columns .map(([, path, style]) => ``) .join(''); return `

${title}

${head}${cells}
${note === undefined ? '' : `

${note}

`}
`; } /** 说明卡:把「为什么这么做」摆在数据旁边 —— 人要在数据上作判断 */ export function noteCard(title: string, html: string): string { return `

${title}

${html}

`; } /** 阶段时间线:loop 按案例模型的阶段与里程碑组织汇报(AM 07) */ export function stagesCard(title: string, rowsPath: string, note?: string): string { return `

${title}

${note === undefined ? '' : `

${note}

`}
`; }