# -*- coding: utf-8 -*-
"""
构建：《宝锐销售 · 飞书活动 / 测试反馈 / 竞品信息 录入规范培训》
- 零外部依赖（全内联，Logo 由 brand_kit 注入 base64）
- 5 张 SVG 示意图，方框等宽等间距，data-fb 标记便于自检
- 末尾调用 brand_kit.inject() 注入页眉/页脚
"""
import os, sys, io

W = os.path.expanduser('~/Desktop/Hermes输出-工作类')
OUTDIR = os.path.join(W, '培训与上手', '培训')
os.makedirs(OUTDIR, exist_ok=True)
RAW = os.path.join(OUTDIR, '_raw.html')
FINAL = os.path.join(OUTDIR, '飞书活动-测试反馈-竞品信息_录入规范培训.html')

# 交互沙盒模块（同目录）：CSS / HTML 三段 / JS
if OUTDIR not in sys.path:
    sys.path.insert(0, OUTDIR)
from _sandbox import SB_CSS, SB_SECTIONS, SB_JS

# ─────────────────────────── SVG helpers ───────────────────────────
def fb(x, y, w, h, fill='#FFFFFF', stroke='#CFDDFB', rx=12, sw='1.5'):
    """流程方框（data-fb 供等宽等间距自检）"""
    return ('<rect x="%s" y="%s" width="%s" height="%s" rx="%s" fill="%s" '
            'stroke="%s" stroke-width="%s" data-fb="1"/>') % (x, y, w, h, rx, fill, stroke, sw)

def rect(x, y, w, h, fill='#FFFFFF', stroke='#CFDDFB', rx=12, sw='1.5'):
    return ('<rect x="%s" y="%s" width="%s" height="%s" rx="%s" fill="%s" '
            'stroke="%s" stroke-width="%s"/>') % (x, y, w, h, rx, fill, stroke, sw)

def tx(x, y, s, size='14', fill='#334155', anchor='start', weight='400'):
    return ('<text x="%s" y="%s" font-size="%s" fill="%s" text-anchor="%s" '
            'font-weight="%s">%s</text>') % (x, y, size, fill, anchor, weight, s)

DEFS = '''<defs>
  <marker id="ar" markerWidth="9" markerHeight="9" refX="7.5" refY="4.5" orient="auto">
    <path d="M0,0 L10,5 L0,10 z" fill="#7E97C8"></path></marker>
  <marker id="arw" markerWidth="9" markerHeight="9" refX="7.5" refY="4.5" orient="auto">
    <path d="M0,0 L10,5 L0,10 z" fill="#C6D3EC"></path></marker>
  <linearGradient id="gA" x1="0" y1="0" x2="0" y2="1">
    <stop offset="0%" stop-color="#FFFFFF"/><stop offset="100%" stop-color="#F7FAFF"/></linearGradient>
</defs>'''

GFONT = '<g font-family="Microsoft YaHei, PingFang SC, sans-serif">'

# ── FIG1：三大痛点 → 培训收益 ─────────────────────────────────────
def fig1():
    rows = [
        ('信息查不到', '想翻上次沟通，活动里只有「聊了下」',
         '结构化留痕', '每次沟通都可检索、可统计、可追溯'),
        ('客户名对不上', '手打全称，系统里成了「两家人」',
         '下拉点选', '客户点选，商机 / 联系人 / 订单自动级联'),
        ('竞品情报沉不下来', '竞品名各写各的，事后埋在活动里',
         '归一到库', '品牌归一化后，自动沉淀进竞对情报库'),
    ]
    p = [DEFS, GFONT]
    y = 16
    for pl, pd, bl, bd in rows:
        p.append(fb(10, y, 452, 52, '#FFF7ED', '#FED7AA', 12))
        p.append(tx(30, y + 22, pl, '14.6', '#9A3412', weight='700'))
        p.append(tx(30, y + 42, pd, '13', '#B45309'))
        p.append('<path d="M470,%d L566,%d" stroke="#7E97C8" stroke-width="2" marker-end="url(#ar)"/>'
                 % (y + 26, y + 26))
        p.append(fb(578, y, 452, 52, '#F0FDF4', '#BBF7D0', 12))
        p.append(tx(598, y + 22, bl, '14.6', '#166534', weight='700'))
        p.append(tx(598, y + 42, bd, '13', '#15803D'))
        y += 60
    p.append('</g>')
    return ('<svg viewBox="0 0 1040 196" role="img" '
            'aria-label="三大痛点与培训收益对照">' + ''.join(p) + '</svg>')

FIG1_CAP = '左：不规范的三个后果　右：规范后你能得到什么'

# ── FIG2：活动录入字段结构图 ──────────────────────────────────────
def fig2():
    CX = [16, 362, 708]; CW = 316; CY = 16; CH = 296
    p = [GFONT]
    # 卡1
    p.append(fb(CX[0], CY, CW, CH, '#FFFFFF', '#CFDDFB', 12))
    p.append(rect(CX[0], CY, CW, 36, '#1E4FA8', '#1E4FA8', 12))
    p.append(tx(CX[0] + 16, CY + 24, '① 关联信息（全活动共用）', '14.6', '#fff', weight='700'))
    c1 = ['客户（必选 · 检索点选）', '关联商机（可多个）', '拜访对象 / 联系人', '关联订单（真实单号）',
          '货号（datalist 联想）', '活动类型（项目 / 采购）', '活动日期（默认今天）']
    yy = CY + 60
    for s in c1:
        p.append('<circle cx="%d" cy="%d" r="3" fill="#0043C1"/>' % (CX[0] + 20, yy - 4))
        p.append(tx(CX[0] + 32, yy, s, '13.4', '#334155'))
        yy += 30
    p.append(tx(CX[0] + 16, CY + 282, '权限：只看得到你名下的客户', '12.2', '#94A3B8'))
    # 卡2
    p.append(fb(CX[1], CY, CW, CH, '#FFFFFF', '#CFDDFB', 12))
    p.append(rect(CX[1], CY, CW, 36, '#0043C1', '#0043C1', 12))
    p.append(tx(CX[1] + 16, CY + 24, '② 活动内容（一个商机一套）', '14.6', '#fff', weight='700'))
    c2 = ['① 客户需求（分点）', '② 技术参数（货号＋量化）', '③ 测试单结果跟进',
          '④ 问题现象（含原话）', '⑤ 客户反馈', '⑥ 竞对信息（品牌＋量化）']
    yy = CY + 62
    for s in c2:
        p.append('<circle cx="%d" cy="%d" r="3" fill="#0043C1"/>' % (CX[1] + 20, yy - 4))
        p.append(tx(CX[1] + 32, yy, s, '13.4', '#334155'))
        yy += 32
    p.append(tx(CX[1] + 16, CY + 268, '关联几个商机 → 就出几套，各写各的', '12.2', '#94A3B8'))
    p.append(tx(CX[1] + 16, CY + 286, '采购跟进：拜访目标 / 内容 / 计划', '12.2', '#94A3B8'))
    # 卡3
    p.append(fb(CX[2], CY, CW, CH, '#FFFFFF', '#CFDDFB', 12))
    p.append(rect(CX[2], CY, CW, 36, '#0B3E8F', '#0B3E8F', 12))
    p.append(tx(CX[2] + 16, CY + 24, '③ 下一步计划（必填）', '14.6', '#fff', weight='700'))
    c3 = ['事项（做什么）', '负责人（默认自己）', '时间节点（到具体日期）']
    yy = CY + 62
    for s in c3:
        p.append('<circle cx="%d" cy="%d" r="3" fill="#0043C1"/>' % (CX[2] + 20, yy - 4))
        p.append(tx(CX[2] + 32, yy, s, '13.4', '#334155'))
        yy += 32
    p.append('<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#E4E9F2"/>'
             % (CX[2] + 16, CY + 172, CX[2] + CW - 16, CY + 172))
    p.append(tx(CX[2] + 16, CY + 194, '提交后三轨落盘', '13', '#002D80', weight='700'))
    for i, s in enumerate(['本地记录（活动编号）', 'CloudBase 数据库', '飞书描述（一键复制）']):
        p.append(tx(CX[2] + 16, CY + 218 + i * 24, s, '12.6', '#475569'))
    p.append('</g>')
    return '<svg viewBox="0 0 1040 328" role="img" aria-label="活动录入字段结构图">' + ''.join(p) + '</svg>'

FIG2_CAP = '活动录入字段结构：关联信息（共用）→ 活动内容（一商机一套）→ 下一步计划 → 三轨落盘'

# ── FIG3：测试反馈闭环流程 ───────────────────────────────────────
def fig3():
    B = [('① 关联真实订单', '检索并点选订单号'),
         ('② 带出全部货号', '勾选本次跟进的货号'),
         ('③ 逐货号写结果', '每货号一句量化结果'),
         ('④ 上传附件', '图片 / PDF / 表格'),
         ('⑤ 归档模块', '进入测试反馈模块')]
    W0 = 176; G = 36; X0 = 8; Y = 42; H = 92
    p = [DEFS, GFONT]
    x = X0
    for i, (l1, l2) in enumerate(B):
        last = (i == len(B) - 1)
        fill = '#0043C1' if last else 'url(#gA)'
        stroke = '#0043C1' if last else '#CFDDFB'
        p.append(fb(x, Y, W0, H, fill, stroke, 12))
        p.append(tx(x + 14, Y + 34, l1, '14', '#fff' if last else '#002D80', weight='700'))
        p.append(tx(x + 14, Y + 58, l2, '12.4', '#DCE6FB' if last else '#64748B'))
        if not last:
            ax = x + W0 + 6
            p.append('<path d="M%d,%d L%d,%d" stroke="#7E97C8" stroke-width="2" marker-end="url(#ar)"/>'
                     % (ax, Y + H // 2, ax + G - 12, Y + H // 2))
        x += W0 + G
    # 回流箭头：⑤ → ①
    p.append('<path d="M964,%d C964,%d 1000,%d 520,%d C120,%d 76,%d 96,%d" fill="none" '
             'stroke="#C6D3EC" stroke-width="1.6" stroke-dasharray="6 5" marker-end="url(#arw)"/>'
             % (Y + H, Y + H + 34, Y + H + 42, Y + H + 42, Y + H + 42, Y + H + 34, Y + H))
    p.append(tx(520, Y + H + 60, '结果回写 → 测试反馈模块是测试反馈数据的「唯二来源」之一，其余渠道不计入',
                '12.6', '#94A3B8', anchor='middle'))
    p.append('</g>')
    return '<svg viewBox="0 0 1040 210" role="img" aria-label="测试反馈闭环流程">' + ''.join(p) + '</svg>'

FIG3_CAP = '测试反馈闭环：关联真实订单 → 带出货号 → 逐货号结果 → 附件 → 归档'

# ── FIG4：竞品情报沉淀闭环 ───────────────────────────────────────
def fig4():
    B = [('① 采集情报', '品牌 / 货号 / 性能 / 评价'),
         ('② 品牌归一化', '别名→标准名，禁手打'),
         ('③ 去重', '品牌＋货号＋客户'),
         ('④ 写入情报库', 'competitor_intel.json'),
         ('⑤ 复用反哺', '选型 / 话术 / 对标')]
    W0 = 176; G = 36; X0 = 8; Y = 34; H = 92
    p = [GFONT]
    x = X0
    for i, (l1, l2) in enumerate(B):
        last = (i == len(B) - 1)
        fill = '#0043C1' if last else 'url(#gA)'
        stroke = '#0043C1' if last else '#CFDDFB'
        p.append(fb(x, Y, W0, H, fill, stroke, 12))
        p.append(tx(x + 14, Y + 34, l1, '14', '#fff' if last else '#002D80', weight='700'))
        p.append(tx(x + 14, Y + 58, l2, '12', '#DCE6FB' if last else '#64748B'))
        if not last:
            ax = x + W0 + 6
            p.append('<path d="M%d,%d L%d,%d" stroke="#7E97C8" stroke-width="2" marker-end="url(#ar)"/>'
                     % (ax, Y + H // 2, ax + G - 12, Y + H // 2))
        x += W0 + G
    p.append('<path d="M964,%d C964,%d 1000,%d 520,%d C120,%d 76,%d 96,%d" fill="none" '
             'stroke="#C6D3EC" stroke-width="1.6" stroke-dasharray="6 5" marker-end="url(#arw)"/>'
             % (Y + H, Y + H + 30, Y + H + 38, Y + H + 38, Y + H + 38, Y + H + 30, Y + H))
    p.append(tx(520, Y + H + 56, '每周复核 · 反哺销售话术与产品选型', '12.6', '#94A3B8', anchor='middle'))
    # 入库字段条
    p.append(rect(8, Y + H + 74, 1024, 46, '#FBFCFE', '#D6E2FA', 10))
    p.append(tx(28, Y + H + 102, '入库字段：', '13', '#002D80', weight='700'))
    p.append(tx(110, Y + H + 102,
                'brand（品牌，归一化后）·  sku（货号）·  performance（性能）·  feedback（客户评价）·  customer（客户）·  owner（录入人）·  time（时间）',
                '12.6', '#475569'))
    p.append('</g>')
    return '<svg viewBox="0 0 1040 268" role="img" aria-label="竞品情报沉淀闭环">' + ''.join(p) + '</svg>'

FIG4_CAP = '竞品情报沉淀闭环：采集 → 品牌归一化 → 去重 → 写入竞对情报库 → 复用'

# ── FIG5：提交前自检流程 ─────────────────────────────────────────
def fig5():
    CX = [16, 362, 708]; CW = 316; G = 30
    R1Y = 24; R2Y = 148; H = 78
    r1 = [('① 客户是点选的吗', '不是手打全称'),
          ('② 参数有货号＋数字吗', '缺一样＝不合格'),
          ('③ 结果挂真实订单了吗', '带出货号，逐条结果')]
    r2 = [('④ 计划有「三件套」吗', '事项 / 负责人 / 时间节点'),
          ('⑤ 竞对写品牌＋量化了吗', '无竞对则留空，别写「无」'),
          ('⑥ 全部通过 → 提交', '绿灯：信息齐备，可提交')]
    p = [DEFS, GFONT]
    for i, (l1, l2) in enumerate(r1):
        p.append(fb(CX[i], R1Y, CW, H, 'url(#gA)', '#CFDDFB', 12))
        p.append('<circle cx="%d" cy="%d" r="12" fill="#0043C1"/>' % (CX[i] + 26, R1Y + 30))
        p.append(tx(CX[i] + 26, R1Y + 35, str(i + 1), '13', '#fff', anchor='middle', weight='700'))
        p.append(tx(CX[i] + 48, R1Y + 34, l1, '14', '#002D80', weight='700'))
        p.append(tx(CX[i] + 48, R1Y + 56, l2, '12.6', '#64748B'))
        if i < 2:
            ax = CX[i] + CW + 4
            p.append('<path d="M%d,%d L%d,%d" stroke="#7E97C8" stroke-width="2" marker-end="url(#ar)"/>'
                     % (ax, R1Y + H // 2, ax + G - 10, R1Y + H // 2))
    for i, (l1, l2) in enumerate(r2):
        last = (i == 2)
        fill = '#0043C1' if last else 'url(#gA)'
        stroke = '#0043C1' if last else '#CFDDFB'
        p.append(fb(CX[i], R2Y, CW, H, fill, stroke, 12))
        num_c = '#fff' if last else '#0043C1'
        circ_c = '#fff' if last else '#0043C1'
        p.append('<circle cx="%d" cy="%d" r="12" fill="%s"/>' % (CX[i] + 26, R2Y + 30, circ_c))
        p.append(tx(CX[i] + 26, R2Y + 35, str(i + 4), '13', num_c, anchor='middle', weight='700'))
        p.append(tx(CX[i] + 48, R2Y + 34, l1, '14', '#fff' if last else '#002D80', weight='700'))
        p.append(tx(CX[i] + 48, R2Y + 56, l2, '12.6', '#DCE6FB' if last else '#64748B'))
        if i < 2:
            ax = CX[i] + CW + 4
            p.append('<path d="M%d,%d L%d,%d" stroke="#7E97C8" stroke-width="2" marker-end="url(#ar)"/>'
                     % (ax, R2Y + H // 2, ax + G - 10, R2Y + H // 2))
    # 换行连接 ③ → ④
    p.append('<path d="M866,%d C866,%d 900,%d 300,%d C120,%d 132,%d 132,%d" fill="none" '
             'stroke="#7E97C8" stroke-width="2" marker-end="url(#ar)"/>'
             % (R1Y + H, R1Y + H + 26, R1Y + H + 34, R1Y + H + 34, R2Y - 30, R2Y - 8, R2Y))
    p.append('</g>')
    return '<svg viewBox="0 0 1040 252" role="img" aria-label="提交前自检流程">' + ''.join(p) + '</svg>'

FIG5_CAP = '提交前逐项打勾：有一项不合格就先补，别带着问题提交'

# ─────────────────────────── HTML 片段 ───────────────────────────
def eg(good, poor, gtitle='✅ 合格版', ptitle='⚠️ 不合格版'):
    return ('<div class="eg"><div class="good"><span class="lb">%s</span><div class="q">%s</div></div>'
            '<div class="poor"><span class="lb">%s</span><div class="q">%s</div></div></div>'
            ) % (gtitle, good, ptitle, poor)

def fig(svg, cap):
    return '<figure><div class="fig-scroll">%s</div><figcaption>%s</figcaption></figure>' % (svg, cap)

CSS = r"""
:root{
  --blue:#0043C1; --deep:#002D80; --tint:#F1F5FE; --line:#E4E9F2; --bg:#F5F7FA;
  --ink:#0F172A; --ink2:#475569; --grey:#94A3B8; --ok:#0d904f; --warn:#B45309; --bad:#C0271D;
  --f:'Microsoft YaHei','PingFang SC','Helvetica Neue',Arial,sans-serif;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{font-family:var(--f);background:var(--bg);color:var(--ink);font-size:14.5px;line-height:1.75;
  display:flex;flex-direction:column;min-height:100vh}
main{flex:1 0 auto}
.wrap{max-width:1220px;margin:0 auto;padding:20px 18px 70px;display:grid;
  grid-template-columns:236px minmax(0,1fr);gap:22px;align-items:start;width:100%}
.toc{position:sticky;top:14px;background:#fff;border:1px solid var(--line);border-radius:12px;
  padding:12px 10px;max-height:calc(100vh - 40px);overflow:auto}
.toc h4{margin:2px 8px 8px;font-size:12px;color:var(--grey);letter-spacing:.5px}
.toc .mt{margin:10px 8px 4px;font-size:11.5px;color:var(--blue);font-weight:800;letter-spacing:.5px;
  border-top:1px dashed var(--line);padding-top:8px}
.toc a{display:block;padding:6px 10px;border-radius:8px;color:var(--ink2);font-size:12.8px;
  text-decoration:none;border-left:2px solid transparent}
.toc a:hover{background:var(--tint);color:var(--blue)}
.toc a.on{background:var(--tint);color:var(--blue);font-weight:700;border-left-color:var(--blue)}
.toc a i{font-style:normal;display:inline-block;width:20px;color:var(--grey);font-weight:700}
.toc a.on i{color:var(--blue)}
@media(max-width:900px){
  .wrap{grid-template-columns:minmax(0,1fr);padding:12px 12px 56px;gap:14px}
  .toc{position:static;max-height:none}
  .toc a{display:inline-block;border-left:0;padding:5px 10px;border:1px solid var(--line);margin:2px}
  .toc .mt{display:inline-block}
}
.hero{background:linear-gradient(135deg,#0d47a1 0%,#1565c0 55%,#1a73e8 100%);color:#fff;
  border-radius:14px;padding:24px 26px;margin-bottom:20px;box-shadow:0 4px 16px rgba(13,71,161,.18)}
.hero h1{margin:0 0 8px;font-size:23px;letter-spacing:.5px;line-height:1.4}
.hero p{margin:0;font-size:13.8px;opacity:.94;line-height:1.8}
.hero .tags{margin-top:12px;display:flex;flex-wrap:wrap;gap:7px}
.hero .tags span{font-size:11.8px;background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.3);
  padding:2px 10px;border-radius:20px}
section{background:#fff;border:1px solid var(--line);border-radius:14px;padding:20px 22px;margin-bottom:20px;scroll-margin-top:14px}
.module{background:linear-gradient(135deg,#0B3E8F,#0043C1);color:#fff;border-radius:14px;
  padding:20px 22px;margin:26px 0 18px;box-shadow:0 3px 12px rgba(11,62,143,.22);scroll-margin-top:14px}
.module .mk{font-size:12px;letter-spacing:2px;opacity:.85;font-weight:700}
.module h2{margin:4px 0 6px;font-size:21px;color:#fff}
.module p{margin:0;font-size:13.4px;opacity:.92}
h2.s{font-size:19px;color:var(--deep);margin:0 0 4px;display:flex;align-items:center;gap:10px}
h2.s .no{display:inline-flex;width:26px;height:26px;border-radius:8px;background:var(--blue);color:#fff;
  font-size:14px;align-items:center;justify-content:center;font-weight:800;flex:none}
.lead{color:var(--ink2);font-size:13.5px;margin:8px 0 14px}
h3{margin:22px 0 10px;font-size:15.5px;color:var(--deep);display:flex;align-items:center;gap:8px}
h3::before{content:'';width:4px;height:15px;border-radius:2px;background:var(--blue);display:inline-block}
h4{margin:16px 0 8px;font-size:14px;color:var(--ink)}
p{margin:8px 0}
ul,ol{margin:8px 0;padding-left:24px}
li{margin:5px 0}
code{background:var(--tint);color:var(--deep);border-radius:5px;padding:1px 6px;font-size:13px;
  font-family:Consolas,Menlo,monospace}
b,strong{color:var(--deep)}
a{color:var(--blue)}
.tblwrap{border:1px solid var(--line);border-radius:10px;overflow:hidden;margin:10px 0 4px}
.tbl{width:100%;border-collapse:separate;border-spacing:0;font-size:13.5px}
.tbl th{background:#F8FAFC;color:var(--deep);font-weight:700;text-align:left;padding:10px 12px;
  border-bottom:1px solid var(--line);font-size:13px}
.tbl td{padding:10px 12px;border-bottom:1px solid #F1F4F9;vertical-align:top}
.tbl tr:last-child td{border-bottom:0}
.tbl tbody tr:hover{background:#FBFCFE}
.tbl .req{color:var(--bad);font-weight:800}
.tbl .opt{color:var(--grey)}
.tblbar{display:flex;align-items:center;gap:8px;background:#F8FAFC;border-bottom:1px solid var(--line);padding:8px 12px}
.tblbar input{flex:1;max-width:280px;border:1px solid var(--line);border-radius:8px;padding:6px 10px;
  font-family:var(--f);font-size:13px;outline:none}
.tblbar input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(0,67,193,.08)}
.tblbar span{font-size:12px;color:var(--grey)}
.grid{display:grid;gap:12px}
.c2{grid-template-columns:repeat(auto-fit,minmax(250px,1fr))}
.c3{grid-template-columns:repeat(auto-fit,minmax(226px,1fr))}
.mini{background:#FBFCFE;border:1px solid var(--line);border-radius:11px;padding:13px 15px}
.mini .t{font-weight:800;color:var(--deep);font-size:13.6px;margin-bottom:5px;display:flex;align-items:center;gap:7px}
.mini .d{font-size:13px;color:var(--ink2)}
.mini .ic{width:23px;height:23px;border-radius:7px;background:var(--tint);color:var(--blue);
  display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;flex:none}
.callout{border-radius:11px;padding:12px 15px;font-size:13.5px;margin:12px 0;border-left:4px solid}
.callout.warn{background:#FFFBEB;border-color:#F59E0B;color:#7C2D12}
.callout.bad{background:#FEF2F2;border-color:#EF4444;color:#7F1D1D}
.callout.ok{background:#F0FDF4;border-color:#22C55E;color:#14532D}
.callout.info{background:var(--tint);border-color:var(--blue);color:var(--deep)}
.callout .h{font-weight:800;display:block;margin-bottom:3px}
.eg{display:grid;grid-template-columns:repeat(auto-fit,minmax(268px,1fr));gap:10px;margin:8px 0}
.eg>div{border-radius:10px;padding:11px 13px;font-size:13px;border:1px solid}
.eg .good{background:#F0FDF4;border-color:#BBF7D0}
.eg .poor{background:#FFF7ED;border-color:#FED7AA}
.eg .lb{font-weight:800;font-size:12.5px;display:block;margin-bottom:4px}
.eg .good .lb{color:#15803D}
.eg .poor .lb{color:#B45309}
.eg .q{background:#fff;border-radius:7px;padding:7px 9px;margin-top:5px;font-size:12.7px;color:#1F2937;
  white-space:pre-wrap;line-height:1.7}
.steps{counter-reset:s;margin:14px 0}
.steps .st{display:flex;gap:12px;padding:11px 0;border-bottom:1px dashed var(--line)}
.steps .st:last-child{border-bottom:0}
.steps .st .i{counter-increment:s;flex:none;width:26px;height:26px;border-radius:50%;background:var(--tint);
  color:var(--blue);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:13px}
.steps .st .i::before{content:counter(s)}
.steps .st .c{flex:1;min-width:0}
.steps .st .c .tt{font-weight:700;color:var(--deep);font-size:13.8px}
.steps .st .c .dd{font-size:13.2px;color:var(--ink2)}
figure{margin:14px 0;background:#FBFCFE;border:1px solid var(--line);border-radius:12px;padding:14px 10px 8px}
figure svg{display:block;width:100%;height:auto}
.fig-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
.fig-scroll svg{min-width:760px}
figcaption{text-align:center;font-size:12.5px;color:var(--grey);margin-top:8px}
.tag{display:inline-block;font-size:11.5px;padding:1px 8px;border-radius:20px;background:var(--tint);
  color:var(--blue);font-weight:700;margin-right:4px}
.tag.g{background:#DCFCE7;color:#15803D}
.tag.r{background:#FEE2E2;color:#B91C1C}
.tag.y{background:#FEF3C7;color:#92400E}
.chk{display:grid;grid-template-columns:repeat(auto-fit,minmax(268px,1fr));gap:9px;margin:10px 0}
.chk .ci{display:flex;gap:9px;align-items:flex-start;background:#FBFCFE;border:1px solid var(--line);
  border-radius:10px;padding:10px 12px;font-size:13px}
.chk .bx{flex:none;width:19px;height:19px;border:2px solid var(--blue);border-radius:5px;margin-top:2px}
.quick{background:#0d2a52;color:#c9d8ee;border-radius:14px;padding:22px 24px;margin-bottom:20px}
.quick h2{color:#fff;font-size:19px;margin:0 0 10px}
.quick .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-top:6px}
.quick .qc{background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.16);border-radius:11px;padding:13px 15px}
.quick .qc .t{color:#fff;font-weight:800;font-size:13.4px;margin-bottom:6px}
.quick .qc .d{font-size:12.8px;line-height:1.85}
.quick b{color:#fff}
.quick .onel{background:#1a73e8;border-radius:10px;padding:12px 16px;margin-top:14px;font-weight:700;color:#fff;font-size:13.6px}
@media print{
  body{background:#fff;font-size:12px}
  .wrap{display:block;max-width:none;padding:0}
  .toc{display:none}
  .hero,.module,.quick{-webkit-print-color-adjust:exact;print-color-adjust:exact}
  section,.module,.hero,.quick,figure{break-inside:avoid;page-break-inside:avoid;border:1px solid #ddd}
  .fig-scroll{overflow:visible}
  .fig-scroll svg{min-width:0}
  main{padding:0}
}
"""

# ─────────────────────────── 正文 ───────────────────────────
BODY = []
A = BODY.append

A('<div class="hero">')
A('<h1>宝锐销售 · 飞书活动 / 测试反馈 / 竞品信息 录入规范培训</h1>')
A('<p>让每一次跑客户都变成<b>可检索、可统计、可追溯</b>的结构化数据。本手册把「7 大规范 + 5 核心」'
  '拆成三块可照抄的动作：飞书活动怎么录、测试反馈怎么记、竞品信息怎么沉。</p>')
A('<div class="tags"><span>面向全体销售</span><span>7 大规范</span><span>5 核心</span>'
  '<span>三大模块</span><span>正反例对照</span><span>提交前自检</span></div>')
A('</div>')

A('<div class="wrap">')
# ── TOC ──
A('<nav class="toc"><h4>目录</h4>')
A('<a href="#s0"><i>✦</i>为什么要规范</a>')
A('<div class="mt">模块一 · 飞书活动录入</div>')
A('<a href="#s1"><i>1</i>7 大规范逐条</a>')
A('<a href="#s2"><i>2</i>5 核心</a>')
A('<a href="#s3"><i>3</i>质量检查清单</a>')
A('<a href="#s4"><i>4</i>正例 / 反例对照</a>')
A('<a href="#s5"><i>5</i>常见错误</a>')
A('<a href="#sb1"><i>🧪</i>试试看 · 活动录入沙盒</a>')
A('<div class="mt">模块二 · 测试反馈</div>')
A('<a href="#s6"><i>6</i>测试单结果怎么记</a>')
A('<a href="#s7"><i>7</i>量化描述要求</a>')
A('<a href="#s8"><i>8</i>问题现象 vs 客户反馈</a>')
A('<a href="#s9"><i>9</i>附件上传规范</a>')
A('<a href="#s10"><i>10</i>常见错误</a>')
A('<a href="#sb2"><i>🧪</i>试试看 · 测试反馈沙盒</a>')
A('<div class="mt">模块三 · 竞品信息</div>')
A('<a href="#s11"><i>11</i>收什么</a>')
A('<a href="#s12"><i>12</i>品牌必须归一化</a>')
A('<a href="#s13"><i>13</i>去重规则</a>')
A('<a href="#s14"><i>14</i>沉淀流程</a>')
A('<a href="#s15"><i>15</i>常见错误</a>')
A('<a href="#sb3"><i>🧪</i>试试看 · 竞品信息沙盒</a>')
A('<div class="mt">收尾</div>')
A('<a href="#s16"><i>★</i>一页速查卡</a>')
A('<a href="#s17"><i>✓</i>提交前自检清单</a>')
A('</nav>')

A('<main>')

# ── 开篇 ──
A('<section id="s0"><h2 class="s"><span class="no">✦</span>为什么要规范：三个真实痛点</h2>')
A('<div class="lead">不规范的活动描述，会让「跑过的客户」变成一条条查不到、对不上、用不上的死数据。</div>')
A('<div class="grid c3">')
A('<div class="mini"><div class="t"><span class="ic">!</span>信息查不到</div><div class="d">'
  '客户下月要复盘，你回去翻上季度的活动，只看到一句「去客户那边聊了下，后面继续跟进」——'
  '聊了什么、测了什么货号、什么结论，全都没写。</div></div>')
A('<div class="mini"><div class="t"><span class="ic">!</span>客户名对不上</div><div class="d">'
  '录入时手打客户全称，少一个字、多一个「科技」，系统里就成了<b>两个客户</b>。'
  '按客户统计、按客户查历史，全部漏掉。</div></div>')
A('<div class="mini"><div class="t"><span class="ic">!</span>竞品情报沉不下来</div><div class="d">'
  '客户明明说过竞品的价格、性能、反馈，但因为写在活动描述正文里、没规整字段，'
  '事后只能靠人肉 grep，情报随时丢失。</div></div>')
A('</div>')
A(fig(fig1(), FIG1_CAP))
A('<div class="callout ok"><span class="h">培训后你能得到什么</span>'
  '① 一次活动 = 一条能<b>被检索到</b>的结构化记录；② 客户、商机、订单、货号<b>点选不手打</b>，永远对得上；'
  '③ 测试反馈与竞品情报<b>自动沉淀</b>进库，下次选型、报价、写话术直接调用。</div>')
A('<div class="callout info"><span class="h">一句话记住</span>'
  '<b>一个商机一套内容，参数带货号，结果挂真单，计划写到人，竞品写品牌。</b></div>')
A('</section>')

# ══════════ 模块一 ══════════
A('<div class="module" id="m1"><div class="mk">模块一</div>'
  '<h2>飞书活动录入规范</h2>'
  '<p>适用范围：所有「项目跟进 / 采购跟进」活动。下面是 7 大规范、5 个核心、字段级质量检查，'
  '以及「同一件事」的合格版与不合格版并排对照。</p></div>')

# s1 7 大规范
A('<section id="s1"><h2 class="s"><span class="no">1</span>7 大规范逐条</h2>')
A('<div class="lead">这 7 条是活动描述的撰写要求，也是抽查质检的口径。下面逐条给出「要求」与「反例特征」。</div>')
A('<div class="tblwrap"><div class="tblbar"><input data-filter="t1" '
  'placeholder="搜索规范名 / 关键词…"><span>输入即筛选</span></div>'
  '<table class="tbl" id="t1">'
  '<thead><tr><th style="width:68px">规范</th><th style="width:190px">名称</th>'
  '<th>要求（做到什么算合格）</th><th style="width:210px">反例特征</th></tr></thead><tbody>')
_rows = [
 ('1', '结构化分点', '需求与内容<b>一行一条编号</b>，一条一个诉求，不写大段散文。', '整段话术、一段到底、没有编号'),
 ('2', '技术参数 + 产品信息', '每条参数<b>带真实货号 + 量化值</b>（时间 / 灵敏度 / Ct / CV / 温度 / 月数）。', '只说「性能好、比进口便宜」，无货号无数字'),
 ('3', '问题现象 + 客户反馈', '写清<b>现象 + 量化影响</b>；客户原话放<b>引号内照抄</b>，不转述。', '「效果不太好」「客户不太满意」'),
 ('4', '测试结果挂真实订单', '关联<b>真实订单编号</b> → 带出该单全部货号 → <b>逐个货号</b>写结果 + 附件。', '「客户测了，还行」，无单号无货号'),
 ('5', '关联商机 + 客户信息', '<b>一个商机一套内容</b>；客户<b>点选</b>不手打，附商机与项目阶段。', '两个商机的内容混写在同一套里'),
 ('6', '标准化术语', '用可量化、可核对的专业术语（如「冻干球吸潮率 30%」）。', '「有点潮」「差不多」「还行」等口语'),
 ('7', '竞品对比 + 差异化', '竞品<b>品牌 + 货号 + 量化对比维度</b> + 客户评价；无竞对则留空。', '「客户也在用别家的，说还行」'),
]
for n, nm, req, bad in _rows:
    A('<tr><td><span class="tag">%s</span></td><td><b>%s</b></td><td>%s</td>'
      '<td style="color:#B45309">%s</td></tr>' % (n, nm, req, bad))
A('</tbody></table></div>')
A('<div class="callout warn"><span class="h">页面质检 7 条 · 速记版（与上面一一对应）</span>'
  '一商机一套内容 · 需求分点写 · 参数带货号＋量化 · 测试结果挂真单 · 原话照抄 · 竞对写量化 · 计划三件套。</div>')
A('</section>')

# s2 5 核心
A('<section id="s2"><h2 class="s"><span class="no">2</span>5 核心：好活动的五个特征</h2>')
A('<div class="lead">7 大规范解决「怎么填」，5 核心解决「填得好不好」——抽查时先看这五个词。</div>')
A('<div class="grid c3">')
_cores = [
 ('结构化', '分点、分块、分商机。信息有层次，不是一团文字。'),
 ('数据化', '一切结论都要有数字与货号支撑，杜绝形容词。'),
 ('可追溯', '客户 / 商机 / 订单 / 货号都能回链，谁、哪天、测了什么，查得到。'),
 ('行动导向', '每条活动都有下一步：事项 + 负责人 + 时间节点，缺一不可。'),
 ('差异化', '相对竞品的优势/差距写清楚，而不是笼统说「更好」。'),
]
for t, d in _cores:
    A('<div class="mini"><div class="t"><span class="ic">◆</span>%s</div><div class="d">%s</div></div>' % (t, d))
A('</div>')
A(fig(fig2(), FIG2_CAP))
A('</section>')

# s3 质量检查
A('<section id="s3"><h2 class="s"><span class="no">3</span>质量检查清单（字段级黄灯）</h2>')
A('<div class="lead">录入页底部有实时校验：命中的项会亮<b>黄灯</b>，全部通过才解锁「生成描述 / 复制」。'
  '黄灯是<b>字段级</b>判定（本地即时、零延迟），不是内容好坏打分。</div>')
A('<div class="tblwrap"><table class="tbl">'
  '<thead><tr><th style="width:60px">项</th><th style="width:210px">黄灯条件</th>'
  '<th>含义与怎么改</th></tr></thead><tbody>')
_chk = [
 ('①', '结构化分点未达标', '客户需求里没有 <code>1.</code> <code>2.</code> 这类编号，或不足两行 → 改成编号分点。'),
 ('②', '技术参数缺货号或量化', '货号为空，或参数里没有 <code>%</code> / <code>copy/μL</code> / <code>ct</code> / <code>×</code> / <code>倍</code> 等量化值。'),
 ('③', '客户原话为空', '问题现象里没有引号照抄的客户原话 → 把原话补上。'),
 ('⑤', '客户 / 商机未选', '客户和商机两个都必须是「点选」状态。'),
 ('⑥', '出现口语化术语', '命中「还可以 / 有点 / 不错 / 差不多 / 还行 / 凑合」等词 → 换成量化术语。'),
 ('⑦', '竞对信息不完整', '勾选了「含竞对」，但品牌或货号 / 性能为空。'),
 ('行动', '下一步计划缺失', '行动、责任人、截止日三者缺一 → 补齐「事项 / 负责人 / 时间节点」。'),
]
for n, c, m in _chk:
    A('<tr><td><span class="tag y">%s</span></td><td>%s</td><td>%s</td></tr>' % (n, c, m))
A('</tbody></table></div>')
A('<div class="callout bad"><span class="h">注意区分两类检查</span>'
  '字段级黄灯是<b>本地即时</b>的（上面这张表）；「描述写得好不好」属<b>内容级</b>，只在提交时用模型判一次，'
  '不实时。两者混用会导致频繁误报，销售就会弃用。底栏出现红字＝不能提交；'
  '出现「信息齐备，可提交（N 套跟进内容）」＝可以交。</div>')
A('</section>')

# s4 正反例
A('<section id="s4"><h2 class="s"><span class="no">4</span>正例 / 反例对照（同一件事，两种写法）</h2>')
A('<div class="lead">下面每一组都是<b>同一件事</b>。左边是合格版，右边是不合格版。'
  '示例中的客户统一匿名化为「某某生物科技有限公司」。</div>')

A('<h3>对照 1 · 客户需求</h3>')
A(eg('1. 全预混体系上机适配，需按 64T 板位预封装\n2. 要求 25℃ 常温稳定 18 个月\n3. 需要宿主核酸残留的检测报告',
     '客户想让我们配合做一个新的体系，要求还挺多的，稳定性方面他们比较关注。'))

A('<h3>对照 2 · 技术参数 / 测试数据</h3>')
A(eg('FM2181-4：扩增 20min，灵敏度 1 copy/μL，批间 CV&lt;5%\nFMD5071-4-DB：可冻干，25℃ 常温 18 个月，复溶后稳定',
     '我们的酶性能很好，比进口的便宜，客户测下来效果不错。\n（问题：没说哪个货号、没有任何数字）'))

A('<h3>对照 3 · 测试单结果跟进</h3>')
A(eg('订单 ZHBR260921046\nFM0324-074901：扩增曲线正常，Ct 提前 1.2，NTC 无起线\nFMD5071-4-DB：复溶后 25℃ 放置 7 天无衰减',
     '客户测了，结果还行。\n（问题：没说是哪一单、哪个货号，也没有可对比的数据）'))

A('<h3>对照 4 · 问题现象（含客户原话）</h3>')
A(eg('上机出现轻微非特异扩增，样品孔荧光值偏低约 15%，复孔一致；客户原话「你们的 NTC 起得比原来晚，但样品孔荧光值偏低」。',
     '客户反馈效果不太好，需要改进。\n（问题：没有现象、没有量化、没有原话）'))

A('<h3>对照 5 · 竞对信息</h3>')
A(eg('诺唯赞（X-Taq-2025）：灵敏度 5 copies/μL，我司 1 copies/μL；客户评价：交期快但单价高 15%。',
     '客户也在用别家的，说是还行。\n（问题：没有品牌、没有对比维度、没有数据）'))

A('<h3>对照 6 · 下一步计划</h3>')
A(eg('寄送 2 支样品并附测试方案 ｜ 本人 ｜ 2026-09-25\n安排技术交流确认上机时间 ｜ 技术支持 ｜ 2026-10-10',
     '继续跟进（问题：没有负责人、没有时间节点）'))
A('</section>')

# s5 常见错误
A('<section id="s5"><h2 class="s"><span class="no">5</span>常见错误</h2>')
A('<div class="grid c2">')
_errs = [
 ('两个商机写在同一套里', '客户同时谈「冻干体系」和「多重体系」，正确做法是关联两个商机、各写一套；写在一起后续无法按商机分析。'),
 ('技术参数只有形容词', '「性能好、便宜」不算参数。没有货号 + 没有数字 = 不合格。'),
 ('客户原话被转述', '规范要求原话放引号照抄，不要换成自己的话，否则细节与语气全丢。'),
 ('计划只写「继续跟进」', '缺负责人、缺时间节点，活动就没有行动导向价值。'),
 ('客户手打全称', '录入客户务必检索后点选，手打容易出现「两家人」。'),
 ('把「客户反馈」当「问题现象」', '现象是「发生了什么」，反馈是「客户怎么评价」。两者是不同字段，别合并（详见模块二第 8 节）。'),
]
for t, d in _errs:
    A('<div class="mini"><div class="t"><span class="ic">✕</span>%s</div><div class="d">%s</div></div>' % (t, d))
A('</div>')
A('</section>')

A(SB_SECTIONS[0])

# ══════════ 模块二 ══════════
A('<div class="module" id="m2"><div class="mk">模块二</div>'
  '<h2>测试反馈规范</h2>'
  '<p>测试反馈是技术、产品、研发共用的一手数据。要求：<b>关联真实订单编号 → 带出货号 → 逐个结果 + 附件</b>，'
  '并做量化描述。</p></div>')

# s6
A('<section id="s6"><h2 class="s"><span class="no">6</span>测试单结果怎么记：一条闭环</h2>')
A('<div class="lead">测试反馈模块是测试反馈数据的<b>唯二来源</b>之一（另一个是用户单独指定可计为测试反馈的记录）；'
  '其余渠道一律不计入。所以「记录顺序」必须严格按下面这条链走。</div>')
A(fig(fig3(), FIG3_CAP))
A('<div class="steps">')
_steps = [
 ('先关联真实订单编号', '检索并<b>点选</b>真实订单号（不是手工敲一串数字）。'),
 ('带出该订单全部货号', '选中订单后，系统带出这一单的所有货号（含赠品、数量）。'),
 ('勾选本次跟进的货号', '只勾选这次真正在做测试的货号；一个货号 = 一组反馈模板。'),
 ('逐个货号写结果', '每个货号单独写结果描述，写清可比对的量（Ct / 灵敏度 / 曲线 / 稳定性等）。'),
 ('逐个货号传附件', '每个货号的结果可单独挂附件：扩增曲线、原始报告、表格、照片都可以。'),
 ('归档进测试反馈模块', '结果结构化落入测试反馈模块，供后续按产品 / 客户 / 销售员统计。'),
]
for t, d in _steps:
    A('<div class="st"><span class="i"></span><div class="c"><div class="tt">%s</div><div class="dd">%s</div></div></div>' % (t, d))
A('</div>')
A('<div class="callout info"><span class="h">两条架构规则（来源：测试反馈模块重构规格 v0.2 定版）</span>'
  '① <b>一货号一组模板</b>：添加一个货号，就单独带出一组测试反馈模板，减行则同步移除；'
  '② <b>唯二数据来源</b>：本模块（结构化录入）+ 用户单独指定，其余不计入。'
  '活动录入里原有的测试字段已改为<b>只读引用</b>，不再自建测试字段。</div>')
A('<h3>关键字段（13 项，按提交人部门自动带出「反馈部门」）</h3>')
A('<div class="tblwrap"><div class="tblbar"><input data-filter="tfb" '
  'placeholder="搜索字段名…"><span>输入即筛选</span></div>'
  '<table class="tbl" id="tfb">'
  '<thead><tr><th style="width:150px">字段</th><th style="width:130px">控件</th><th>说明</th></tr></thead><tbody>')
_fb = [
 ('反馈部门', '只读文本', '按提交人所在部门<b>自动带出</b>，不手填。'),
 ('关联客户', '单行文本', '与活动录入一致，建议点选。'),
 ('关联订单', '单行文本', '真实订单编号。'),
 ('关联货号', '单行文本', '⭐ 每加一个货号，<b>触发一组反馈模板复制</b>。'),
 ('反馈日期', '单行文本', '本次反馈日期。'),
 ('是否有效反馈', '下拉单选', '有效反馈 / 无效反馈 / 待判定。'),
 ('项目背景', '多行文本', '这个项目在解决什么问题。'),
 ('具体需求参数', '多行文本', '客户给的具体指标要求。'),
 ('测试结果（语言描述）', '多行文本', '本次测试结果，量化描述。'),
 ('测试结果附件', '附件上传', '<b>不限类型 · 不限大小</b>。'),
 ('下一步计划', '多行文本', '描述。'),
 ('参与人', '单行文本', '下一步的参与人。'),
 ('时间节点', '单行文本', '下一步的时间节点。'),
]
for a, b, c in _fb:
    A('<tr><td><b>%s</b></td><td class="opt">%s</td><td>%s</td></tr>' % (a, b, c))
A('</tbody></table></div>')
A('</section>')

# s7 量化描述
A('<section id="s7"><h2 class="s"><span class="no">7</span>量化描述要求</h2>')
A('<div class="lead">测试结果的每一句结论，都要能被别人复算或核对。没数字的结论等于没结论。</div>')
A('<div class="tblwrap"><table class="tbl">'
  '<thead><tr><th style="width:210px">维度</th><th>量化写法示例</th></tr></thead><tbody>')
_q = [
 ('扩增性能', 'Ct 提前 1.2；扩增效率 98%；NTC 无起线'),
 ('灵敏度', 'LOD 1 copy/μL；可检出 pg 级总 RNA'),
 ('重复性', '批间 CV&lt;5%；复孔一致；重复性 3 次均通过'),
 ('稳定性', '25℃ 常温 18 个月；复溶后 7 天无衰减；-20℃ 不上冻'),
 ('对比结论', '相对诺唯赞 X-Taq-2025，灵敏度高 1 个数量级'),
 ('问题量化', '样品孔荧光值偏低约 15%；非特异扩增占比 &lt;3%'),
]
for a, b in _q:
    A('<tr><td><b>%s</b></td><td>%s</td></tr>' % (a, b))
A('</tbody></table></div>')
A('<h3>正例 / 反例</h3>')
A(eg('FMD5071-4-DB：复溶后 25℃ 放置 7 天无衰减，Ct 波动 &lt;0.5\nFM0324-074901：扩增曲线正常，Ct 提前 1.2，NTC 无起线',
     '客户的测试结果还可以，我们的产品要比竞品好一些，具体数据客户还没给。'))
A('</section>')

# s8 问题现象 vs 客户反馈
A('<section id="s8"><h2 class="s"><span class="no">8</span>「问题现象」和「客户反馈」是两回事</h2>')
A('<div class="lead">这两个是<b>独立字段，不要合并</b>。现象写客观发生的事，反馈写客户的态度与条件。</div>')
A('<div class="eg">'
  '<div class="good"><span class="lb">④ 问题现象（客观 · 量化 · 含原话）</span>'
  '<div class="q">上机出现轻微非特异扩增，样品孔荧光值偏低约 15%，复孔一致；'
  '客户原话「你们的 NTC 起得比原来晚，但样品孔荧光值偏低」。</div></div>'
  '<div class="good"><span class="lb">⑤ 客户反馈（客户的态度 / 条件）</span>'
  '<div class="q">客户认可灵敏度，但要求先补齐常温稳定性数据再批量采购；'
  '关注点是交期与账期（希望 30% 预付、余款 60 天）。</div></div>'
  '</div>')
A('<div class="callout info"><span class="h">判断口诀</span>'
  '<b>现象 = 发生了什么（客观、可量化、带原话）；反馈 = 客户怎么看、有什么条件（主观、含态度与商务条件）。</b>'
  '把两者混在一个字段里，后续做「产品问题归因」就会失准。</div>')
A('<h3>正例 / 反例</h3>')
A(eg('【现象】上机非特异扩增，样品孔荧光值偏低约 15%，复孔一致；客户原话「…」\n'
     '【反馈】客户认可灵敏度，要求补齐常温稳定性数据后再批量采购',
     '客户反馈效果不太好，需要改进。（问题：把现象和反馈混成一句，没有量化、没有原话、没有商务条件）'))
A('</section>')

# s9 附件
A('<section id="s9"><h2 class="s"><span class="no">9</span>附件上传规范</h2>')
A('<div class="grid c3">')
_att = [
 ('挂在哪', '附件挂在<b>货号</b>下面。先勾选货号、展开该货号的结果区，才看得到「选择附件 / 拍照上传」。'),
 ('能传什么', '类型不限、大小不限：扩增曲线、原始报告、PDF、Excel、照片都行；可多选，手机可直接调相机拍照。'),
 ('怎么访问', '附件存 CloudBase 云存储，不公开；点文件名时签发一个 <b>1 小时有效</b>的临时链接，超时重取。'),
]
for t, d in _att:
    A('<div class="mini"><div class="t"><span class="ic">📎</span>%s</div><div class="d">%s</div></div>' % (t, d))
A('</div>')
A('<h3>正例 / 反例</h3>')
A(eg('每个测试货号各挂 1 个附件：\n· FM0324-074901 → 扩增曲线.png\n· FMD5071-4-DB → 稳定性记录.xlsx',
     '整单只传一张模糊截屏，没有对应到具体货号；或结果写了「见附件」但附件没传。'))
A('</section>')

# s10 常见错误
A('<section id="s10"><h2 class="s"><span class="no">10</span>测试反馈 · 常见错误</h2>')
A('<div class="grid c2">')
_te = [
 ('没挂真实订单号', '结果直接手写货号，不关联订单，信息无法溯源到具体出货批次。'),
 ('一单一个结果', '一单测了三个货号，却只写一句话，没有逐个货号分开写结果。'),
 ('结果无量化', '「效果不错」「客户满意」这类描述，无法进入产品统计。'),
 ('把现象当反馈', '把客观现象写进「客户反馈」，或反过来，导致后续归因错位。'),
 ('附件没对应到货号', '整单塞一张图，无法判断是哪个货号的证据。'),
 ('漏填下一步', '测试完没有下一步计划（参与人 + 时间节点），测试就断了。'),
]
for t, d in _te:
    A('<div class="mini"><div class="t"><span class="ic">✕</span>%s</div><div class="d">%s</div></div>' % (t, d))
A('</div>')
A('</section>')

A(SB_SECTIONS[1])

# ══════════ 模块三 ══════════
A('<div class="module" id="m3"><div class="mk">模块三</div>'
  '<h2>竞品信息规范</h2>'
  '<p>竞品信息是选型、报价、写话术的弹药。要点：<b>品牌必须归一化</b>、按「品牌+货号+客户」去重、'
  '结构化沉淀进竞对情报库。</p></div>')

# s11 收什么
A('<section id="s11"><h2 class="s"><span class="no">11</span>收什么：竞品四要素</h2>')
A('<div class="lead">看到客户在用竞品，就记这四样：<b>品牌 / 货号 / 性能 / 客户评价</b>（有价格的再加价格与单位）。</div>')
A('<div class="grid c3">')
_comp = [
 ('品牌', '竞品的厂商名，必须<b>归一化</b>后选（见第 12 节）。书写规范：<code>厂家名称-货号（品名）</code>。'),
 ('货号', '竞品型号 / 货号。没有货号的信息，后续无法逐条对标，价值大打折扣。'),
 ('性能', '竞品的核心优势 / 宣传点，尽量量化（灵敏度、延伸速度、保真倍数…）。'),
 ('客户评价', '客户对竞品的<b>真实反馈</b>（不限内容）。其中「我们不如竞品」的几条是全表最值钱的信息，务必单独写清。'),
]
for t, d in _comp:
    A('<div class="mini"><div class="t"><span class="ic">◆</span>%s</div><div class="d">%s</div></div>' % (t, d))
A('</div>')
A('<div class="callout info"><span class="h">价格也要按规范写（内部情报）</span>'
  '到手价分「实际价格 / 非实际价格」，并写清单位与购买量：<code>实际价格：8元一支（1ml/支）；单次采购500支；</code>。'
  '单位有 5 种（元/支、元/ml、元/T、元/rxn、元/盒），<b>不能直接横向比</b>；'
  '跨产品的「元/T」也不可比（20μl 体系与 50μl 体系的 1 个 T 不等价）。'
  '⚠️ 竞品价与客户反馈属<b>内部情报</b>，禁止放进对外物料或发给客户。</div>')
A('</section>')

# s12 品牌归一化
A('<section id="s12"><h2 class="s"><span class="no">12</span>品牌必须归一化（否则产生脏键）</h2>')
A('<div class="lead">这是竞品信息里最容易出错、后果最严重的一条：<b>品牌字段不许自由输入</b>。</div>')
A('<div class="callout bad"><span class="h">反面后果</span>'
  '如果品牌靠手打，「诺唯赞 / 诺维赞 / Vazyme / 诺维赞生物」会被系统当成<b>四个不同的品牌</b>。'
  '结果就是：按品牌统计时同一条产品线被拆成好几份，占比全错，对标分析失效。</div>')
A('<h3>正确做法</h3>')
A('<div class="steps">')
_norm = [
 ('品牌字段用「下拉 + datalist」', '只能从系统收录的标准品牌里选，凡不在名单里的，原文保留但不进统计、不新增脏键。'),
 ('保存前走别名归一化', '系统内置别名表（约 20 个标准品牌），把别名映射到标准名；未知品牌原样返回，不做猜测。'),
 ('归一化后才是「同一个键」', '「诺维赞」「Vazyme」统一归一为「诺唯赞」，此后所有统计都归到同一条品牌上。'),
]
for t, d in _norm:
    A('<div class="st"><span class="i"></span><div class="c"><div class="tt">%s</div><div class="dd">%s</div></div></div>' % (t, d))
A('</div>')
A('<h3>正例 / 反例</h3>')
A(eg('品牌：诺唯赞（从下拉点选）\n货号：X-Taq-2025\n性能：灵敏度 5 copies/μL\n客户评价：交期快，单价高 15%',
     '品牌：诺维赞（手打）\n另一条又写成：Vazyme\n（问题：同一家被记成两个品牌，统计时各算一份，占比全错）'))
A('</section>')

# s13 去重
A('<section id="s13"><h2 class="s"><span class="no">13</span>去重规则：品牌 + 货号 + 客户</h2>')
A('<div class="lead">同一条竞品情报不要重复写入。去重键是一个<b>三元组</b>：</div>')
A('<div class="callout ok"><span class="h">去重键</span>'
  '<code>(brand, sku, customer)</code> —— <b>品牌 + 货号 + 客户</b>三者完全相同，则视为同一条，跳过不重复写。</div>')
A('<div class="tblwrap"><table class="tbl">'
  '<thead><tr><th style="width:300px">两条记录</th><th>判定</th></tr></thead><tbody>')
_dd = [
 ('品牌=诺唯赞，货号=X-Taq-2025，客户=某某生物',
  '<span class="tag r">重复</span>三元组完全相同 → 跳过，不写入。'),
 ('品牌=诺唯赞，货号=X-Taq-2025，客户=另一家客户',
  '<span class="tag g">保留</span>客户不同 → 是新情报（同一竞品在不同客户的反馈要分别记）。'),
 ('品牌=诺唯赞，货号=Q233，客户=某某生物',
  '<span class="tag g">保留</span>货号不同 → 是另一款产品的情报。'),
]
for a, b in _dd:
    A('<tr><td>%s</td><td>%s</td></tr>' % (a, b))
A('</tbody></table></div>')
A('<div class="callout warn"><span class="h">为什么要按客户区分</span>'
  '同一款竞品，在 A 客户是「效果好」，在 B 客户可能是「曝光时间较长」——'
  '若按品牌+货号去重，会丢掉客户侧的差异，对标就失准。</div>')
A('</section>')

# s14 沉淀流程
A('<section id="s14"><h2 class="s"><span class="no">14</span>沉淀到竞对情报库的流程</h2>')
A('<div class="lead">录入活动时顺手勾一下，情报就自动落库，不用事后人肉整理。</div>')
A(fig(fig4(), FIG4_CAP))
A('<div class="steps">')
_cp = [
 ('录入活动时勾选「含竞对」', '在「竞对信息」处勾选，展开品牌 / 货号 / 性能 / 客户评价四个字段。'),
 ('填完即结构化成独立字段', '竞对信息不再埋在活动描述正文里，而是独立字段，可靠性远高于事后 grep。'),
 ('提交时自动提取', '提交活动时，系统把竞对字段抽取出来，走品牌归一化 + 去重。'),
 ('写入竞对情报库', '落进 <code>competitor_intel.json</code>（字段：brand / sku / performance / feedback / customer / owner / time）。'),
 ('反哺复用', '情报进入选型、话术、对标分析；每周复核，反哺销售。'),
]
for t, d in _cp:
    A('<div class="st"><span class="i"></span><div class="c"><div class="tt">%s</div><div class="dd">%s</div></div></div>' % (t, d))
A('</div>')
A('<div class="callout info"><span class="h">入库字段一览</span>'
  '<code>brand</code> 品牌（归一化后）· <code>sku</code> 货号 · <code>performance</code> 性能 · '
  '<code>feedback</code> 客户评价 · <code>customer</code> 客户 · <code>owner</code> 录入人 · <code>time</code> 时间。</div>')
A('</section>')

# s15 常见错误
A('<section id="s15"><h2 class="s"><span class="no">15</span>竞品信息 · 常见错误</h2>')
A('<div class="grid c2">')
_ce = [
 ('品牌自由输入', '手打品牌名造成脏键（诺唯赞 / 诺维赞 / Vazyme 各算一个）。'),
 ('缺货号', '只记「客户在用诺唯赞」，没货号，后续无法逐条对标。'),
 ('性能只写「还行」', '没有量化维度，无法构成差异化卖点。'),
 ('漏记「我们不如」', '只记我们占优的，不记差距——恰恰差距是最值钱、最该升级成攻关任务的信息。'),
 ('重复写入', '同一品牌+货号+客户反复写，拉高统计分母。'),
 ('把内部情报外发', '竞品成交价与客户反馈属内部情报，误放进对外物料或发给客户。'),
]
for t, d in _ce:
    A('<div class="mini"><div class="t"><span class="ic">✕</span>%s</div><div class="d">%s</div></div>' % (t, d))
A('</div>')
A('</section>')

A(SB_SECTIONS[2])

# ══════════ 收尾 ══════════
A('<div class="module" id="m4"><div class="mk">收尾</div>'
  '<h2>速查卡 + 提交前自检</h2><p>可以把这一屏截图存到手机上，提交前扫一眼。</p></div>')

# s16 速查卡
A('<section id="s16"><h2 class="s"><span class="no">★</span>一页速查卡</h2>')
A('<div class="quick"><h2>宝锐销售 · 录入速查卡</h2>')
A('<div class="cards">')
A('<div class="qc"><div class="t">活动录入 · 7 大规范</div><div class="d">'
  '① 结构化分点　② 技术参数带货号＋量化　③ 问题现象＋客户反馈（原话照抄）<br>'
  '④ 测试结果挂真实订单　⑤ 关联商机＋客户信息（一商机一套）<br>'
  '⑥ 标准化术语　⑦ 竞品对比＋差异化</div></div>')
A('<div class="qc"><div class="t">5 核心</div><div class="d">'
  '结构化 · 数据化 · 可追溯 · 行动导向 · 差异化</div></div>')
A('<div class="qc"><div class="t">测试反馈 · 一条链</div><div class="d">'
  '关联真实订单 → 带出全部货号 → 逐个货号写结果 → 逐个货号传附件 → 归档模块</div></div>')
A('<div class="qc"><div class="t">竞品信息 · 四要素</div><div class="d">'
  '品牌（<b>归一化选，禁手打</b>）· 货号 · 性能 · 客户评价<br>去重键 = 品牌 + 货号 + 客户</div></div>')
A('</div>')
A('<div class="onel">一个商机一套内容，参数带货号，结果挂真单，计划写到人，竞品写品牌。</div>')
A('</div>')
A('</section>')

# s17 自检清单
A('<section id="s17"><h2 class="s"><span class="no">✓</span>提交前自检清单</h2>')
A('<div class="lead">提交前逐项打勾——有一项没打勾，就先补，别带着问题提交。</div>')
A(fig(fig5(), FIG5_CAP))
A('<div class="chk">')
_self = [
 '客户是「点选」的，不是手打全称',
 '一个商机一套内容，没有把两个商机混写',
 '技术参数每条都有货号 + 量化值',
 '测试结果挂在真实订单号上，且逐货号有结果',
 '问题现象有量化、客户原话用引号照抄',
 '竞品写了品牌（归一化）+ 货号 + 量化对比',
 '下一步计划都有「事项 / 负责人 / 时间节点」',
 '附件对应到了具体货号（不是整单一张图）',
]
for s in _self:
    A('<div class="ci"><span class="bx"></span><div>%s</div></div>' % s)
A('</div>')
A('<div class="callout ok"><span class="h">全部打勾之后</span>'
  '底栏出现「信息齐备，可提交（N 套跟进内容）」即可提交；提交后弹出可一键复制的飞书描述，'
  '粘贴到飞书活动里，由你选客户完成挂接。</div>')
A('</section>')

A('</main>')
A('</div>')

JS = r"""
(function(){
  /* 目录高亮 */
  var links = Array.prototype.slice.call(document.querySelectorAll('.toc a'));
  var secs = links.map(function(a){ return document.querySelector(a.getAttribute('href')); });
  function onScroll(){
    var y = window.scrollY + 130, cur = 0;
    for (var i=0;i<secs.length;i++){ if (secs[i] && secs[i].offsetTop <= y) cur = i; }
    links.forEach(function(a,i){ a.classList.toggle('on', i===cur); });
  }
  window.addEventListener('scroll', onScroll, {passive:true});
  onScroll();

  /* 表格字段筛选 —— 中文输入法安全：合成期间不重渲染 */
  var composing = false;
  document.addEventListener('compositionstart', function(){ composing = true; });
  document.addEventListener('compositionend', function(){ composing = false; });
  Array.prototype.forEach.call(document.querySelectorAll('input[data-filter]'), function(inp){
    inp.addEventListener('input', function(){
      if (composing) return;                       /* 拼音组合中：不触发过滤/重渲染 */
      var tid = inp.getAttribute('data-filter');
      var q = (inp.value || '').trim().toLowerCase();
      var rows = document.querySelectorAll('#' + tid + ' tbody tr');
      Array.prototype.forEach.call(rows, function(tr){
        tr.style.display = (!q || tr.textContent.toLowerCase().indexOf(q) >= 0) ? '' : 'none';
      });
    });
  });
})();
"""

HTML = ('<!DOCTYPE html>\n<html lang="zh-CN">\n<head>\n<meta charset="utf-8">\n'
        '<meta name="viewport" content="width=device-width,initial-scale=1">\n'
        '<title>宝锐销售 · 飞书活动 / 测试反馈 / 竞品信息 录入规范培训</title>\n'
        '<style>\n' + CSS + SB_CSS + '\n</style>\n</head>\n<body>\n'
        + '\n'.join(BODY) +
        '\n<script>\n' + JS + '\n' + SB_JS + '\n</script>\n</body>\n</html>\n')

# 写原始文件（不含品牌头尾）
with io.open(RAW, 'w', encoding='utf-8') as f:
    f.write(HTML)
print('raw written:', RAW, len(HTML), 'chars')
print('svg count in raw:', HTML.count('<svg'))

# ── 注入品牌（最后一步）──
sys.path.insert(0, os.path.join(W, '脚本'))
import brand_kit as BK
out = BK.inject(HTML, theme='dark', tag='营销中心 · 销售管理',
                footer_extra='内部培训资料 · 培训对象：全体销售 · 生成于 2026-10-06')
v = BK.verify(out)
assert all(v.values()), '品牌自检未过: %s' % v
with io.open(FINAL, 'w', encoding='utf-8') as f:
    f.write(out)
print('FINAL written:', FINAL, len(out), 'chars')
for k, ok in v.items():
    print('  ', 'OK' if ok else 'BAD', k)
