import re
s = open('v17_src.html').read()
def R(a, b, cnt=1):
    global s
    n = s.count(a)
    assert n == cnt, (n, a[:90])
    s = s.replace(a, b)
def RS(start, end, new):
    global s
    i = s.index(start); j = s.index(end, i)
    s = s[:i] + new + s[j:]
def RL(start, new):
    global s
    i = s.index(start); j = s.index('\n', i)
    s = s[:i] + new + s[j:]

# ---------- 头部 ----------
R('<title>NobadyBox v17 前口拆解图</title>', '<title>NobadyBox v18 内插拆解图</title>')
R('NOBADY BOX · 硬件方案 v17 · 前口直插版 · 拆解效果图', 'NOBADY BOX · 硬件方案 v18 · 前口内插版 · 拆解效果图')
R('<h1>AI 手机仓 v17 前口直插版逐级拆解</h1>', '<h1>AI 手机仓 v18 前口内插版逐级拆解</h1>')
R('、侧面 USB 开口都按你给的参数表画。壁厚、合页、风道、前口尺寸和 TPU 垫是我按这组参数定的估算值。',
  '都按你给的参数表画。电源口、分线板、插头座、前口尺寸、壁厚、合页、风道和 TPU 垫是我按这组参数定的估算值。')

RS('<section>\n  <h3>v17 改了什么', '<section>\n  <h3>4 个 3D 打印零件</h3>', '''<section>
  <h3>v18 改了什么：前口开大，充电线固定在里面，只有一个电源口</h3>
  <p class="lead">前口从 88 × 22 开到 90 × 45 mm，手指能伸进去拿手机。手机充电插头固定在仓底后端，手机充电口朝里推进去就插上。整机只有底仓后面底部一个 USB-C 电源口，进来以后在分线板上分两路，一路上去给主板，一路直接给手机。</p>
  <div class="tbl"><table class="cmp">
    <thead><tr><th></th><th>v17 前口直插版</th><th>v18 前口内插版</th></tr></thead>
    <tbody>
      <tr><td>前口</td><td class="num">88 × 22 mm</td><td class="num">90 × 45 mm</td></tr>
      <tr><td>放手机</td><td>充电口朝外推进去，再从外面插线</td><td>充电口朝里推进去，推到底就插上了</td></tr>
      <tr><td>拿手机</td><td>拔线带出来</td><td>手指伸进前口捏住手机往外拉，插头自己脱开</td></tr>
      <tr><td>外面的线</td><td>2 根</td><td>1 根，插在后面底部</td></tr>
      <tr><td>电源口</td><td>顶盖侧面（只给主板）</td><td>底仓后面底部，分线板分给主板和手机</td></tr>
      <tr><td>主板 USB 差 19 mm 的问题</td><td>还在</td><td>没有了：外面不用主板的 USB 口</td></tr>
      <tr><td>打印件</td><td>3 + 1</td><td>3 + 1</td></tr>
    </tbody>
  </table></div>
</section>

''')

RS('  <p class="lead">盒子和手机各用一根线。', '    <div class="req">\n      <h4>合页、磁铁和校准', '''  <p class="lead">整机外面只有一根线。点 3D 视图右上角的「走线」可以看到每根线的走向。</p>
  <div class="reqs">
    <div class="req">
      <h4>电源口放哪里<small>底仓后面底部</small></h4>
      <ul>
        <li>USB-C 电源口开在底仓后壁底部偏右，离桌面约 20 mm。线从后面走，前面和顶上都干净，开盖也不会扯到线。</li>
        <li>口子后面贴一块小分线板（约 28 × 22 mm），上面是 USB-C 母座、保险丝和一个电子开关。充电器要支持 5 V 3 A。</li>
        <li>第一路给手机：分线板旁边就是手机插头座，线只有约 5 cm，大电流不用绕远路，也不过合页。</li>
        <li>第二路给主板：一根 3 芯细线（5 V、地、充电开关信号）沿后壁线槽上去，贴着右侧合页转轴进顶盖，插主板上的 XH 3P。主板用开关信号控制手机充电：电量 40–80% 之间开关充电，超过 40 °C 先停。</li>
      </ul>
    </div>
    <div class="req">
      <h4>手机怎么放、怎么充电<small>推到底就插上</small></h4>
      <ul>
        <li>前口 90 × 45 mm，在前壁最底部。手机屏幕朝上、充电口朝里，平着推进去。</li>
        <li>仓底后端有一个插头座，C 公头口朝前固定在里面，高度和手机充电口对齐。手机推到底就插上，开始充电。</li>
        <li>插头座是浮动的，左右上下能让约 1 mm，两侧 TPU 条把手机夹在中间，推进去自然对准。</li>
        <li>拿手机：手指伸进前口，捏住手机前端往外拉，插头自己脱开。</li>
      </ul>
    </div>
''')
R('平时放手机走前口，顶盖只在维修时打开。</li>', '平时放手机走前口，顶盖只在维修时打开。开盖前先拔主板上的 XH 3P。</li>')
R('<li>底仓两侧下部 4 扇进风百叶，前口也能进风。', '<li>底仓两侧下部 4 扇进风百叶，加上前口，进风很充足。')
R('<li>2 颗 M3×30 穿合页把顶盖装到底仓上，压 4 颗磁铁，放进 TPU 垫，做一次标定。</li>',
  '<li>分线板 2 颗 M2 装到后壁，插头卡进插头座，3 芯线压进后壁线槽；2 颗 M3×30 穿合页装顶盖，插上 XH 3P，压 4 颗磁铁，放 TPU 条，做一次标定。</li>')
R('<li>烫 8 颗 M2 热熔螺母。</li>', '<li>烫 10 颗 M2 热熔螺母（多 2 颗装分线板）。</li>')
RS('    <div><b>USB 口够不到侧壁</b>', '    <div><b>风扇正下方的元件高度</b>', '')
RS('    <div><b>前口会进一点光</b>', '  </div>\n</section>', '''    <div><b>前口更大，进光更多</b>前口开到 45 mm 高以后，平着进来的光会多一些。第一版先不挡，实测如果影响摄像头识别屏幕，再加一片软帘或者把前口高度收到 35 mm。</div>
    <div><b>手机要能对准插头</b>插头座按手机充电口在端面正中、手机厚 7–9 mm 设计。带厚壳的手机插不到底，需要摘壳；不同厚度用 TPU 垫片调高度。</div>
''')
R('<h3>需要你确认的 3 件事</h3>', '<h3>需要你确认的事</h3>')

# ---------- 尺寸 / INFO ----------
R("const SLOT_W = 44, SLOT_Y = 25, RIB = 7;   // 前口半宽 / 前口顶 / 支撑筋顶", "const SLOT_W = 45, SLOT_Y = 48, RIB = 7;   // 前口半宽 / 前口顶 / 支撑筋顶")
R("role:'放手机的仓。前面开 88 × 22 mm 前口，带支撑筋、手机框侧壁、进风百叶、合页耳、磁铁座和定位标记。'}",
  "role:'放手机的仓。前面开 90 × 45 mm 前口，后面底部开 USB-C 电源口，带插头座、支撑筋、手机框侧壁、后壁线槽、进风百叶、合页耳、磁铁座和定位标记。'}")
R("support:'不需要（百叶 45°，合页耳和磁铁座下 45° 斜撑，前口顶边桥接 88 mm）'", "support:'不需要（百叶 45°，合页耳和磁铁座下 45° 斜撑，前口顶边桥接 90 mm）'")
R("support:'不需要（USB 开口顶部桥接 21 mm，合页孔泪滴形）'", "support:'不需要（合页孔泪滴形）'")
R("role:'顶盖的外壳。顶上嵌风扇和格栅，+X 侧开 USB-C 口，后边是合页，前面 2 颗磁铁。'}", "role:'顶盖的外壳。顶上嵌风扇和格栅，后边是合页和过线缺口，前面 2 颗磁铁。外面不再开口。'}")
R("USB-C 在 +X 端。'}", "电从后面来的 3 芯线插 XH 3P；PCB 上的 USB-C 只在开盖刷固件时用。'}")
R("wall:'侧条 4.5 mm，后垫按手机长度 0–13 mm，高 6 mm'", "wall:'侧条 4.5 mm，高 6 mm；另配 0.5 / 1 mm 垫片调插头高度'")
R("role:'两侧把手机卡在中间对准镜头；后垫让手机推进去时停在对的位置。'}", "role:'两侧把手机卡在中间，屏幕对准镜头，充电口对准后端插头。'}")
RL("  cable:{name:'手机充电线（外接）'", "  cable:{name:'电源分线板 + 充电插头', mat:'USB-C 母座 + 保险丝 + 电子开关，约 28 × 22 mm；C 公头（内置 56 kΩ）；3 芯线约 400 mm 到 XH 3P', role:'整机唯一电源口。一路约 5 cm 直接给手机插头，一路沿后壁线槽、贴合页转轴上去给主板。'},")
R("role:'屏幕朝上，充电口朝外，从前口推进去。屏幕离盒底 16 mm。'}", "role:'屏幕朝上，充电口朝里，从前口推到底就插上。屏幕离盒底 16 mm。'}")
R("ins:{name:'热熔螺母 M2 × 8', mat:'黄铜', role:'安装板 4 颗（PCB）、顶盖外壳 4 颗（安装板）。'}", "ins:{name:'热熔螺母 M2 × 10', mat:'黄铜', role:'安装板 4 颗（PCB）、顶盖外壳 4 颗（安装板）、后壁 2 颗（分线板）。'}")

RS("const STEPS = [", "const NSTEP", """const STEPS = [
  {p:[], t:'放好手机的样子', d:'100 × 190 × 138.2 mm。前面底部是 90 × 45 mm 的前口，后面底部是唯一的电源口。往右拖滑块，每一级拆下一个零件，当前这一级会标成橙色。'},
  {p:['phone'], t:'抽出手机', d:'手指伸进前口捏住手机往外拉，插头自己脱开。放手机反过来：充电口朝里推到底就插上了。'},
  {p:['tpu'], t:'取出 TPU 条', d:'两侧条把手机卡在中间，屏幕对准镜头，充电口对准插头。按手机大小选 1 套。'},
  {p:['shell'], t:'掀开顶盖（维修时）', d:'平时不用开。维修时顶盖绕后边的合页往后翻，约 100° 停住自己立着。前面 2 对磁铁平时把它吸住。'},
  {p:['hinge'], t:'拔出合页销', d:'2 颗 M3×30 螺丝当合页销，拧下防松螺母就能抽出来。拆之前先拔掉主板上的 XH 3P。'},
  {p:['shell','plate'], t:'取下整个顶盖模块', d:'主板、摄像头、风扇都在顶盖里，整块拿下来。维修或者换代时直接换整个顶盖。'},
  {p:['shell'], t:'拆开顶盖外壳', d:'拧下 4 颗 M2 螺丝，顶盖外壳和安装板分开。外壳顶上嵌着风扇。'},
  {p:['fan'], t:'拆风扇', d:'3007 风扇用 2 颗自攻螺丝装在外壳顶板下面，正对镜头中心，离 PCB 顶面约 2.5 mm，往上排风。'},
  {p:['pcb'], t:'拆 PCB 和摄像头', d:'摄像头焊在 PCB 中心，4 颗 M2 螺丝装在安装板的凸台上。镜头从安装板中间的 Ø15 孔穿下去，只露出镜头。'},
  {p:['cable'], t:'取出分线板和充电线', d:'分线板 2 颗 M2 装在后壁电源口后面。一路约 5 cm 到插头座里的 C 公头；一路 3 芯线沿后壁线槽上去，贴合页转轴到主板 XH 3P。'},
  {p:['mag'], t:'取出磁铁', d:'4 颗 Ø6 × 3 mm 钕磁铁压入，不用胶。压之前对一下极性，保证上下相吸。'},
  {p:['ins'], t:'热熔螺母', d:'10 颗 M2：安装板 4 颗（PCB），顶盖外壳 4 颗（安装板），后壁 2 颗（分线板）。烙铁约 220 °C 烫进去。'},
  {p:['body'], t:'只剩底仓', d:'一体打印的底仓。虚线标注是它身上做出来的功能：前口、电源口、插头座、支撑筋、后壁线槽、进风百叶、合页耳、磁铁座和定位标记。'}
];
""")

# ---------- 底仓 ----------
R("g.add(boxR(-XI,XI,0,SPLIT,-L/2,-ZI,m));   // 后壁",
  "g.add(boxR(-XI,15.5,0,SPLIT,-L/2,-ZI,m), boxR(24.5,39,0,SPLIT,-L/2,-ZI,m), boxR(15.5,24.5,0,17.75,-L/2,-ZI,m), boxR(15.5,24.5,22.25,SPLIT,-L/2,-ZI,m), boxR(45,XI,0,SPLIT,-L/2,-ZI,m), boxR(39,45,0,114,-L/2,-ZI,m));   // 后壁：底部 USB-C 电源口，顶上 6 mm 过线缺口\n"
  "  g.add(boxR(37.2,38.4,34,SPLIT-3,-ZI,-89.6,m));                               // 后壁线槽\n"
  "  for(const y of [50,80,105]) g.add(boxR(38.4,40.6,y,y+3,-90.6,-89.6,m));       // 线槽卡扣\n"
  "  g.add(boxR(-9,-5.7,7,16.4,-89.5,-79.5,m), boxR(5.7,9,7,16.4,-89.5,-79.5,m), boxR(-9,9,14.9,16.4,-89.5,-80,m), boxR(-5.7,5.7,7,8.2,-89.5,-79.5,m));   // 插头座")
R("// 前壁，底部开 88 × 22 前口", "// 前壁，底部开 90 × 45 前口")
R("g.add(boxR(-XI,XI,SPLIT,H-WT,-L/2,-ZI,m),", "g.add(boxR(-XI,39,SPLIT,H-WT,-L/2,-ZI,m), boxR(45,XI,SPLIT,H-WT,-L/2,-ZI,m), boxR(39,45,126,H-WT,-L/2,-ZI,m),")
R(" sR.holes.push(rectH(-10.5,127.75,21,4.5));", "")

RS("function buildTPU(m){", "/* ---------- 组装体", """function buildTPU(m){
  const g = new T.Group();
  g.add(boxR(-FX,-38,7,13,-77.5,84.5,m), boxR(38,FX,7,13,-77.5,84.5,m));
  return g;
}
""")
R("  hPCB.add(boxR(17,22,1.6,5.6,6,11,mW));", "  hPCB.add(boxR(-28,-21,1.6,7.6,-12,-4,mW));            // XH 3P 电源入\n  hPCB.add(boxR(17,22,1.6,5.6,6,11,mW));")
R("const hPhone = holder(asm,0,0,PZ); hPhone.rotation.y = Math.PI;", "const hPhone = holder(asm,0,0,PZ);")

# ---------- 分线板、插头和线 ----------
RS("// 外接充电线（手机原装线，从前口插进来）", "// 热熔螺母", """// 电源分线板 + 手机充电插头 + 两路线
const mCab = M(0x15181c,{roughness:.7}), mPlug = M(0x2b2f35), mBrd = M(0x1f5f4a,{roughness:.5});
const hCable = holder(asm,0,0,0);
hCable.add(boxR(-5.5,5.5,8.3,14.7,-86,-79.5,mPlug), boxR(-4.15,4.15,10.3,12.7,-79.5,-74,mSteel));          // C 公头，口朝前
hCable.add(boxR(11,39,8,30,-92.4,-90.8,mBrd), boxR(15.5,24.5,17.75,22.25,-94.2,-92.4,mSteel));           // 分线板 + USB-C 母座
hCable.add(boxR(28,33,12,15,-90.8,-89.8,mPlug), boxR(29,36,20,24,-90.8,-89.6,mPlug), boxR(31,37,26,29.5,-90.8,-88.5,M(0xf1f1ee)));   // 保险丝、开关、XH
hCable.add(new T.Mesh(new T.TubeGeometry(new T.CatmullRomCurve3([[0,11.5,-86],[0,11.5,-88.5],[4,10.5,-90.2],[10,12,-90.3],[14,13.5,-90.5]].map(a=>new T.Vector3(...a))),40,1.6,8,false),mCab));   // 第一路：给手机
reg('cable',hCable,[mCab,mPlug,mBrd]);
let cableMesh=null;
const CAB_FIXED = [[34,29.5,-89],[38,31,-90.5],[42,36,-91.2],[42,60,-91.2],[42,110,-91.2],[42,117,-93.5],[42,PIV.y-1,PIV.z]].map(a=>new T.Vector3(...a));
const CAB_LID = [[42,123.5,-93],[42,124,-88],[38,124.5,-60],[-20,124.5,-40]].map(a=>new T.Vector3(...a));   // 安装板上方（lidG 坐标）
const CAB_PCB = [[-24.5,4,-22],[-24.5,4.6,-12]].map(a=>new T.Vector3(...a));                      // XH 3P（PCB 坐标）
// 外接电源线（唯一一根）
const mExtP=M(0x8a929c,{roughness:.7});
asm.add(boxR(14,26,14.5,25.5,-114,-95.2,mExtP), new T.Mesh(new T.TubeGeometry(new T.CatmullRomCurve3([[20,20,-112],[20,20,-122],[20,8,-140],[20,1.8,-160],[45,1.8,-215],[90,1.8,-250]].map(a=>new T.Vector3(...a))),80,2,10,false),mExtP));
""")
R("wires.add(boxR(50,70,127.8,132.2,-5.5,5.5,mExt), tube([[70,130,0],[90,130,0],[120,100,0],[140,20,0]],2.2,mExt));\n", "")
R("wires.add(tube([[28.5,129.2,0],[38,129.5,0],[47.6,130,0]],1.6,mWarn));\n", "")
R("// 走线视图：外部电源线、风扇线、USB 待确认段", "// 走线视图：风扇线")

# ---------- 每一级怎么动 ----------
RS("mv(hCable,1,0,0,70);", "/* ---------- 打印摆放", """for(const x of [14,36]) ins(hBody,x,19,-91.4,[0,0,25]);
mv(hPhone,1,0,0,200);
mv(hTPU,2,0,110,0);
hHinge.forEach((h,i)=> mv(h,4,(i?1:-1)*45,0,0));
mv(lidPivot,5,0,190,40);
mv(hShell,6,0,85,0);
mv(hFan,7,0,45,0);
mv(hPCB,8,0,22,0);
mv(hCable,9,85,0,0);
hMag.forEach(o=> mv(o.h,10,0,o.up?18:22,0));
hIns.forEach(k=> mv(k,11,...k.userData.dir));

""")
R("PART.ins.mats.push(mBrass);", "PART.ins.mats.push(mBrass);")
R("const tp=addEdges(buildTPU(mTPU)); tp.position.set(300,-7,0);", "const tp=addEdges(buildTPU(mTPU)); tp.position.set(300,-7,-3.5);")
R("text:'<em>P4</em>TPU 垫 · 平放'", "text:'<em>P4</em>TPU 条 · 平放'")

# ---------- 标注 ----------
R("PID.tpu=lab(hTPU,[-FX,13,-40],'<em>P4</em>TPU 垫','printed tpu',()=>true);", "PID.tpu=lab(hTPU,[-FX,13,-40],'<em>P4</em>TPU 条','printed tpu',()=>true);")
R("PID.phone=lab(hPhone,[38,16,-40],'手机（示意）','',()=>true);", "PID.phone=lab(hPhone,[38,16,40],'手机（示意）','',()=>true);")
R("'合页销 M3×30','',after(5)", "'合页销 M3×30','',after(4)")
R("'3007 风扇','',after(8)", "'3007 风扇','',after(7)")
R("'PCB + 摄像头','',after(9)", "'PCB + 摄像头','',after(8)")
R("PID.cable=lab(hCable,[6,14.8,PZ+95],'手机充电线（外接）','',()=>true);", "PID.cable=lab(hCable,[39,30,-90.8],'电源分线板 + 充电插头','',after(9));")
R("lab(lidG,[70,132,0],'盒子供电 USB-C 5 V 1 A','wire',WL);", "lab(asm,[20,25.5,-114],'唯一电源口 USB-C 5 V 3 A','wire',WL);\nlab(hCable,[11,8,-90.8],'分线板：一路给手机，一路给主板','wire',WL);\nlab(hCable,[5.5,14.7,-83],'插头固定在插头座里，手机推到底就插上','wire',WL);\nlab(hBody,[42,70,-91.5],'3 芯线：后壁线槽 + 3 个卡扣','wire',WL);\nlab(asm,[42,PIV.y,PIV.z-2],'贴合页转轴穿过，只扭不折','wire',WL);\nlab(lidG,[-24.5,132,-12],'XH 3P → 主板','wire',WL);")
RL("lab(lidG,[38,130,0],'PCB 到侧壁差约 19 mm", "")
RL("lab(hCable,[0,11.5,PZ+110],'手机原装线从前口插进来'", "")
R("[[0,SLOT_Y,L/2],'前口 88 × 22'],", "[[0,SLOT_Y,L/2],'前口 90 × 45'],[[20,22.25,-L/2],'电源口'],[[0,16.4,-84],'插头座'],[[42,100,-91],'后壁线槽'],")
R("if(k===0) h+='<p class=\"note\">打印件 4 个：P1 底仓、P2 顶盖外壳、P3 摄像头安装板、P4 TPU 垫。", "if(k===0) h+='<p class=\"note\">打印件 4 个：P1 底仓、P2 顶盖外壳、P3 摄像头安装板、P4 TPU 条。")

# ---------- 姿态 ----------
RS("  lidPivot.rotation.x = -Math.PI*100/180 * tOf(4,v) * (1-tOf(6,v));", "const accentColor", """  lidPivot.rotation.x = -Math.PI*100/180 * tOf(3,v) * (1-tOf(5,v));
  asm.updateMatrixWorld(true);
  const tc=tOf(9,v), off=new T.Vector3(85*tc,0,0);
  const pts=CAB_FIXED.map(p=>p.clone().add(off));
  CAB_LID.forEach(p=>{ const now=lidG.localToWorld(p.clone()); const fin=p.clone().add(off); fin.y=Math.min(fin.y,SPLIT-2); pts.push(now.lerp(fin,tc)); });
  CAB_PCB.forEach(p=>{ const now=hPCB.localToWorld(p.clone()); const fin=p.clone().add(new T.Vector3(0,126,0)).add(off); fin.y=SPLIT-2; pts.push(now.lerp(fin,tc)); });
  if(cableMesh){ asm.remove(cableMesh); cableMesh.geometry.dispose(); }
  cableMesh=new T.Mesh(new T.TubeGeometry(new T.CatmullRomCurve3(pts,false,'centripetal'),300,1.3,8,false),mCab); asm.add(cableMesh);
}
""")
R("const s0=c(x-1), s=c(x-3), s2=c((x-5)/3); return {ty:69+45*s+65*s2, tz:60*s0*(1-s),", "const s0=c(x), s=c(x-2), s2=c((x-4)/3); return {ty:69+45*s+65*s2, tz:60*s0*(1-s),")
R("camera.position.set(400,220,420)", "camera.position.set(380,240,-400)")

# ---------- 零件卡与清单 ----------
R("body:['合页耳下面有 45° 斜撑，受力不会把耳朵掰断','前口上面还有 95 mm 高的前壁，和两侧壁连成一圈，开口不会变形','4 条 4 mm 支撑筋托住手机，手机推进推出顺滑，背面也通风']",
  "body:['合页耳下面有 45° 斜撑，受力不会把耳朵掰断','前口上面还有 72 mm 高的前壁，和两侧壁连成一圈，开口不会变形','插头座和底板一体打印，插拔上千次不松','4 条 4 mm 支撑筋托住手机，推进推出顺滑，背面也通风']")
R("tpu:['软料吸震，不刮手机','后垫上有 3 道软筋，手机推到底有一点缓冲','坏了单独重打，0.4 h','按手机分档，屏幕中心对准镜头']",
  "tpu:['软料吸震，不刮手机','把手机夹在正中，充电口对准插头','坏了单独重打，0.4 h','按手机宽度分档']")
RS("const BOM=[", "document.getElementById('bom')", """const BOM=[
  ['P1 底仓','打印','1','PETG HF 黑','12'],['P2 顶盖外壳','打印','1','PETG HF 黑','6'],['P3 摄像头安装板','打印','1','PETG HF 黑','5'],['P4 TPU 条','打印','按手机选 1 套','TPU 95A','2'],
  ['PCB + 摄像头','你的设计','1','57 × 28 × 6.5 mm，摄像头 Ø13.8 × 15 mm；电源从 XH 3P 进','8'],['3007 风扇','采购','1','30 × 30 × 7 mm，5 V 滚珠','7'],
  ['电源分线板','新做','1','约 28 × 22 mm：USB-C 母座、保险丝、电子开关、XH 3P','9'],['手机充电插头','采购','1','C 公头带短线，内置 56 kΩ','9'],['3 芯线','采购','1','约 400 mm，两端 XH 3P','9'],
  ['合页销','采购','2','M3×30 + 防松螺母','4'],['钕磁铁','采购','4','Ø6 × 3 mm','10'],
  ['热熔螺母','采购','10','M2','11'],['其他螺丝','采购','12','M2×6 ×10（PCB、安装板、分线板）、M2 自攻 ×2（风扇）（图中省略）','—'],
  ['PD 充电器 + 1 m C-C 线','随机附','1','必须支持 5 V 3 A','—']
];
""")
open('v18_src.html', 'w').write(s)
print('ok')
