You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

134 lines
7.9 KiB

# -*- 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(</style> 前)
txt = rep(txt, ".logday { font-weight: 600; color: #909399; margin: 10px 0 4px; font-size: 13px; }\n</style>",
".logday { font-weight: 600; color: #909399; margin: 10px 0 4px; font-size: 13px; }\n" + p1 + "\n</style>", "css")
# 2. contactsShell(subBox 之后、detailPanel 收口前)
txt = rep(txt, ' <div id="subBox"></div>\n </div>',
' <div id="subBox"></div>\n' + p2 + "\n </div>", "shell")
# 3. qa/cimp 弹窗(toast 前)
txt = rep(txt, '<div id="toast"></div>', p3 + "\n<div id=\"toast\"></div>", "modals")
# 4. apiDownload(登录节前)
txt = rep(txt, "/* ================= 登录 ================= */", p0 + "/* ================= 登录 ================= */", "apidl")
# 5. thead 加「关注」列
txt = rep(txt, "<th>客户名称</th><th>编号</th>", "<th>关注</th><th>客户名称</th><th>编号</th>", "thead")
# 6. rowHtml 星标 td
old_row = r''' '<td class="row" onclick="openDetail(\'' + r.id + '\')">' + esc(r.customerName) + '</td>' +'''
new_row = r''' '<td><span class="star-link' + (Number(r.focusFlag) ? '' : ' off') + '" title="关注/取关" onclick="event.stopPropagation();toggleFocus(\'' + r.id + '\',' + (Number(r.focusFlag) ? 0 : 1) + ')">' + (Number(r.focusFlag) ? '' : '') + '</span></td>' +
'<td class="row" onclick="openDetail(\'' + r.id + '\')">' + esc(r.customerName) + '</td>' +'''
txt = rep(txt, old_row, new_row, "rowstar")
# 7. colspan 11 → 12
txt = rep(txt, '<td colspan="11" class="empty">无数据</td>', '<td colspan="12" class="empty">无数据</td>', "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, "'<button onclick=\"dlgContact()\">+联系人</button>",
"'<button onclick=\"openQa()\">+快速添加</button>", "qabtn")
# 10. switchSub:contacts 改走 contactsShell 常驻面板(其余页签显隐 subBox)
old_sw = " const box = $('subBox');\n if (!D.id) { box.innerHTML = '<div class=\"empty\">请先打开一个客户</div>'; 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 = '<div class=\"empty\">请先打开一个客户</div>'; 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"
" '<button onclick=\"dlgContact(\\'' + c.id + '\\')\">编辑</button> <button onclick=\"delContact(\\'' + c.id + '\\')\">删除</button> <button onclick=\"dlgCheckPhone(\\'' + esc(c.phone || '') + '\\')\">查重</button>']));\n"
" return '<div class=\"toolbar\"><button onclick=\"dlgContact()\">+ 新建联系人</button></div>' + 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()))