#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""PCR 机理分割动画（自包含）：要素 × 结构域 × 分步演示
输出：销售分析/PCR机理动画_要素与结构域_2026-10-17.html
"""
import io
import sys

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

W = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
OUT = W + '/销售分析/PCR机理动画_要素与结构域_2026-10-17.html'

STEPS = [
    ('0', '待机：体系里有什么',
     '模板 DNA、引物、dNTP、Mg²⁺、缓冲体系、聚合酶、探针（探针法）同时存在。此时酶未结合 DNA。',
     '要素：模板 / 引物 / dNTP / Mg²⁺ / 酶', '—'),
    ('1', '① 变性 94–95℃',
     '双链氢键断开，两条链分开。聚合酶必须在这一步不失活——耐热性看的是整酶构象稳定性，不是单一催化位点。',
     '要素：温度 · 酶的热稳定性', 'Taq/Tth 聚合酶域整体'),
    ('2', '② 退火 55–65℃',
     '引物与模板互补配对。手指域（fingers）识别引物–模板复合物并闭合；Mg²⁺ 中和磷酸骨架电荷，让配对稳定。',
     '要素：引物 · Mg²⁺ · 缓冲 pH', '手指域（291–420）'),
    ('3', '③ 延伸 72℃',
     'dNTP 进入手掌域（palm）催化中心，Mg²⁺ 辅助磷酸转移，dNTP 掺入并释放焦磷酸；拇指域（thumb）夹住双链 DNA，决定持续合成能力。',
     '要素：dNTP · Mg²⁺ · 酶催化中心', '手掌域（421–610）+ 拇指域（611–832）'),
    ('4', '④ 探针水解（Taq 探针法）',
     'Taq 的 5′→3′ 外切酶域在延伸/置换时切开发光探针，荧光基团与淬灭基团分离 → 产生荧光信号。没有这个域，探针法不成立。',
     '要素：探针 · 5′→3′ 外切酶活性', '5′→3′ 外切酶域（1–290）'),
    ('5', '⑤ 逆转录（一步法 RT-qPCR）',
     'MMLV 逆转录酶以 RNA 为模板合成 cDNA。RT 域的 fingers/palm/thumb 完成催化；这一步与 Taq 共用缓冲，Mg²⁺ 与 pH 要同时满足两种酶。',
     '要素：RNA 模板 · dNTP · Mg²⁺ · 双酶兼容缓冲', 'MMLV RT 域（741–1100）'),
    ('6', '⑥ RNase H 降解 RNA 链',
     'MMLV 的 RNase H 域会切掉 RNA–DNA 杂交链中的 RNA。切得太早/太多，长片段与低丰度模板就扩不出来——这是“一步法长片段掉”的结构域解释。',
     '要素：RNase H 活性 · cDNA 长度', 'RNase H 域（1174–1320，预测）'),
    ('7', '⑦ UNG 防污染',
     'UNG 切除 DNA 中的尿嘧啶（来自既往扩增产物），使污染模板无法再扩增；热敏 UNG 在升温后被灭活，不干扰本轮反应。',
     '要素：dUTP/UNG 体系 · 温度', 'UNG 催化域（1–229）· D64'),
]

ELEMENTS = [
    ('模板 DNA', '提供序列信息；双链在变性时解开', '—', '浓度过高会增加非特异；过低则 Ct 晚'),
    ('引物', '决定扩增起点与特异性；退火时结合', '手指域识别引物–模板', '二聚体会与引物竞争，导致某一重不出'),
    ('dNTP', '延伸的底物，提供能量与碱基', '手掌域催化掺入', '浓度失衡会降低保真度；dUTP 用于 UNG 体系'),
    ('Mg²⁺', '中和磷酸骨架、辅助磷酸转移', '手掌域活性中心', '过低酶不工作，过高降低特异性'),
    ('缓冲 pH', '维持酶构象与活性', '全酶构象', '一步法中要同时满足 RT 与 Taq'),
    ('探针', '结合靶标后被子解，产生荧光', '5′→3′ 外切酶域', '探针降解或二聚会导致假阴性'),
    ('聚合酶', '催化 dNTP 掺入、延伸新链', '手掌 / 拇指 / 手指', '耐热性、持续合成、保真度是三个不同维度'),
    ('UNG / dUTP', '切除尿嘧啶，消除既往产物污染', 'UNG 催化域 D64', '热敏 UNG 需在升温后失活，否则伤及本轮产物'),
]

steps_js = ',\n'.join(
    '{t:%s,cap:%s,el:%s,dom:%s}' % (
        __import__('json').dumps(s[1], ensure_ascii=False),
        __import__('json').dumps(s[2], ensure_ascii=False),
        __import__('json').dumps(s[3], ensure_ascii=False),
        __import__('json').dumps(s[4], ensure_ascii=False))
    for s in STEPS)

CSS = """
*{box-sizing:border-box}body{margin:0;background:#f4f6fa;font-family:-apple-system,"PingFang SC",sans-serif;color:#1b2430;font-size:13px;line-height:1.6}
.wrap{max-width:1180px;margin:0 auto;padding:0 18px 40px}
h1{font-size:21px;margin:14px 0 4px}.meta{color:#5f6b7a;font-size:12.2px;margin-bottom:12px}
.card{background:#fff;border:1px solid #e3e8f1;border-radius:10px;padding:14px 16px;margin-bottom:14px}
.card h2{font-size:15px;margin:0 0 10px;padding-left:9px;border-left:3px solid #1F4E79}
.card h2 span{font-size:10.8px;color:#8a94a3;font-weight:600;margin-left:6px}
.stage{position:relative;background:linear-gradient(180deg,#fbfdff,#f2f6fc);border:1px solid #e3e8f1;border-radius:10px;overflow:hidden}
.stage svg{width:100%;height:auto;display:block}
.ctrl{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px}
.ctrl button{padding:7px 14px;border:1px solid #cfd8e6;background:#fff;border-radius:7px;cursor:pointer;font-size:12.5px}
.ctrl button.primary{background:#1852B6;color:#fff;border-color:#1852B6}
.ctrl button:hover{border-color:#1852B6;color:#1852B6}
.ctrl button.primary:hover{color:#fff}
.dots{display:flex;gap:5px;margin-left:6px}
.dot{width:9px;height:9px;border-radius:50%;background:#d6deea;cursor:pointer}
.dot.on{background:#1852B6;transform:scale(1.25)}
.cap{margin-top:10px;background:#1F4E79;color:#fff;border-radius:8px;padding:11px 14px}
.cap h4{margin:0 0 4px;font-size:14px;color:#FFD479}
.cap p{margin:2px 0;font-size:12.8px;line-height:1.65}
.cap .tags{margin-top:6px;font-size:11.5px;color:#CFE0F5}
table{width:100%;border-collapse:collapse;font-size:11.9px}
th{background:#1F4E79;color:#fff;padding:7px;text-align:left}td{padding:6px 7px;border-bottom:1px solid #eef1f6;vertical-align:top}
.fn{color:#6b7684;font-size:11.3px;margin-top:7px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.box{background:#f7f9fd;border:1px solid #dfe6f2;border-radius:8px;padding:10px 12px}
@media(max-width:900px){.grid2{grid-template-columns:1fr}.bior-tagwrap{display:none!important}}
"""

SVG = """
<svg viewBox="0 30 1000 440" id="scene" data-step="0">
  <defs>
    <marker id="arw" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#43505f"/></marker>
    <filter id="glow"><feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
  </defs>

  <!-- 背景标注 -->
  <text x="20" y="46" font-size="12" fill="#8a94a3">反应体系（示意）</text>

  <!-- 模板双链 -->
  <g id="ds">
    <path id="ds-top" d="M120,250 C220,238 320,262 420,250 C520,238 620,262 720,250 C800,241 860,250 900,250" stroke="#43505f" stroke-width="5" fill="none" stroke-linecap="round"/>
    <path id="ds-bot" d="M120,272 C220,284 320,260 420,272 C520,284 620,260 720,272 C800,281 860,272 900,272" stroke="#8a94a3" stroke-width="5" fill="none" stroke-linecap="round"/>
    <text x="908" y="256" font-size="11" fill="#43505f">模板</text>
  </g>

  <!-- 引物 -->
  <g id="primer">
    <path d="M250,248 L340,248" stroke="#0F9D8F" stroke-width="6" stroke-linecap="round"/>
    <path d="M340,248 L330,243 L330,253 z" fill="#0F9D8F"/>
    <text x="250" y="236" font-size="11" fill="#0F6B62">引物</text>
  </g>

  <!-- dNTP -->
  <g id="dntp">
    <circle cx="700" cy="170" r="9" fill="#E08A1E"/><text x="714" y="174" font-size="10" fill="#8a5a12">dNTP</text>
    <circle cx="740" cy="196" r="9" fill="#E08A1E"/>
    <circle cx="670" cy="196" r="9" fill="#E08A1E"/>
  </g>

  <!-- Mg2+ -->
  <g id="mg">
    <circle cx="640" cy="300" r="5" fill="#7B61A8"/><text x="650" y="304" font-size="10" fill="#5b4a80">Mg²⁺</text>
    <circle cx="600" cy="316" r="5" fill="#7B61A8"/>
  </g>

  <!-- 聚合酶：四个域 -->
  <g id="pol">
    <circle id="exo" cx="415" cy="330" r="34" fill="#E08A1E" opacity="0.92"/>
    <circle id="fingers" cx="490" cy="230" r="46" fill="#1852B6" opacity="0.92"/>
    <circle id="palm" cx="560" cy="286" r="50" fill="#2E7DD1" opacity="0.92"/>
    <circle id="thumb" cx="628" cy="236" r="40" fill="#4FA3E3" opacity="0.92"/>
    <text x="415" y="386" font-size="10.5" text-anchor="middle" fill="#8a5a12">外切域</text>
    <text x="490" y="180" font-size="10.5" text-anchor="middle" fill="#1852B6">手指</text>
    <text x="560" y="352" font-size="10.5" text-anchor="middle" fill="#1F4E79">手掌（催化）</text>
    <text x="628" y="188" font-size="10.5" text-anchor="middle" fill="#2E7DD1">拇指</text>
  </g>

  <!-- 探针 -->
  <g id="probe">
    <path d="M700,222 L800,222" stroke="#C0392B" stroke-width="4" stroke-linecap="round"/>
    <circle id="fluor" cx="700" cy="222" r="7" fill="#0F9D8F"/>
    <circle id="quench" cx="800" cy="222" r="7" fill="#5F6B7A"/>
    <text x="690" y="208" font-size="10" fill="#0F6B62">F</text>
    <text x="806" y="208" font-size="10" fill="#5F6B7A">Q</text>
  </g>

  <!-- RNA 链（MMLV） -->
  <g id="rna">
    <path d="M300,340 C400,326 500,352 600,338 C680,328 740,338 780,338" stroke="#7B61A8" stroke-width="4" stroke-dasharray="9 6" fill="none"/>
    <text x="300" y="364" font-size="10.5" fill="#5B4A80">RNA 模板（cDNA 合成 / RNase H）</text>
  </g>

  <!-- 尿嘧啶 -->
  <g id="ura">
    <rect x="520" y="196" width="12" height="12" rx="2" fill="#C0392B"/>
    <text x="538" y="206" font-size="10" fill="#B3261E">U</text>
  </g>

  <!-- 温度/状态标签 -->
  <text id="temp" x="940" y="40" font-size="13" text-anchor="end" font-weight="700" fill="#1852B6">待机</text>
</svg>
"""

stage_css = """
/* 步骤状态：用 data-step 驱动，CSS 过渡实现动画 */
#scene [id]{transition: transform .9s ease, opacity .6s ease}
#scene[data-step="1"] #ds-top{transform:translateY(-26px)}
#scene[data-step="1"] #ds-bot{transform:translateY(26px)}
#scene[data-step="2"] #primer{transform:translateX(70px)}
#scene[data-step="2"] #fingers{transform:scale(1.08)}
#scene[data-step="3"] #dntp{transform:translate(-90px,60px)}
#scene[data-step="3"] #palm{transform:scale(1.12)}
#scene[data-step="3"] #thumb{transform:scale(1.08)}
#scene[data-step="4"] #exo{transform:translate(180px,-40px)}
#scene[data-step="4"] #probe{opacity:0.35}
#scene[data-step="4"] #fluor{transform:translate(-40px,-20px);filter:url(#glow)}
#scene[data-step="4"] #quench{transform:translate(40px,20px)}
#scene[data-step="5"] #rna{opacity:1}
#scene[data-step="5"] #pol{opacity:0.35}
#scene[data-step="6"] #rna{opacity:0.35}
#scene[data-step="6"] #rna path{stroke-dasharray:3 12}
#scene[data-step="7"] #ura{transform:translate(0,-26px);opacity:0.25}
"""

page = """<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PCR 机理分割动画：要素 × 结构域</title><style>__CSS____STAGECSS__</style></head><body><div class="wrap">
<h1>PCR 机理分割动画：要素如何作用于结构域</h1>
<div class="meta">按“分割演示”组织：每一步只讲一个动作，画面同步高亮对应结构域。适配 Taq / Tth / MMLV / UNG 四种酶。点击播放可自动演示，也可单步查看。截至 2026-10-17。</div>

<div class="card">
<div class="stage">__SVG__</div>
<div class="ctrl">
  <button class="primary" id="play">▶ 播放</button>
  <button id="prev">← 上一步</button>
  <button id="next">下一步 →</button>
  <div class="dots" id="dots"></div>
</div>
<div class="cap" id="cap"><h4></h4><p></p><p class="tags"></p></div>
</div>

<div class="card"><h2>反应要素：各自起什么作用、作用在哪个域 <span>销售可照着讲</span></h2>
<table><thead><tr><th style="width:14%">要素</th><th style="width:26%">作用</th><th style="width:22%">作用于哪个结构域</th><th>常见异常</th></tr></thead><tbody>
__ELEMENTS__
</tbody></table>
<div class="fn">结构域编号见蛋白工作台；MMLV 的 RNase H 与 Tth 外切域为预测结构，讲的时候要说明。</div></div>

<div class="card"><h2>怎么用这套动画讲给客户 <span>三段式</span></h2>
<div class="grid2">
<div class="box"><b>第一步：先定位在哪一段</b><p style="margin:3px 0">让客户说现象（Ct 晚 / 不出峰 / 长片段掉 / 污染），对应到变性、退火、延伸、探针水解、逆转录、RNase H、UNG 七个环节之一。</p></div>
<div class="box"><b>第二步：指结构域</b><p style="margin:3px 0">“您这个问题落在手掌域（催化）还是拇指域（持续合成）”——把酶的黑箱拆成可讨论的部件。</p></div>
<div class="box"><b>第三步：给品类与边界</b><p style="margin:3px 0">按环节给品类方案，并说明不能承诺的边界（>4 kb 一步法、任意重数多重、常温 12 个月）。</p></div>
<div class="box"><b>不要做的事</b><p style="margin:3px 0">不把“Ct 晚”直接归因于酶活；不在没有阳性对照与复孔数据时下结论；不用预测结构讲精确构象。</p></div>
</div></div>
__FOOTER__
</div>
<script>
var STEPS=[__STEPS__];
var i=0, timer=null;
var scene=document.getElementById('scene'), cap=document.getElementById('cap'), dots=document.getElementById('dots');
STEPS.forEach(function(s,k){
  var d=document.createElement('span'); d.className='dot'+(k===0?' on':'');
  d.onclick=function(){ stop(); show(k); };
  dots.appendChild(d);
});
function show(k){
  i=(k+STEPS.length)%STEPS.length;
  var s=STEPS[i];
  scene.setAttribute('data-step', i);
  document.getElementById('temp').textContent = s.t.replace(/^[①②③④⑤⑥⑦]\s*/, '').slice(0,12);
  cap.querySelector('h4').textContent=s.t;
  cap.querySelectorAll('p')[0].textContent=s.cap;
  cap.querySelectorAll('p')[1].textContent='要素：'+s.el+' ｜ 结构域：'+s.dom;
  document.querySelectorAll('.dot').forEach(function(d,j){ d.classList.toggle('on', j===i); });
}
function play(){
  if(timer){ stop(); return; }
  document.getElementById('play').textContent='⏸ 暂停';
  timer=setInterval(function(){ show(i+1); }, 3200);
}
function stop(){
  if(timer){ clearInterval(timer); timer=null; }
  document.getElementById('play').textContent='▶ 播放';
}
document.getElementById('play').onclick=play;
document.getElementById('prev').onclick=function(){ stop(); show(i-1); };
document.getElementById('next').onclick=function(){ stop(); show(i+1); };
show(0);
</script></body></html>"""

page = (page.replace('__CSS__', CSS).replace('__STAGECSS__', stage_css).replace('__SVG__', SVG)
        .replace('__ELEMENTS__', ''.join('<tr><td><b>%s</b></td><td>%s</td><td>%s</td><td>%s</td></tr>' % e for e in ELEMENTS))
        .replace('__STEPS__', steps_js))
page = BK.inject(page, theme='dark', tag='PCR 机理动画 · 要素与结构域',
                 footer_extra='内部资料 · 请勿外发 ｜ 结构域依据 UniProt / PDB；机理依据 PubMed')
page = page.replace('__FOOTER__', '')
io.open(OUT, 'w', encoding='utf-8').write(page)
print('✅ %.0f KB' % (len(page.encode()) / 1024))
