# -*- coding: utf-8 -*- """票02 装配:12 联系人新端点收编进主 demo index.html。全部锚点断言唯一;CRLF 保持;幂等防护。""" import io, sys, os sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") BASE = r"e:\code\crm-backend-matt" DEMO = os.path.join(BASE, ".scratch", "customer-e2e", "demo", "index.html") PD = os.path.join(BASE, ".scratch", "customer-frontend-handover", "patch-t02") def rd(name): return open(os.path.join(PD, name), encoding="utf-8").read().rstrip("\n") def rep(txt, old, new, tag): n = txt.count(old) assert n == 1, "[%s] anchor count=%d (expect 1)" % (tag, n) return txt.replace(old, new, 1) raw = open(DEMO, "rb").read().decode("utf-8") crlf = "\r\n" in raw print("crlf =", crlf, "base lines =", len(raw.splitlines())) txt = raw.replace("\r\n", "\n") assert "contactsShell" not in txt, "already patched" p0 = rd("p0-apidownload.txt") p1 = rd("p1-css.txt") p2 = rd("p2-shell.txt") p3 = rd("p3-modals.txt") p4 = "\n".join(rd("p4%s-jscore.txt" % x) for x in ["a", "b", "c", "d", "e"]) p5 = "\n".join(rd("p5%s-jsedit.txt" % x) for x in ["a", "b", "c", "d"]) p6 = "\n".join(rd("p6%s-jslist.txt" % x) for x in ["a", "b", "c", "d", "e"]) print("patch sizes:", len(p0), len(p1), len(p2), len(p3), len(p4), len(p5), len(p6)) # 1. CSS( 前) txt = rep(txt, ".logday { font-weight: 600; color: #909399; margin: 10px 0 4px; font-size: 13px; }\n", ".logday { font-weight: 600; color: #909399; margin: 10px 0 4px; font-size: 13px; }\n" + p1 + "\n", "css") # 2. contactsShell(subBox 之后、detailPanel 收口前) txt = rep(txt, '
\n ', '
\n' + p2 + "\n ", "shell") # 3. qa/cimp 弹窗(toast 前) txt = rep(txt, '
', p3 + "\n
", "modals") # 4. apiDownload(登录节前) txt = rep(txt, "/* ================= 登录 ================= */", p0 + "/* ================= 登录 ================= */", "apidl") # 5. thead 加「关注」列 txt = rep(txt, "客户名称编号", "关注客户名称编号", "thead") # 6. rowHtml 星标 td old_row = r''' '' + esc(r.customerName) + '' +''' new_row = r''' '' + (Number(r.focusFlag) ? '★' : '☆') + '' + '' + esc(r.customerName) + '' +''' txt = rep(txt, old_row, new_row, "rowstar") # 7. colspan 11 → 12 txt = rep(txt, '无数据', '无数据', "colspan") # 8. toggleFocus(batchFocus 后) txt = rep(txt, "const batchFocus = () => batchPost('focus-batch');", """const batchFocus = () => batchPost('focus-batch'); async function toggleFocus(id, target) { try { await api('/api/customer/' + (target ? 'focus' : 'unfocus') + '?id=' + id, { method: 'POST' }); toast(target ? '已关注' : '已取关', 'ok'); loadList(); loadSummary(); } catch (e) { toast('失败: ' + e.message + codeSuffix(e), 'err'); } }""", "togglefocus") print("part1 replacements done (1-8)") # ============ part2 ============ # 9. loadHead:记录客户名(图谱 custName 用)+ +联系人 按钮换 快速添加 txt = rep(txt, " const d = await api('/api/customer/detail-head?id=' + D.id);\n $('detailPanel').style.display = '';", " const d = await api('/api/customer/detail-head?id=' + D.id);\n D.cname = d.customerName;\n $('detailPanel').style.display = '';", "cname") txt = rep(txt, "'", "'", "qabtn") # 10. switchSub:contacts 改走 contactsShell 常驻面板(其余页签显隐 subBox) old_sw = " const box = $('subBox');\n if (!D.id) { box.innerHTML = '
请先打开一个客户
'; return; }" new_sw = (" const box = $('subBox');\n" " const shell = $('contactsShell');\n" " if (s === 'contacts') {\n" " box.style.display = 'none'; shell.style.display = '';\n" " openContactsTab(); return;\n" " }\n" " box.style.display = ''; shell.style.display = 'none';\n" " if (!D.id) { box.innerHTML = '
请先打开一个客户
'; return; }") txt = rep(txt, old_sw, new_sw, "swhead") old_br = (" } else if (s === 'contacts') {\n" " const d = await api('/api/customer/contacts?customerId=' + D.id);\n" " const rows = Array.isArray(d) ? d : ((d && d.content) || []);\n" " window._contacts = rows;\n" " box.innerHTML = contactsHtml(rows);\n" " } else if (s === 'follow') {") txt = rep(txt, old_br, " } else if (s === 'follow') {", "swbranch") # 11. 旧 contactsHtml 整删(收编为 contactsShell 常驻面板) old_ch = ('/* ---- 联系人 ---- */\n' 'function contactsHtml(rows) {\n' " const t = tableHtml(['姓名', '职务', '电话', '关键', '内线', '来源', '礼品备注', '操作'], rows.map(c =>\n" " [esc(c.name || '--'), esc(c.jobTitleName || '--'), esc(c.phone || '--'),\n" " c.isKeyContact === 1 ? '是' : '', c.isInternal === 1 ? '是' : '', esc(c.source || '--'), esc(c.giftRemark || '--'),\n" " ' ']));\n" " return '
' + t;\n" '}') txt = rep(txt, old_ch, '/* ---- 联系人:收编为 contactsShell 常驻面板(p4/p6 注入),旧 contactsHtml 退役 ---- */', "contactsh") # 12. delContact:confirm 改 gConfirm,删除后图谱编辑态退出 + 双刷 old_dc = ("async function delContact(cid) {\n" " if (!confirm('确认删除该联系人?')) return;\n" " try { await api('/api/customer/contact/delete?id=' + cid, { method: 'POST' }); toast('已删除', 'ok'); switchSub('contacts'); }\n" " catch (e) { toast('失败: ' + e.message + codeSuffix(e), 'err'); }\n" "}") new_dc = ("async function delContact(cid) {\n" " gConfirm('删除联系人', '确认删除该联系人?删除后列表与图谱同步刷新。', async () => {\n" " try {\n" " await api('/api/customer/contact/delete?id=' + cid, { method: 'POST' });\n" " toast('已删除', 'ok');\n" " if (D.sub === 'contacts') { if (G.editing) doExitEdit(); await Promise.all([loadContacts(), loadGraph()]); layoutGraph(); renderContactsAll(); }\n" " } catch (e) { toast('失败: ' + e.message + codeSuffix(e), 'err'); }\n" " });\n" "}") txt = rep(txt, old_dc, new_dc, "delcontact") # 13. 注入 p4(图谱核心)与 p5+p6(编辑器 + 列表侧) txt = rep(txt, "/* ================= 弹窗框架 ================= */", p4 + "\n/* ================= 弹窗框架 ================= */", "p4") txt = rep(txt, "/* ================= 初始化 ================= */", p5 + "\n" + p6 + "\n/* ================= 初始化 ================= */", "p5p6") # 14. init:绑一次 contactsShell 事件 txt = rep(txt, " paintWho();\n if (TOKEN()) { loadList(); loadSummary(); }\n})();", " paintWho();\n bindContactsUI();\n if (TOKEN()) { loadList(); loadSummary(); }\n})();", "init") out = txt.replace("\n", "\r\n") if crlf else txt open(DEMO, "wb").write(out.encode("utf-8")) print("written:", DEMO, "lines =", len(out.splitlines()))