/* ================= 联系人图谱/页签增强(票02 收编自 customer-contact-graph demo 修订⑧版) ================= */ /* 修订⑧冻结口径:等级 1-10(10=顶层)、只有手动边(无自动生成/否决)、公司边 = level==10 前端派生视图不可删、 布局纯由 level 推导零坐标、graph/save 全量快照 + version CAS(67017 冲突退出编辑并重拉)。 数据分工:列表(ContactDTO 全字段)= 旧端点 /api/customer/contacts;图谱(脱敏+等级+边)= graph/detail。 */ const COMPANY_ID = '__company__'; const CARD_W = 150, CARD_H = 104; const COMPANY_POS = { x: 1080, y: 184 }; const ROW_X = 20, ROW_W = 1910; const UNREC_X = 1960, UNREC_W = 420, UNREC_Y = 40, UNREC_H = 1320; const LV10_Y = 40, LV9_Y = 328, ROW_STEP = 116; function lvRowY(lv) { return LV9_Y + (9 - lv) * ROW_STEP; } const ROWS = []; for (let lv = 10; lv >= 1; lv--) ROWS.push({ lv: lv, y: lv === 10 ? LV10_Y : lvRowY(lv) }); /* G = 图谱态;C = 列表态(列表数据源 = 旧端点 ContactDTO 全字段) */ const G = { contacts: [], edges: [], version: 0, editing: false, pendingAdds: [], pendingDeletes: [], editSnapshot: null, cardPos: {}, cycleHighlight: null, logs: [], view: { zoom: 0.62, panX: 0, panY: 0 }, fitOnce: false, curView: 'list' }; const C = { list: [], query: { keyword: '', internal: '' }, checked: {}, editing: false, editSnapshot: {}, dirtyRows: {}, revealAll: false }; let revealed = {}; // id -> true(单点临时明文;列表/图谱共享) let JOB_DICT = []; // [{cat, code, jobs:[{name, code, level}]}] let SOURCE_DICT = []; // [{code, name}] let qaRows = []; const wrap = $('graphWrap'), canvasInner = $('canvasInner'), edgeSvg = $('edgeSvg'); const custName = () => D.cname || ('客户 ' + (D.id || '')); const opName = () => { const m = ME(); return (m && (m.userName || m.nickName)) || '当前用户'; }; function nowStr() { return new Date().toTimeString().slice(0, 8); } function addLog(type, text) { G.logs.push({ time: nowStr(), type: type, text: text }); renderGraphLogs(); } function maskPhone(p) { return p ? p.slice(0, 3) + '****' + p.slice(-4) : '--'; } function byId(id) { return G.contacts.find(c => c.id === id); } // 图谱节点 function cById(id) { return C.list.find(c => String(c.id) === String(id)); } // 列表行 function cname(id) { return id === COMPANY_ID ? custName() : (byId(id) || { name: id }).name; } function jobCodeOfName(name) { for (const g of JOB_DICT) { const j = (g.jobs || []).find(x => x.name === name); if (j) return j.code; } return ''; } function sourceName(code) { const s = SOURCE_DICT.find(x => x.code === code); return s ? s.name : (code || '--'); } let dictLoaded = false; async function ensureDicts() { if (dictLoaded) return; const pair = await Promise.all([ api('/api/dict/item/tree?groupCode=job_title').catch(() => []), api('/api/dict/item/enabled-list?groupCode=contact_source').catch(() => []) ]); JOB_DICT = (pair[0] || []).map(g => ({ cat: g.name, code: g.code, jobs: (g.children || []).map(j => ({ name: j.name, code: j.code, level: j.level == null ? null : Number(j.level) })) })); SOURCE_DICT = (pair[1] || []).map(s => ({ code: s.code, name: s.name })); dictLoaded = true; }