dy-dashboard/static/js/department_matching.js
2026-10-29 18:24:00 +08:00

140 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 部门匹配独立规则库,不对业务数据执行回写。
var departmentMatchingState = {rows: [], kind: 'platform', page: 1, search: ''};
var departmentMatchingKinds = {platform: '平台层规则', brand: '品牌层规则', nonbrand: '非品牌清单', shop: '店铺归属对照表'};
var departmentMatchingFields = {
platform: [['department', '归属部门(必填)'], ['shop_keywords', '店铺关键词(逗号分隔)'], ['source_keywords', '空店铺来源(逗号分隔,精确匹配)']],
brand: [['keyword', '品牌关键词(必填)'], ['department', '默认归属'], ['mode', '匹配方式(必填)'], ['pdd_department', '拼多多归属'], ['overseas_department', '京东海外/国际归属'], ['notes', '判定说明']],
nonbrand: [['name', '名称(必填)'], ['reason', '为什么不是品牌'], ['guidance', '判定说明']],
shop: [['shop', '店铺(必填)'], ['department', '预期归属(必填)'], ['layer', '参考命中层级']]
};
var departmentMatchingModes = {fixed: '固定归属(全平台)', split: '按平台分配归属', pdd_only: '拼多多专属,其他回落平台层'};
async function departmentMatchingRequest(path, data) {
var response = await fetch('/api/department-matching/' + path, data === undefined ? {} : {
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)
});
var result = await response.json();
if (!response.ok || result.code !== 0) throw new Error(result.msg || '请求失败');
return result;
}
async function loadDepartmentMatchingPage(container) {
container.innerHTML = `<style>
.dm-table{border-collapse:collapse;width:100%;font-size:13px}
.dm-table th,.dm-table td{border:1px solid var(--border,#e5e7eb);padding:10px 12px;text-align:left;vertical-align:top;overflow-wrap:anywhere;max-width:360px}
.dm-table th{background:var(--bg-secondary,#f5f6f8);position:sticky;top:0;z-index:1;white-space:nowrap}
.dm-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:12px 0}
.dm-panel{padding:18px;margin-bottom:16px}.dm-help{color:var(--text-light,#6b7280);font-size:12px;line-height:1.8}
.dm-scroll{overflow:auto;max-height:calc(100vh - 470px);min-height:200px}
.dm-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
.dm-fields label{display:block;font-size:13px;margin-bottom:6px}.dm-fields .form-input{width:100%;box-sizing:border-box}
@media(max-width:700px){.dm-fields{grid-template-columns:1fr}}
</style><div class="page-header"><h1>部门匹配管理</h1></div>
<div class="card dm-panel"><strong>匹配测试</strong><div class="dm-help">品牌层优先 → 平台关键词按具体程度匹配 → 店铺为空时按来源精确兜底。仅试算归属,不修改业务数据。</div>
<div class="dm-toolbar"><input id="dmShop" class="form-input" style="width:360px;max-width:100%" placeholder="店铺名称" onkeydown="if(event.key==='Enter')testDepartmentMatching()">
<input id="dmSource" class="form-input" style="width:230px;max-width:100%" placeholder="数据来源(店铺为空时使用)" onkeydown="if(event.key==='Enter')testDepartmentMatching()">
<button class="btn btn-primary" onclick="testDepartmentMatching()">测试匹配</button><button class="btn" onclick="checkDepartmentMatching()">核验店铺对照表</button></div>
<div id="dmResult" role="status" class="dm-help">请输入店铺或数据来源进行测试。</div></div>
<div class="card dm-panel"><div id="dmTabs" class="dm-toolbar"></div>
<div class="dm-toolbar"><input id="dmSearch" class="form-input" style="width:300px" placeholder="搜索关键词、归属、店铺或说明" onkeydown="if(event.key==='Enter')searchDepartmentMatching(false)">
<button class="btn btn-primary" onclick="searchDepartmentMatching(false)">搜索</button><button class="btn" onclick="searchDepartmentMatching(true)">重置</button>
<button id="dmCreate" class="btn btn-primary" style="margin-left:auto;display:none" onclick="editDepartmentMatching(0)">新增</button></div>
<div class="dm-help" style="margin-bottom:10px">非品牌清单用于解释排除项;归属通过平台规则判定。平台层先比较关键词长度,长度相同时优先级数字越小越优先。</div>
<div id="dmTable" class="dm-scroll">正在加载…</div><div id="dmPager" class="dm-toolbar" style="justify-content:center"></div></div>`;
try {
var result = await departmentMatchingRequest('records');
departmentMatchingState.rows = result.data;
renderDepartmentMatching();
} catch (err) { document.getElementById('dmTable').textContent = err.message; }
}
function renderDepartmentMatching() {
var state = departmentMatchingState;
var tabs = document.getElementById('dmTabs');
if (!tabs) return;
tabs.innerHTML = Object.keys(departmentMatchingKinds).map(function(kind) {
return '<button class="btn ' + (state.kind === kind ? 'btn-primary' : '') + '" onclick="selectDepartmentMatchingTab(\'' + kind + '\')">' + departmentMatchingKinds[kind] + '(' + state.rows.filter(function(r){return r.kind === kind;}).length + ')</button>';
}).join('');
document.getElementById('dmCreate').style.display = window._hasPagePerm('department_matching', 'create') ? '' : 'none';
document.getElementById('dmSearch').value = state.search;
var rows = state.rows.filter(function(row) {
return row.kind === state.kind && (!state.search || departmentMatchingFields[row.kind].map(function(f){return row.payload[f[0]] || '';}).join(' ').toLowerCase().includes(state.search.toLowerCase()));
});
var pages = Math.max(1, Math.ceil(rows.length / 20));
state.page = Math.min(Math.max(state.page, 1), pages);
var fields = departmentMatchingFields[state.kind];
var canEdit = window._hasPagePerm('department_matching', 'edit');
var canDelete = window._hasPagePerm('department_matching', 'delete');
var rule = state.kind === 'platform' || state.kind === 'brand';
var showPriority = state.kind === 'platform';
var heads = '<th>序号</th>' + fields.map(function(f){return '<th>' + escHtml(f[1].replace('(必填)','')) + '</th>';}).join('') + (showPriority ? '<th>优先级</th>' : '') + (rule ? '<th>状态</th>' : '') + (canEdit || canDelete ? '<th>操作</th>' : '');
var body = rows.slice((state.page - 1) * 20, state.page * 20).map(function(row, i) {
return '<tr><td>' + ((state.page - 1) * 20 + i + 1) + '</td>' + fields.map(function(f) {
var value = row.payload[f[0]] || '';
if (f[0] === 'mode') value = departmentMatchingModes[value] || value;
return '<td>' + escHtml(String(value)) + '</td>';
}).join('') + (showPriority ? '<td>' + row.priority + '</td>' : '') + (rule ? '<td>' + (row.enabled ? '启用' : '停用') + '</td>' : '') + (canEdit || canDelete ? '<td style="white-space:nowrap">' +
(canEdit ? '<button class="btn btn-sm" onclick="editDepartmentMatching(' + row.id + ')">编辑</button> ' : '') +
(canDelete ? '<button class="btn btn-sm" style="color:#dc2626" onclick="deleteDepartmentMatching(' + row.id + ')">删除</button>' : '') + '</td>' : '') + '</tr>';
}).join('');
document.getElementById('dmTable').innerHTML = '<table class="dm-table"><thead><tr>' + heads + '</tr></thead><tbody>' + (body || '<tr><td colspan="15">暂无记录</td></tr>') + '</tbody></table>';
document.getElementById('dmPager').innerHTML = '<span>共 ' + rows.length + ' 条,每页20条</span><button class="btn" onclick="pageDepartmentMatching(-1)" ' + (state.page === 1 ? 'disabled' : '') + '>上一页</button><span>' + state.page + ' / ' + pages + '</span><button class="btn" onclick="pageDepartmentMatching(1)" ' + (state.page === pages ? 'disabled' : '') + '>下一页</button><input id="dmJump" class="form-input" type="number" min="1" max="' + pages + '" style="width:70px" value="' + state.page + '" onkeydown="if(event.key===\'Enter\')pageDepartmentMatching(0)"><button class="btn" onclick="pageDepartmentMatching(0)">跳转</button>';
}
function selectDepartmentMatchingTab(kind) { departmentMatchingState.kind=kind; departmentMatchingState.page=1; renderDepartmentMatching(); }
function searchDepartmentMatching(reset) { departmentMatchingState.search=reset?'':document.getElementById('dmSearch').value.trim(); departmentMatchingState.page=1; renderDepartmentMatching(); }
function pageDepartmentMatching(delta) {
departmentMatchingState.page = delta ? departmentMatchingState.page + delta : (parseInt(document.getElementById('dmJump').value,10) || 1);
renderDepartmentMatching();
}
function editDepartmentMatching(id) {
var row = departmentMatchingState.rows.find(function(r){return r.id === id;}) || {id:0, kind:departmentMatchingState.kind, payload:{}, priority:100, enabled:1};
document.getElementById('dmEditModal')?.remove();
var modal = document.createElement('div'); modal.id='dmEditModal'; modal.className='modal-overlay'; modal.style.cssText='display:flex;z-index:15000';
var fields = departmentMatchingFields[row.kind].map(function(f) {
var value = row.payload[f[0]] || '';
var control;
if (f[0] === 'mode') control = '<select class="form-input" data-dm-field="mode">' + Object.keys(departmentMatchingModes).map(function(mode) {return '<option value="' + mode + '" ' + (value === mode ? 'selected' : '') + '>' + departmentMatchingModes[mode] + '</option>';}).join('') + '</select>';
else control = '<textarea class="form-input" rows="' + (['notes','guidance','reason'].includes(f[0]) ? 3 : 2) + '" maxlength="4000" data-dm-field="' + f[0] + '">' + escHtml(String(value)) + '</textarea>';
return '<div><label>' + escHtml(f[1]) + '</label>' + control + '</div>';
}).join('');
var rule = row.kind === 'brand' || row.kind === 'platform';
modal.innerHTML = '<div class="modal" style="width:min(760px,94vw);max-height:90vh;display:flex;flex-direction:column"><div class="modal-header"><h3>' + (id ? '编辑' : '新增') + departmentMatchingKinds[row.kind] + '</h3><button class="modal-close" onclick="document.getElementById(\'dmEditModal\').remove()">×</button></div><form id="dmEditForm" style="display:flex;flex-direction:column;min-height:0"><div class="modal-body" style="overflow:auto"><div class="dm-fields">' + fields + (rule ? '<div><label>优先级(0–100000,数字越小越优先)</label><input id="dmPriority" type="number" min="0" max="100000" required class="form-input" value="' + row.priority + '"></div><div><label>状态</label><select id="dmEnabled" class="form-input"><option value="1">启用</option><option value="0" ' + (!row.enabled ? 'selected' : '') + '>停用</option></select></div>' : '') + '</div><div class="dm-help" style="margin-top:12px">品牌关键词取自店铺名称。按平台分配时,拼多多优先使用拼多多归属,京东海外/国际其次,其他使用默认归属。非品牌条目用于说明,不独立覆盖归属。</div><div id="dmEditError" style="color:#dc2626;margin-top:8px" role="alert"></div></div><div class="modal-footer"><button type="button" class="btn" onclick="document.getElementById(\'dmEditModal\').remove()">取消</button><button id="dmSave" type="submit" class="btn btn-primary">保存</button></div></form></div>';
if (row.kind === 'brand') modal.querySelector('#dmPriority').parentElement.remove();
document.body.appendChild(modal);
document.getElementById('dmEditForm').onsubmit = async function(event) {
event.preventDefault();
var payload=Object.assign({},row.payload); modal.querySelectorAll('[data-dm-field]').forEach(function(el){payload[el.dataset.dmField]=el.value;});
var button=document.getElementById('dmSave'); button.disabled=true;
try {
await departmentMatchingRequest('save', {id:row.id, kind:row.kind, payload:payload, priority:row.kind==='platform'?Number(document.getElementById('dmPriority').value):row.priority, enabled:rule?Number(document.getElementById('dmEnabled').value):1});
departmentMatchingState.rows=(await departmentMatchingRequest('records')).data;
modal.remove(); renderDepartmentMatching();
} catch(err) { document.getElementById('dmEditError').textContent=err.message; button.disabled=false; }
};
}
async function deleteDepartmentMatching(id) {
if (!confirm('确认删除此记录?删除规则会影响后续匹配测试结果。')) return;
try { await departmentMatchingRequest('delete',{id:id}); departmentMatchingState.rows=(await departmentMatchingRequest('records')).data; renderDepartmentMatching(); }
catch(err) { alert(err.message); }
}
async function testDepartmentMatching() {
var el=document.getElementById('dmResult');
try {
var data=(await departmentMatchingRequest('test',{shop:document.getElementById('dmShop').value, source:document.getElementById('dmSource').value})).data;
el.textContent='归属:' + (data.department || '未匹配') + ' | ' + data.layer + ' | ' + data.reason;
} catch(err) { el.textContent=err.message; }
}
async function checkDepartmentMatching() {
var el=document.getElementById('dmResult'); el.textContent='正在核验…';
try {
var data=(await departmentMatchingRequest('check',{})).data;
el.textContent='核验完成:' + data.passed + ' / ' + data.total + ' 条归属一致。';
if (data.mismatches.length) el.innerHTML += '<div style="overflow:auto;max-height:240px"><table class="dm-table"><thead><tr><th>店铺</th><th>预期归属</th><th>实际归属</th><th>原因</th></tr></thead><tbody>' + data.mismatches.map(function(r){return '<tr><td>' + escHtml(r.shop) + '</td><td>' + escHtml(r.expected) + '</td><td>' + escHtml(r.actual || '未匹配') + '</td><td>' + escHtml(r.reason) + '</td></tr>';}).join('') + '</tbody></table></div>';
} catch(err) { el.textContent=err.message; }
}