/* ---- 页内批量编辑(batchEdit:原子批任一失败零落库;字段 null=不改;脱敏形态(含*)提交=不改) ---- */ function enterListEdit() { C.editing = true; C.dirtyRows = {}; C.editSnapshot = {}; C.list.forEach(c => { C.editSnapshot[c.id] = JSON.parse(JSON.stringify(c)); }); renderContactsList(); toast('已进入页内编辑:可修改姓名/职务/电话/关键/来源;保存走 batchEdit(原子批)'); } function listDirtyCount() { return Object.keys(C.dirtyRows).length; } function doExitListEdit() { C.editing = false; C.dirtyRows = {}; renderContactsList(); } function resetListEditSilent() { Object.keys(C.editSnapshot).forEach(id => { const c = cById(id); if (c) Object.assign(c, C.editSnapshot[id]); }); C.dirtyRows = {}; } function btnResetListClick() { gConfirm('放弃修改', '确定放弃本次全部修改吗?数据将恢复到进入编辑模式时的状态。', () => { resetListEditSilent(); doExitListEdit(); toast('已放弃未保存修改'); }); } function editListToggle() { if (C.editing) exitListEditAttempt(); else enterListEdit(); } function exitListEditAttempt(after) { if (!listDirtyCount()) { doExitListEdit(); if (after) after(); return; } gUnsavedDlg({ discard: () => { resetListEditSilent(); doExitListEdit(); if (after) after(); toast('已放弃未保存修改'); }, save: () => { saveListAll().then(ok => { if (ok) { doExitListEdit(); if (after) after(); } }); } }); } /* 编辑态单元格 → 数据 + dirty 标记(input 不重渲染以保焦点)。 修复原型潜伏 bug:职务下拉 value=职务 code,须按「类别下的职务 code」反查(原型按类别 code 反查恒 miss → 页内改职务静默失效)。D1:职务变更 → 档位字典 level 跟随重算(levelSource=1 由服务端落库)。 */ function applyCellEdit(input, isInput) { const id = input.dataset.id, f = input.dataset.f, c = cById(id); if (!c) return; const v = input.value; if (f === 'jobFull') { const grp = JOB_DICT.find(g => (g.jobs || []).some(j => j.code === v)); const job = grp && grp.jobs.find(j => j.code === v); if (grp && job) { c._catCode = grp.code; c._jobCode = v; c.jobTitleName = job.name; c.jobTitleCategory = grp.cat; c.jobTitleLevel = job.level; } } else if (f === 'isKeyContact') { c[f] = Number(v); } else { c[f] = v; } C.dirtyRows[id] = true; const tr = input.closest('tr'); tr.classList.add('dirty-row'); if (!tr.querySelector('.undo-link')) { const td = tr.lastElementChild; const s = document.createElement('span'); s.className = 'undo-link'; s.dataset.op = 'undo'; s.dataset.id = id; s.textContent = '撤销修改'; td.appendChild(s); } } function undoRow(id) { const snap = C.editSnapshot[id], c = cById(id); if (snap && c) Object.assign(c, snap); delete C.dirtyRows[id]; renderContactsList(); toast('已撤销该行修改'); } async function saveListAll() { const dirtyIds = Object.keys(C.dirtyRows); if (!dirtyIds.length) { toast('无修改行'); return true; } let firstErr = null; for (const id of dirtyIds) { const c = cById(id); if (!c) continue; let msg = null; if (!c.name || !c.name.trim()) msg = '姓名必填'; else if (!(c._jobCode || jobCodeOfName(c.jobTitleName))) msg = '职务必填'; else if (c.phone && c.phone.indexOf('*') < 0 && !/^1\d{10}$/.test(c.phone)) msg = '电话须为 11 位手机号(脱敏回显=未修改可放行)'; if (msg && !firstErr) firstErr = { id: id, msg: msg }; } if (firstErr) { renderContactsList(); const tr = $('contactTbody').querySelector('tr[data-id="' + firstErr.id + '"]'); if (tr) { tr.classList.add('err-row'); tr.scrollIntoView({ block: 'center' }); } const c = cById(firstErr.id); toast('校验失败:' + (c ? c.name + ' — ' : '') + firstErr.msg + '(已定位首个错误行,不提交任何行)', 'err'); return false; } const form = new URLSearchParams(); dirtyIds.forEach((id, i) => { const c = cById(id); form.set('rows[' + i + '].id', c.id); form.set('rows[' + i + '].name', c.name.trim()); form.set('rows[' + i + '].jobTitleCode', c._jobCode || jobCodeOfName(c.jobTitleName)); form.set('rows[' + i + '].phone', c.phone || ''); form.set('rows[' + i + '].source', c.source || ''); form.set('rows[' + i + '].isKeyContact', Number(c.isKeyContact) ? '1' : '0'); }); try { const ret = await api('/api/customer/contact/batchEdit', { method: 'POST', body: form.toString() }); if (ret && ret.failedRows && ret.failedRows.length) { const first = ret.failedRows[0]; const c = cById(dirtyIds[first.rowIndex]); toast('保存失败(原子批零落库):' + (c ? c.name + ' — ' : '') + (first.reason || ''), 'err'); return false; } await Promise.all([loadContacts(), loadGraph()]); C.dirtyRows = {}; addLog('数据', '页内编辑保存全部:共 ' + dirtyIds.length + ' 行(batchEdit 原子批)'); toast('保存全部成功', 'ok'); layoutGraph(); renderContactsAll(); return true; } catch (err) { toast('保存失败(' + err.code + '):' + err.message, 'err'); return false; } }