#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成两份交付物（发给 IT）：
  1) 产品说明/销售活动录入_产品说明.html   —— 产品规格（字段/校验/规则/数据模型/接口）
  2) 产品说明/销售活动录入_Demo.html       —— 独立可点 Demo（mock 数据、无需后端）

事实来源：工具/活动录入.html + 脚本/销售工作台服务.py（均为实测抽取，非凭记忆）
匿名化：示例客户/订单号全部虚构；不含真实客户名与销售员姓名。
"""
import io
import os
import re
import subprocess
import sys
import tempfile

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
sys.path.insert(0, os.path.join(ROOT, '脚本'))
import brand_kit as BK                                    # noqa: E402

OUTDIR = os.path.join(ROOT, '产品说明')
os.makedirs(OUTDIR, exist_ok=True)
F_SPEC = os.path.join(OUTDIR, '销售活动录入_产品说明.html')
F_DEMO = os.path.join(OUTDIR, '销售活动录入_Demo.html')

SHARED = """
*{box-sizing:border-box}
body{margin:0;background:#f5f7fa;color:#1f2328;font:14px/1.68 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif}
.wrap{max-width:1240px;margin:0 auto;padding:18px}
h1{font-size:24px;font-weight:900;color:#0F3A8A;margin:8px 0 4px}
.sub{color:#5f6368;font-size:13px;margin-bottom:14px}
.sec{background:#fff;border-radius:12px;padding:16px 18px;margin-bottom:14px;box-shadow:0 1px 3px rgba(16,24,40,.08)}
.sec>h2{font-size:16.5px;font-weight:900;color:#0F3A8A;margin:0 0 12px;border-left:4px solid #1852B6;padding-left:10px}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{padding:7px 9px;border-bottom:1px solid #EEF1F5;text-align:left;vertical-align:top}
th{background:#F0F4FB;color:#0F3A8A;font-weight:700}
tbody tr:hover{background:#F7FAFF}
code{background:#F1F4F9;border-radius:4px;padding:1px 5px;font-size:12.5px;font-family:ui-monospace,Menlo,monospace}
.req{color:#D93025;font-weight:800}
.tg{display:inline-block;border-radius:6px;padding:1px 7px;font-size:11.5px;font-weight:700;background:#E8F0FE;color:#1852B6}
.tg.g{background:#E7F6EE;color:#0E7A45}
.tg.o{background:#FFF4D6;color:#B45309}
.tg.r{background:#FDECEC;color:#C0392B}
.warn{background:#FFF8E6;border-left:4px solid #E9A800;border-radius:9px;padding:11px 14px;font-size:13px;margin-bottom:12px}
.warn b{color:#8A6100}
.note{background:#EEF4FF;border-left:4px solid #1852B6;border-radius:9px;padding:11px 14px;font-size:13px;margin-bottom:12px}
.tip{font-size:12px;color:#98A2B3}
.foot{background:#fff;border-radius:12px;padding:16px 18px;font-size:12.5px;color:#475467;line-height:1.9}
.foot h3{font-size:15px;color:#0F3A8A;margin:0 0 7px}
pre{background:#F7F9FC;border:1px solid #EEF1F5;border-radius:8px;padding:11px;font-size:12px;
  font-family:ui-monospace,Menlo,monospace;overflow:auto;max-height:420px}
.flow{display:flex;gap:0;align-items:center;overflow-x:auto;padding:8px 2px}
.fb{flex:0 0 148px;background:#F4F8FF;border:1.5px solid #C9DBF5;border-radius:10px;padding:9px 8px;text-align:center}
.fb b{display:block;color:#0F3A8A;font-size:13px}
.fb span{font-size:11px;color:#5f6368}
.ar{flex:0 0 26px;text-align:center;color:#9DB8E0;font-weight:900}
"""

# ═══════════════════════════ 产品说明 ═══════════════════════════
FLOW = [
    ('① 入口', '「拜访与活动」Tab 的「✍️ 录」/ 客户页发起', ''),
    ('② 基本信息', '客户 · 联系人 · 日期 · 活动形式 · 活动类型 · 细分', ''),
    ('③ 关联订单/商机', '检索订单→带出货号；关联商机→每商机一套跟进', ''),
    ('④ 填跟进内容', '需求 / 参数 / 问题现象 / 竞对 / 价格 / 决策链', ''),
    ('⑤ 自动生成待办', '由「问题现象 / 下一步计划」派生，可改可删', ''),
    ('⑥ 人工确认', '逐条 confirmed，未确认不入库', ''),
    ('⑦ 提交', 'POST /api/activity/submit', ''),
    ('⑧ 落库与统计', '本地 JSON + CloudBase；计入拜访统计', ''),
]
FLOW_HTML = ('<div class="flow">' + ''.join(
    ('<div class="fb"><b>%s</b><span>%s</span></div>' % (t, d)) + ('<div class="ar">▶</div>' if i < len(FLOW) - 1 else '')
    for i, (t, d, _) in enumerate(FLOW)) + '</div>')

FIELDS = [
    ('基本信息', '客户', '单选检索', '是', '必填；从客户工作项检索选择（仅本人可见范围）', '飞书「客户」工作项'),
    ('基本信息', '拜访对象 / 联系人', '文本', '否', '—', '手工填写'),
    ('基本信息', '活动日期', '日期', '否', '默认今天', '手工 / 默认'),
    ('基本信息', '活动形式', '下拉单选', '<span class="req">是</span>', '线下拜访 / 线上跟进 / 陌拜 / 协同拜访；<b>与飞书「活动类型」字段同口径</b>，拜访统计按此计数', '字典'),
    ('基本信息', '活动类型', '下拉单选', '<span class="req">是</span>', '项目跟进 / 采购跟进 / 客户档案完善；<b>决定下方跟进内容的必填项</b>', '字典'),
    ('基本信息', '采购跟进细分', '下拉单选', '<span class="req">是</span>', '拜访采购 / 跟进下单 / 催款（提交时校验）', '字典'),
    ('关联关系', '关联商机', '多选', '否', '可关联多个；<b>每关联一个，下方自动增一套跟进内容</b>', '飞书「商机」工作项'),
    ('关联关系', '订单编号', '检索多选', '否', '按单号或客户名检索，仅显示本人有权限的订单；选中后可带出该单货号', 'ERP 订单 / /api/activity/orders'),
    ('跟进内容（每套）', '客户需求', '多行文本', '条件必填', '项目跟进：与「技术参数 / 问题现象」至少填一项', '手工'),
    ('跟进内容（每套）', '技术参数', '结构化列表', '条件必填', '货号 + 描述，可多行', '手工 / 订单带出'),
    ('跟进内容（每套）', '问题现象', '多行文本', '条件必填', '<b>填写后系统自动派生待办，需人工确认</b>', '手工'),
    ('跟进内容（每套）', '下一步计划', '结构化列表', '条件必填', '至少一套必须填；<b>同样派生待办</b>', '手工'),
    ('跟进内容（每套）', '竞对 / 竞品价格', '结构化列表', '否', '勾选「已获取价格」则至少填一条（品牌 / 货号 / 单价）', '手工'),
    ('跟进内容（每套）', '决策链（决策人）', '结构化列表', '否', '姓名 / 职务 / 角色 / 影响力 / 态度 / 关注点等', '手工'),
    ('跟进内容（每套）', '主营业务 / 营收 / TOP3 供应商', '结构化', '条件必填', '「客户档案完善」类型下：与决策链等至少填一项', '手工'),
    ('跟进内容（每套）', '我司当前份额 / 主要竞对', '数值 / 文本', '否', '—', '手工'),
    ('档案类', '成立时间 / 注册资本 / 融资情况 / 人员规模 / 注册证数 / 客户行业 / 公司类型 / 资质', '文本', '否', '客户档案完善类字段', '手工 / 外部查询'),
    ('系统字段', '提交人 / 归属销售 / 创建时间 / id / 附件数 / 商机数 / 区块数', '系统生成', '自动', '提交后由服务端写入，不可编辑', '服务端'),
]

VALID = [
    ('通用', '客户', '必填'),
    ('通用', '活动形式', '必填'),
    ('通用', '采购跟进细分', '必填'),
    ('通用', '跟进内容', '至少一套'),
    ('通用', '下一步计划', '至少一套填了；部分未填时按商机名逐条提示'),
    ('通用', '价格信息', '勾选「已获取价格」的区块，至少填一条（品牌 / 货号 / 单价）'),
    ('按类型：项目跟进', '客户需求 / 技术参数 / 问题现象', '每个区块至少填一项'),
    ('按类型：客户档案完善', '档案完善内容', '决策链 / 主营业务 / 营收 / TOP3 供应商 至少填一项'),
    ('按类型：采购跟进', '采购跟进细分', '必填（拜访采购 / 跟进下单 / 催款）'),
]

RULES = [
    ('待办生成', '<b>自动生成 + 提交前人工确认</b>：由「问题现象」「下一步计划」派生，每条带稳定 uid；'
                 '销售可改措辞、可标记删除、可勾选确认。提交时<b>只提交 <code>confirmed 且未 removed</code></b> 的条目。'
                 '手工新建的待办永不被活动覆盖（靠指纹 upsert）。'),
    ('订单关联', '只能检索到<b>本人有权限</b>的订单（服务端过滤）；选中订单后可带出该单货号，用于技术参数/测试关联。'),
    ('技术支持', '<b>只留入口</b>：活动可标记「需技术支持跟进」，落库并打标，<b>不派单、不跨人镜像</b>。'),
    ('草稿', '填写过程自动存草稿（浏览器本地），重开页面出现「恢复草稿」条，可选择恢复或丢弃。'),
    ('附件', '支持上传附件；对外用短时有效的下载地址。'),
    ('归属', '提交人 = 登录人；活动计入「拜访与活动」统计（按 部门 → 销售 → 客户 下钻）。'),
]

MODEL = """{
  "id": "ACT20261008143012",
  "created": "2026-10-08 14:30:12",
  "submitted_by": "(登录人姓名)",
  "sales": "(登录人姓名)",
  "customer": "(客户名)",
  "contact": "(联系人)",
  "date": "2026-10-08",
  "channel": "offline | online | cold | joint",
  "type": "项目跟进 | 采购跟进 | 客户档案完善",
  "subtype": "拜访采购 | 跟进下单 | 催款",
  "order_refs": ["ZHBR26xxxxxxx"],
  "order_ref_nos": ["ZHBR26xxxxxxx"],
  "order_no": "(主订单号)",
  "blocks": [
    {
      "opp": "(关联商机名) / 空",
      "opp_status": "", "opp_url": "",
      "need": "(客户需求)",
      "params":  [{"sku": "(货号)", "desc": "(描述)"}],
      "issue":   "(问题现象)",
      "feedback": "", "plan": [{"text": "(下一步计划)", "uid": "(稳定uid)"}],
      "competitors": [{"brand": "", "sku": "", "note": ""}],
      "prices":  [{"brand": "", "sku": "", "spec": "", "qty": "", "price": "", "source": ""}],
      "price_on": false,
      "decision": [{"name": "", "title": "", "role": "", "power": "", "phone": "", "note": ""}],
      "biz_desc": "", "revenues": [{"year": "", "amt": "", "source": ""}],
      "suppliers": [{"name": "", "project": ""}],
      "self_share": "", "main_rival": ""
    }
  ],
  "todos": [{"text": "", "due": "", "type": "", "scope": "", "tech_support": false}],
  "feishu_text": "(渲染后的飞书可读文本)",
  "_idx": {"sku_count": 0, "att_count": 0, "opp_count": 0, "block_count": 1}
}"""

APIS = [
    ('GET', '/api/activity/options', '活动类型 / 细分 / 下拉选项字典', '登录用户'),
    ('GET', '/api/activity/orders?q=&limit=', '订单检索（服务端按权限过滤）', '登录用户（仅本人可见订单）'),
    ('GET', '/api/activity/order_skus?no=', '按订单号取货号明细', '登录用户'),
    ('POST', '/api/activity/submit', '提交活动：落库（本地 JSON + CloudBase）+ 待办 upsert', '登录用户'),
    ('GET', '/api/activity/list', '活动列表（按权限过滤）', '登录用户'),
    ('GET', '/api/activity/detail?id=', '活动详情（含飞书可读文本）', '登录用户（越权 403）'),
    ('POST', '/api/activity/attachment', '上传附件', '登录用户'),
    ('GET', '/api/activity/attachment_url?id=', '取附件短时下载地址', '登录用户'),
    ('GET', '/api/users/roster', '人员名册（用于归属展示）', '登录用户'),
    ('GET/POST', '/api/todos', '待办读写（活动派生的待办落此）', '登录用户'),
]

spec = f"""<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>销售活动录入 · 产品说明</title><style>{SHARED}</style></head><body><div class="wrap">

<h1>销售活动录入 · 产品说明</h1>
<div class="sub"><span class="tg">营销中心 · 销售管理</span> 供 IT 评估与实现参考 ｜ 版本 v1.0 ｜ 2026-10-08
 ｜ 现状：<b>已有可用实现</b>（宝锐销售工作台「拜访与活动 → ✍️ 录」）</div>

<div class="note"><b>这份文档是什么：</b>「销售活动录入」页的完整产品规格 —— 字段、校验、业务规则、数据模型、接口。
配套 <b>可点 Demo</b>（<code>销售活动录入_Demo.html</code>，独立打开、无需后端）可把流程走一遍。
文中示例均为<b>虚构数据</b>，不含真实客户名与销售员姓名。</div>

<div class="sec"><h2>一、定位与使用场景</h2>
<b>一句话：</b>销售在客户现场 / 电话后，用一页把「这次做了什么、客户要什么、下一步谁跟进」结构化录下来，<b>顺带把待办生成掉</b>。<br><br>
<table><thead><tr><th style="width:150px">场景</th><th>说明</th></tr></thead><tbody>
<tr><td>拜访后即时录入</td><td>从「拜访与活动」页的「✍️ 录」进入，提交后自动回到「看」并计入统计</td></tr>
<tr><td>客户页发起</td><td>带 <code>?customer=</code> 参数进入，客户字段预填</td></tr>
<tr><td>补录</td><td>支持改活动日期（默认今天）</td></tr>
<tr><td>草稿续填</td><td>中途离开不丢，重开出现「恢复草稿」</td></tr>
</tbody></table></div>

<div class="sec"><h2>二、角色与权限</h2>
<table><thead><tr><th style="width:130px">角色</th><th>能做什么</th><th style="width:230px">数据可见范围</th></tr></thead><tbody>
<tr><td>销售（sales）</td><td>新建 / 提交活动；检索并关联<b>自己的</b>订单与商机</td><td>仅本人提交的活动与订单</td></tr>
<tr><td>部门经理 / 总监 / 管理员</td><td>查看下属活动、按部门/销售下钻统计</td><td>管辖范围；admin·gm 全量</td></tr>
<tr><td>未登录</td><td>不可进入（提示未登录，不出任何数据）</td><td>—</td></tr>
</tbody></table>
<div class="tip">权限在<b>服务端</b>过滤，前端过滤只作纵深防御。越权访问活动详情返回 403。</div></div>

<div class="sec"><h2>三、页面结构与流程</h2>
{FLOW_HTML}
<div class="tip">页面为单页表单（客户 → 关系 → 跟进内容 → 待办确认 → 提交），提交前一次性校验；<b>不做实时校验</b>（与现网实现一致）。</div></div>

<div class="sec"><h2>四、字段说明</h2>
<table><thead><tr><th style="width:110px">分组</th><th style="width:170px">字段</th><th style="width:96px">类型</th>
<th style="width:74px">必填</th><th>校验 / 说明</th><th style="width:150px">取值来源</th></tr></thead><tbody>
{''.join('<tr><td>%s</td><td><b>%s</b></td><td>%s</td><td>%s</td><td>%s</td><td class="tip">%s</td></tr>' % f for f in FIELDS)}
</tbody></table></div>

<div class="sec"><h2>五、校验规则（提交时执行）</h2>
<table><thead><tr><th style="width:190px">分组</th><th style="width:250px">校验项</th><th>规则</th></tr></thead><tbody>
{''.join('<tr><td>%s</td><td>%s</td><td>%s</td></tr>' % v for v in VALID)}
</tbody></table>
<div class="tip">校验失败时一次性列出全部缺失项（如「客户、活动形式、下一步计划」），并禁用提交按钮至通过。</div></div>

<div class="sec"><h2>六、业务规则</h2>
<table><thead><tr><th style="width:130px">主题</th><th>规则</th></tr></thead><tbody>
{''.join('<tr><td><b>%s</b></td><td>%s</td></tr>' % r for r in RULES)}
</tbody></table></div>

<div class="sec"><h2>七、数据模型</h2>
<pre>{MODEL}</pre>
<div class="tip">服务端对 <code>blocks[]</code> 做<b>逐字段白名单</b>清洗（未列字段静默丢弃）；空块整块丢弃。
索引 <code>_index.json</code> 只保留轻字段（id / created / submitted_by / type / customer / date / channel / 各类计数），统计与列表读索引，不读全量。</div></div>

<div class="sec"><h2>八、接口清单</h2>
<table><thead><tr><th style="width:80px">方法</th><th style="width:270px">路径</th><th>说明</th><th style="width:190px">权限</th></tr></thead><tbody>
{''.join('<tr><td><span class="tg %s">%s</span></td><td><code>%s</code></td><td>%s</td><td>%s</td></tr>' % ('g' if m == 'GET' else 'o', m, p, d, a) for m, p, d, a in APIS)}
</tbody></table></div>

<div class="sec"><h2>九、与其它模块的关系</h2>
<table><thead><tr><th style="width:170px">关联模块</th><th>关系</th></tr></thead><tbody>
<tr><td>待办</td><td>活动派生的待办写入待办模块（对内 / 对外分层）；<b>按指纹 upsert，只更新变动项，绝不动完成状态与结论</b></td></tr>
<tr><td>拜访与活动（看板）</td><td>活动是统计来源；按<b>活动形式</b>分桶（线下拜访 / 线上跟进 / 陌拜 / 协同拜访），支持 部门→销售→客户 下钻</td></tr>
<tr><td>客户 / 商机 / 订单</td><td>分别取自飞书「客户」「商机」与 ERP 订单；订单金额口径以 ERP 为准</td></tr>
<tr><td>技术支持</td><td>仅标记 + 留入口，不做派单</td></tr>
</tbody></table></div>

<div class="sec"><h2>十、交付现状与待 IT 决策</h2>
<div class="note"><b>现状：已有可用实现</b>（本机服务 + 工作台页面），不是从零开始。IT 可选用「直接对接现网接口」或「参照本规格自研复刻」两条路径。</div>
<table><thead><tr><th style="width:52px">#</th><th style="width:270px">待决策项</th><th>可选方案</th></tr></thead><tbody>
<tr><td>1</td><td>录入入口放在哪里</td><td>沿用现网工作台 / 并入飞书 / 并入 ERP —— 影响登录态与权限来源</td></tr>
<tr><td>2</td><td>活动形式字典是否与飞书对齐</td><td><b>建议对齐</b>（线下拜访 / 线上跟进 / 陌拜 / 协同拜访），否则按形式统计会对不上</td></tr>
<tr><td>3</td><td>待办的确认动作是否保留</td><td>现网为「自动生成 → 人工确认」，建议保留（避免误派活）</td></tr>
<tr><td>4</td><td>订单/商机数据来源</td><td>订单取 ERP、客户/商机取飞书；两边靠单号与工作项 id 关联</td></tr>
<tr><td>5</td><td>附件与移动端</td><td>是否需要拍照上传 / 移动端适配</td></tr>
</tbody></table></div>

<div class="foot"><h3>附：配套文件</h3>
① <b>销售活动录入_Demo.html</b> —— 独立可点的流程 Demo（mock 数据、无需后端、不写本机存储）<br>
② 现网实现：工作台「拜访与活动」页 →「✍️ 录」<br>
③ 本规格依据现网代码抽取，字段名、校验分支、接口路径均与实现一致。<br>
<span class="tip">本文件供内部与 IT 评估使用。</span>
</div>
</div></body></html>
"""

# ═══════════════════════════ Demo ═══════════════════════════
DEMO_CSS = """
.wiz{display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap}
.wiz div{flex:1;min-width:150px;background:#fff;border-radius:10px;padding:9px 12px;border:1.5px solid #E3E9F2;font-size:12.5px;color:#5f6368}
.wiz div.on{border-color:#1852B6;background:#EEF4FF;color:#0F3A8A;font-weight:800}
.wiz div.done{border-color:#7DC49A;background:#F1FAF4;color:#0E7A45}
.frm{background:#fff;border-radius:12px;padding:16px 18px;margin-bottom:14px;box-shadow:0 1px 3px rgba(16,24,40,.08)}
.frm h2{font-size:16px;color:#0F3A8A;margin:0 0 12px;border-left:4px solid #1852B6;padding-left:10px}
.row{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}
@media(max-width:760px){.row{grid-template-columns:1fr}}
.f label{display:block;font-size:12.5px;color:#475467;margin-bottom:4px;font-weight:700}
.f input,.f select,.f textarea{width:100%;border:1px solid #D0D5DD;border-radius:8px;padding:7px 10px;font-size:13px;font-family:inherit;background:#fff}
.f textarea{min-height:64px;resize:vertical}
.f input:focus,.f select:focus,.f textarea:focus{outline:none;border-color:#1852B6;box-shadow:0 0 0 3px #E8F0FE}
.blk{border:1px solid #E3E9F2;border-radius:10px;padding:12px;margin-bottom:11px;background:#FAFCFF}
.blk h4{margin:0 0 9px;font-size:13.5px;color:#0F3A8A}
.btn{border:1px solid #D0D5DD;background:#fff;border-radius:8px;padding:7px 13px;font-size:13px;cursor:pointer;color:#344054}
.btn:hover{background:#F2F6FD;border-color:#1852B6;color:#1852B6}
.btn.p{background:#1852B6;border-color:#1852B6;color:#fff}.btn.p:hover{background:#0F3A8A}
.btn.p:disabled{background:#B9C6DA;border-color:#B9C6DA;cursor:not-allowed}
.bar{display:flex;gap:9px;align-items:center;flex-wrap:wrap;margin-top:6px}
.chk{display:flex;align-items:center;gap:7px;font-size:13px;margin:6px 0}
.chk input{width:auto}
.res{border-left:4px solid #C0392B;background:#FDECEC;border-radius:9px;padding:10px 13px;font-size:13px;margin-bottom:11px}
.res.ok{border-color:#0E7A45;background:#F1FAF4}
.res b{color:#922B21}.res.ok b{color:#0E7A45}
.todo{background:#FFF8E6;border:1px solid #F0DFAE;border-radius:9px;padding:9px 12px;margin-bottom:8px;font-size:13px}
.todo .h{display:flex;align-items:center;gap:8px;font-weight:800;color:#8A6100}
.todo.g{background:#F1FAF4;border-color:#B6DFC8}.todo.g .h{color:#0E7A45}
.ord{display:flex;gap:8px;align-items:center;background:#F4F8FF;border:1px solid #D6E5FB;border-radius:9px;padding:8px 11px;margin-bottom:7px;font-size:13px}
.ord b{font-family:ui-monospace,Menlo,monospace;color:#1852B6}
"""

demo = f"""<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>销售活动录入 · Demo</title><style>{SHARED}{DEMO_CSS}</style></head><body><div class="wrap">

<h1>销售活动录入 · Demo</h1>
<div class="sub"><span class="tg">流程演示</span> 独立打开即可走完一次录入 ｜ 数据为<b>虚构示例</b>，不连后端、不写本机存储</div>

<div class="warn"><b>说明：</b>本 Demo 与现网实现一致 —— <b>提交时</b>一次性校验（现网不做实时校验）。
点「一键填充示例」看合格流程，点「一键清空」看校验如何逐条提示。</div>

<div class="wiz" id="wiz">
  <div class="on">① 基本信息</div><div>② 关联订单</div><div>③ 跟进内容</div><div>④ 待办确认</div><div>⑤ 提交</div>
</div>

<div class="frm">
  <h2>① 基本信息</h2>
  <div class="row">
    <div class="f"><label>客户 <span class="req">*</span></label>
      <select id="customer"><option value="">请选择…</option>
        <option>某某大学生命科学学院</option><option>某某医科大学</option><option>某某大学附属第一医院</option></select></div>
    <div class="f"><label>拜访对象 / 联系人</label><input id="contact" placeholder="如：张老师"></div>
    <div class="f"><label>活动日期</label><input id="date" type="date"></div>
    <div class="f"><label>活动形式 <span class="req">*</span></label>
      <select id="channel"><option value="">请选择…</option>
        <option value="offline">线下拜访</option><option value="online">线上跟进</option>
        <option value="cold">陌拜</option><option value="joint">协同拜访</option></select></div>
    <div class="f"><label>活动类型 <span class="req">*</span></label>
      <select id="type"><option value="">请选择…</option>
        <option>项目跟进</option><option>采购跟进</option><option>客户档案完善</option></select></div>
    <div class="f"><label>采购跟进细分 <span class="req">*</span></label>
      <select id="subtype"><option value="">请选择…</option>
        <option>拜访采购</option><option>跟进下单</option><option>催款</option></select></div>
  </div>
</div>

<div class="frm">
  <h2>② 关联订单（演示：检索后勾选）</h2>
  <div class="bar" style="margin:0 0 10px">
    <input id="oq" placeholder="输入单号或客户名检索…" style="max-width:280px;border:1px solid #D0D5DD;border-radius:8px;padding:7px 10px;font-size:13px">
    <button class="btn" onclick="searchOrd()">检索</button>
    <span class="tip" id="omsg">演示数据：3 张订单</span>
  </div>
  <div id="ordlist"></div>
</div>

<div class="frm">
  <h2>③ 跟进内容（每关联一个商机 = 一套）</h2>
  <div class="f"><label>关联商机</label>
    <select id="opp"><option value="">（不关联商机）</option>
      <option>某某课题组 qPCR 项目</option><option>某某大学 RT-qPCR 试用</option></select></div>
  <div class="blk" style="margin-top:12px">
    <h4>跟进内容 · 第 1 套</h4>
    <div class="f" style="margin-bottom:10px"><label>客户需求</label><textarea id="need" placeholder="例：需要把 RNA 检出时间压到 1 小时内"></textarea></div>
    <div class="f" style="margin-bottom:10px"><label>技术参数（货号 + 说明，用「|」分隔多行）</label>
      <textarea id="params" placeholder="例：BR1D303 | 一管式 cDNA，5 min&#10;BR1E102 | 快速 qPCR，30 min 40 循环"></textarea></div>
    <div class="f" style="margin-bottom:10px"><label>问题现象 <span class="tip">（填写后会自动派生待办）</span></label>
      <textarea id="issue" placeholder="例：客户反馈体系 Ct 偏高，怀疑逆转录效率不足"></textarea></div>
    <div class="f" style="margin-bottom:10px"><label>下一步计划 <span class="tip">（至少一套必填；同样派生待办）</span></label>
      <textarea id="plan" placeholder="例：10/10 前寄 BR1D303 试用装并跟测试结果"></textarea></div>
    <label class="chk"><input type="checkbox" id="price_on" onchange="render()"> 已获取竞品价格</label>
    <div id="priceBox" style="display:none;margin-top:8px">
      <div class="f"><label>竞品价格（品牌 | 货号 | 单价，用「|」分隔）</label>
        <textarea id="prices" placeholder="例：某品牌 | ABC-100 | 1200"></textarea></div>
    </div>
  </div>
</div>

<div class="frm">
  <h2>④ 待办确认 <span class="tip">（系统自动生成，提交前可改可删；只有确认的才入库）</span></h2>
  <div id="todos"></div>
  <div class="tip">取消勾选 = 不入库；现网规则相同。</div>
</div>

<div class="frm">
  <h2>⑤ 提交</h2>
  <div id="res" class="res" style="display:none"></div>
  <div class="bar">
    <button class="btn p" id="btnSubmit" onclick="doSubmit()">提交活动</button>
    <button class="btn" onclick="fillDemo()">一键填充示例</button>
    <button class="btn" onclick="clearAll()">一键清空</button>
    <button class="btn" onclick="copyJSON()">复制活动 JSON</button>
    <span class="tip" id="cmsg"></span>
  </div>
  <pre id="out" style="display:none;margin-top:12px"></pre>
</div>

<div class="foot"><h3>这个 Demo 演示了什么</h3>
① 表单结构与字段（与现网一致：客户 / 联系人 / 日期 / 活动形式 / 活动类型 / 采购跟进细分）；<br>
② 订单检索与勾选（真实页在服务端按权限过滤）；<br>
③ 跟进内容成套填写（需求 / 参数 / 问题现象 / 下一步计划 / 竞品价格）；<br>
④ <b>问题现象与下一步计划自动派生待办，提交前人工确认</b>（只提交确认且未删除的）；<br>
⑤ 提交时一次性校验并逐条提示 —— 与现网一致（现网<b>不做</b>实时校验）。<br>
<span class="tip">本 Demo 纯前端、无网络请求、不写浏览器存储；示例客户与订单号均为虚构。</span>
</div>
</div>

<script>
var ORDERS = [
  {{no:'ZHBR26xxxxx01', cust:'某某大学生命科学学院', date:'2026-10-06', skus:['BR1D303','BR1E102']}},
  {{no:'ZHBR26xxxxx02', cust:'某某医科大学', date:'2026-09-28', skus:['BR1C103']}},
  {{no:'ZHBR26xxxxx03', cust:'某某大学生命科学学院', date:'2026-09-15', skus:['BR1E102']}}
];
var picked = [];
function h(s){{return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}}
function txt(id){{var e=document.getElementById(id);return e?String(e.value||'').trim():'';}}
function searchOrd(){{
  var q=(document.getElementById('oq').value||'').trim().toLowerCase();
  var list=q?ORDERS.filter(function(o){{return (o.no+o.cust).toLowerCase().indexOf(q)>=0;}}):ORDERS;
  document.getElementById('omsg').textContent = q? ('命中 '+list.length+' 张') : '演示数据：3 张订单';
  renderOrds(list);
}}
function renderOrds(list){{
  list = list || ORDERS;
  var h2='';
  list.forEach(function(o){{
    var on = picked.indexOf(o.no)>=0;
    h2 += '<div class="ord"><input type="checkbox" data-no="'+h(o.no)+'" '+(on?'checked':'')+' onchange="toggleOrd(this)">'
       + '<b>'+h(o.no)+'</b><span>'+h(o.cust)+'</span><span class="tip">'+h(o.date)+' ｜ 货号 '+o.skus.length+' 个</span></div>';
  }});
  document.getElementById('ordlist').innerHTML = h2 || '<div class="tip">无匹配</div>';
}}
function toggleOrd(cb){{
  var no=cb.getAttribute('data-no');
  if(cb.checked){{ if(picked.indexOf(no)<0) picked.push(no); }}
  else picked = picked.filter(function(x){{return x!==no;}});
  render();
}}
function lines(id){{return (txt(id)||'').split('\\n').map(function(x){{return x.trim();}}).filter(Boolean);}}
function todosOf(){{
  var out=[];
  var iss=txt('issue'), pl=txt('plan');
  if(iss) out.push({{uid:'T1',text:'【问题跟进】'+iss.slice(0,40),src:'问题现象',done:true}});
  if(pl)  out.push({{uid:'T2',text:'【后续计划】'+pl.slice(0,40),src:'下一步计划',done:true}});
  return out;
}}
function render(){{
  document.getElementById('priceBox').style.display = document.getElementById('price_on').checked?'':'none';
  var wiz=document.getElementById('wiz').children;
  var step = txt('customer')&&txt('channel')&&txt('type') ? (picked.length? (txt('need')||txt('params')||txt('issue')?3:2):2) : 0;
  for(var i=0;i<wiz.length;i++) wiz[i].className = window.__done ? 'done' : (i<step?'done':(i===step?'on':''));
  renderOrds();
  window.__td = window.__td || {{}};
  var ts = todosOf();
  var html='';
  if(!ts.length) html='<div class="tip">填了「问题现象」或「下一步计划」后，这里会出现自动生成的待办。</div>';
  ts.forEach(function(t){{
    var ck = window.__td[t.uid]===undefined ? t.done : window.__td[t.uid];
    html += '<div class="todo '+(ck?'g':'')+'"><label class="chk"><input type="checkbox" '+(ck?'checked':'')
         + ' onchange="window.__td[\\''+t.uid+'\\']=this.checked;render()"> <span class="h">待办 · 来源：'+h(t.src)+'</span></label>'
         + '<div style="margin-top:4px">'+h(t.text)+'</div></div>';
  }});
  document.getElementById('todos').innerHTML = html;
}}
function payload(){{
  var ts = todosOf().filter(function(t){{return (window.__td[t.uid]===undefined?t.done:window.__td[t.uid]);}});
  return {{
    customer: txt('customer'), contact: txt('contact'), date: txt('date'),
    channel: txt('channel'), type: txt('type'), subtype: txt('subtype'),
    order_refs: picked, opp: txt('opp'),
    blocks: [{{
      opp: txt('opp'), need: txt('need'),
      params: lines('params').map(function(s){{var p=s.split('|');return {{sku:(p[0]||'').trim(),desc:(p[1]||'').trim()}};}}),
      issue: txt('issue'), plan: lines('plan').map(function(s,i){{return {{text:s,uid:'P'+(i+1)}};}}),
      price_on: document.getElementById('price_on').checked,
      prices: lines('prices').map(function(s){{var p=s.split('|');return {{brand:(p[0]||'').trim(),sku:(p[1]||'').trim(),price:(p[2]||'').trim()}};}})
    }}],
    todos: ts
  }};
}}
function validate(){{
  var d=payload(), miss=[];
  if(!d.customer) miss.push('客户');
  if(!d.channel) miss.push('活动形式（线下拜访 / 线上跟进 / 陌拜 / 协同拜访）');
  if(!d.subtype) miss.push('采购跟进细分');
  var b=d.blocks[0];
  if(!(b.need||b.params.length||b.issue)) miss.push('客户需求 / 技术参数 / 问题现象（至少填一项）');
  if(!b.plan.length) miss.push('下一步计划');
  if(b.price_on && !b.prices.length) miss.push('价格信息（已勾选，请至少填一条：品牌/货号/单价）');
  return {{d:d, miss:miss}};
}}
function doSubmit(){{
  var r=validate(), box=document.getElementById('res');
  box.style.display='';
  if(r.miss.length){{
    box.className='res';
    window.__done=false;
    box.innerHTML='<b>提交未通过，请补全 '+r.miss.length+' 项：</b><br>'+r.miss.map(function(x){{return '· '+h(x);}}).join('<br>');
    document.getElementById('out').style.display='none';
    return;
  }}
  window.__done = true;
  box.className='res ok';
  box.innerHTML='<b>提交成功。</b>活动已落库，并计入「拜访与活动」统计（活动形式：'
    + h(document.getElementById('channel').selectedOptions[0].textContent) + '）。派生待办 '+r.d.todos.length+' 条。';
  var o=document.getElementById('out');
  o.style.display=''; o.textContent=JSON.stringify(r.d,null,1);
  render();
}}
function fillDemo(){{
  document.getElementById('customer').value='某某大学生命科学学院';
  document.getElementById('contact').value='张老师';
  document.getElementById('date').value='2026-10-08';
  document.getElementById('channel').value='offline';
  document.getElementById('type').value='项目跟进';
  document.getElementById('subtype').value='跟进下单';
  document.getElementById('opp').value='某某课题组 qPCR 项目';
  document.getElementById('need').value='希望把 RNA 检出整体时间压到 1 小时内，减少手工步骤';
  document.getElementById('params').value='BR1D303 | 一管式 cDNA，gDNA 清除+逆转录同管，最快 5 min\\nBR1E102 | 快速 qPCR，30 min 跑完 40 循环';
  document.getElementById('issue').value='客户反馈现有体系 Ct 偏高，怀疑逆转录效率不足';
  document.getElementById('plan').value='10/10 前寄 BR1D303 试用装，10/15 前回收测试数据';
  document.getElementById('price_on').checked=true;
  document.getElementById('prices').value='某品牌 | ABC-100 | 1200';
  picked=['ZHBR26xxxxx01'];
  window.__td={{}};
  window.__done=false;
  document.getElementById('out').style.display='none';
  document.getElementById('res').style.display='none';
  render();
}}
function clearAll(){{
  ['customer','channel','type','subtype','opp'].forEach(function(i){{document.getElementById(i).value='';}});
  ['contact','date','need','params','issue','plan','prices','oq'].forEach(function(i){{document.getElementById(i).value='';}});
  document.getElementById('price_on').checked=false;
  picked=[]; window.__td={{}}; window.__done=false;
  document.getElementById('out').style.display='none';
  document.getElementById('res').style.display='none';
  render();
}}
function copyJSON(){{
  var s=JSON.stringify(payload(),null,1);
  var done=function(){{document.getElementById('cmsg').textContent='已复制';}};
  if(navigator.clipboard) navigator.clipboard.writeText(s).then(done,function(){{fb(s,done);}}); else fb(s,done);
}}
function fb(s,done){{
  var ta=document.createElement('textarea');ta.value=s;document.body.appendChild(ta);ta.select();
  try{{document.execCommand('copy');done();}}catch(e){{document.getElementById('cmsg').textContent='复制失败，请手动选择';}}
  document.body.removeChild(ta);
}}
document.getElementById('date').value='2026-10-08';
render();
</script>
</body></html>
"""

for path, html_, tag, extra in [(F_SPEC, spec, '营销中心 · 销售管理', '销售活动录入 · 产品说明（供 IT）'),
                                (F_DEMO, demo, '营销中心 · 销售管理', '销售活动录入 · Demo（虚构数据）')]:
    out = BK.inject(html_, theme='light', tag=tag, footer_extra=extra)
    io.open(path, 'w', encoding='utf-8').write(out)
    print('=== %s ===' % os.path.basename(path))
    vr = BK.verify(out)
    bad = [k for k, v in vr.items() if not v]
    print(('✅' if not bad else '❌'), '品牌套件', '全 True' if not bad else bad)
    ext = re.findall(r'(?:src|href)\s*=\s*["\'](?:https?:)?//', out)
    print(('✅' if not ext else '❌'), '外链 %d' % len(ext))
    blocks = re.findall(r'<script[^>]*>(.*?)</script>', out, re.S)
    f = 0
    for b in blocks:
        fd, p = tempfile.mkstemp(suffix='.js'); os.write(fd, b.encode()); os.close(fd)
        r = subprocess.run(['node', '--check', p], capture_output=True, text=True)
        if r.returncode:
            f += 1
            print('   ❌', r.stderr.strip().splitlines()[-1][:110])
        os.unlink(p)
    print(('✅' if not f else '❌'), 'node --check：%d 块 / %d 报错' % (len(blocks), f))
    print('   %.0f KB' % (os.path.getsize(path) / 1024.0))

# 匿名化：Demo 不得出现真实客户/销售员名
raw = re.sub(r'data:image/[^"\')]{200,}', '', io.open(F_DEMO, encoding='utf-8').read())
import json as _json
cust = {c['客户'] for c in _json.load(io.open(ROOT + '/销售分析/科研销售_按人与区域_20260820-20260930.json', encoding='utf-8'))['customers']}
ppl = {p['销售员'] for p in _json.load(io.open(ROOT + '/销售分析/科研销售_按人与区域_20260820-20260930.json', encoding='utf-8'))['people']}
hit = sorted([n for n in (cust | ppl) if n and len(n) >= 2 and n in raw])
print('\n=== 匿名化检查（Demo）===')
print(('✅' if not hit else '❌'), '真实客户/销售员名残留 %d 个 %s' % (len(hit), hit[:5]))
