Files
CleanFlow/static/index.html
2025-10-07 23:19:05 +08:00

658 lines
29 KiB
HTML
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.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>🌌 恶意客户智能匹配平台</title>
<!-- Tailwind CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* 小动画与样式 */
.rotate { transition: transform .22s ease; display:inline-block; }
.rotate.open { transform: rotate(90deg); }
.fade-up { transform: translateY(6px); opacity: 0; transition: all .28s ease; }
.fade-up.in { transform: translateY(0); opacity: 1; }
.glass { background: linear-gradient(180deg, rgba(255,255,255,0.75), rgba(255,255,255,0.62)); backdrop-filter: blur(6px); }
.scrollbar-thin::-webkit-scrollbar { height:8px; width:8px; }
.scrollbar-thin::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.12); border-radius: 8px; }
</style>
</head>
<body class="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-purple-100 font-sans">
<div class="max-w-6xl mx-auto p-6">
<!-- Header -->
<header class="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-6">
<div>
<h1 class="text-3xl font-extrabold text-gray-900 flex items-center gap-3">
<span>🚀 恶意客户智能匹配平台</span>
</h1>
<p class="text-sm text-gray-600 mt-1">上传订单 Excel选择客户库进行快速匹配 — 地址 → 客户 → 订单 三层视图,支持 CRUD 管理。</p>
</div>
<div class="flex gap-3">
<div class="flex items-center gap-2 text-sm text-gray-600">
<div class="w-2 h-2 bg-blue-400 rounded-full"></div><span>分析</span>
<div class="w-2 h-2 bg-red-400 rounded-full ml-2"></div><span>管理</span>
</div>
</div>
</header>
<!-- Tabs -->
<nav class="mb-6 bg-transparent">
<div class="inline-flex rounded-xl bg-white/70 glass border border-gray-200 p-1">
<button id="tabAnalyze" class="tab-btn px-4 py-2 rounded-lg font-medium text-sm text-gray-800 bg-gradient-to-r from-blue-50 to-purple-50 shadow-sm">📦 订单分析</button>
<button id="tabManage" class="tab-btn px-4 py-2 rounded-lg font-medium text-sm text-gray-600 hover:text-gray-800">🧠 恶意客户管理</button>
</div>
</nav>
<!-- Main content -->
<main>
<!-- ========== 分析面板 ========== -->
<section id="panelAnalyze" class="fade-up in">
<div class="grid grid-cols-1 md:grid-cols-12 gap-6">
<!-- Left: 控件 -->
<div class="md:col-span-4 bg-white glass rounded-2xl p-5 shadow">
<h2 class="text-lg font-semibold mb-3">上传 & 分析</h2>
<label class="text-sm text-gray-600">订单文件 (.xlsx)</label>
<input id="fileInput" type="file" accept=".xlsx" class="mt-2 w-full border rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-blue-300" />
<label class="text-sm text-gray-600 mt-4 block">选择恶意客户库</label>
<select id="suspectSelect" class="mt-2 w-full border rounded-lg p-2"></select>
<div class="mt-4 flex gap-2">
<button id="analyzeBtn" class="flex-1 bg-gradient-to-r from-blue-500 to-purple-500 text-white font-semibold px-4 py-2 rounded-lg shadow hover:scale-[1.02] transition">⚡ 发起分析</button>
<button id="refreshBtn" title="刷新客户库" class="px-4 py-2 rounded-lg border bg-white"></button>
</div>
<p id="statusText" class="mt-3 text-sm text-gray-500">状态:未开始</p>
<!-- 浮动统计(小版)-->
<div id="statsMini" class="mt-4 bg-white rounded-lg p-3 border flex flex-col gap-2">
<div class="flex justify-between text-sm text-gray-600"><span>地址:</span><strong id="miniAddr">0</strong></div>
<div class="flex justify-between text-sm text-gray-600"><span>订单:</span><strong id="miniOrders">0</strong></div>
<div class="flex justify-between text-sm text-gray-600"><span>恶意客户:</span><strong id="miniSuspects">0</strong></div>
</div>
<!-- 说明 -->
<div class="mt-4 text-xs text-gray-500">
地址层默认展开;点击客户可展开订单列表。支持查看 note / info_url / registertime。
</div>
</div>
<!-- Right: 结果 -->
<div class="md:col-span-8 space-y-4">
<!-- 搜索 / filters -->
<div class="flex gap-3 items-center">
<input id="globalSearch" placeholder="🔎 搜索:姓名 / 电话 / 地址 / 订单号" class="flex-1 p-2 rounded-lg border" />
<div class="text-sm text-gray-500">显示:<span id="displayCount">0</span> 条匹配</div>
</div>
<!-- 结果容器 -->
<div id="resultsContainer" class="space-y-4"></div>
</div>
</div>
</section>
<!-- ========== 管理面板 ========== -->
<section id="panelManage" class="hidden">
<div class="bg-white glass rounded-2xl p-5 shadow">
<div class="flex gap-4 md:gap-8 items-start">
<div class="w-full md:w-1/3">
<h3 class="font-semibold text-lg">管理客户库</h3>
<div class="mt-3">
<label class="text-sm text-gray-600">选择库文件</label>
<div class="flex gap-2 mt-2">
<select id="manageSelect" class="flex-1 border rounded-lg p-2"></select>
<button id="createFileBtn" class="px-3 py-2 rounded-lg bg-green-50 border border-green-200">新建</button>
</div>
<input id="newFileName" placeholder="文件名(不带 .json" class="mt-2 p-2 border rounded-lg w-full" />
</div>
<div class="mt-4">
<label class="text-sm text-gray-600">搜索记录</label>
<input id="manageSearch" placeholder="按姓名/手机号/地址搜索" class="mt-2 p-2 border rounded-lg w-full" />
</div>
<div class="mt-4">
<button id="addRecordBtn" class="w-full bg-blue-600 text-white py-2 rounded-lg shadow"> 添加记录</button>
</div>
</div>
<div class="flex-1">
<h3 class="font-semibold text-lg">记录列表</h3>
<div id="recordsList" class="mt-3 space-y-2 max-h-[60vh] overflow-auto p-2 scrollbar-thin"></div>
</div>
</div>
</div>
</section>
</main>
</div>
<!-- 全局浮动统计框(左下) -->
<div id="statsBox" class="fixed bottom-6 left-6 bg-white/95 glass rounded-xl shadow-lg p-4 w-64 hidden">
<div class="text-sm text-gray-600">分析统计</div>
<div class="mt-2">
<div class="flex justify-between text-sm"><span>地址数:</span><strong id="statAddress">0</strong></div>
<div class="flex justify-between text-sm"><span>订单数:</span><strong id="statOrders">0</strong></div>
<div class="flex justify-between text-sm"><span>恶意客户数:</span><strong id="statSuspects">0</strong></div>
</div>
</div>
<!-- 弹窗:添加/编辑记录 -->
<div id="recordModal" class="fixed inset-0 bg-black/40 hidden items-center justify-center p-4">
<div class="bg-white rounded-2xl w-full max-w-2xl p-5 shadow-lg">
<div class="flex justify-between items-center">
<h4 id="recordModalTitle" class="font-semibold">添加记录</h4>
<button id="closeRecordModal" class="text-gray-500"></button>
</div>
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3">
<input id="f_name" placeholder="姓名" class="p-2 border rounded" />
<input id="f_phone" placeholder="手机号" class="p-2 border rounded" />
<input id="f_address" placeholder="地址(省 市 区)" class="p-2 border rounded col-span-1 md:col-span-2" />
<input id="f_store" placeholder="店铺" class="p-2 border rounded" />
<input id="f_order_id" placeholder="关联订单号允许多个逗号分隔系统会保留后4位" class="p-2 border rounded" />
<input id="f_info_url" placeholder="信息链接info_url" class="p-2 border rounded" />
<textarea id="f_note" placeholder="备注 / note" class="p-2 border rounded col-span-1 md:col-span-2"></textarea>
</div>
<div class="mt-4 flex justify-end gap-2">
<button id="cancelRecord" class="px-4 py-2 rounded border">取消</button>
<button id="saveRecord" class="px-4 py-2 rounded bg-green-600 text-white">保存</button>
</div>
</div>
</div>
<!-- 简易确认模态 -->
<div id="confirmModal" class="fixed inset-0 bg-black/40 hidden items-center justify-center p-4">
<div class="bg-white rounded-xl p-4 shadow">
<div id="confirmText" class="text-sm text-gray-700"></div>
<div class="mt-3 flex justify-end gap-2">
<button id="confirmNo" class="px-3 py-1 rounded border">取消</button>
<button id="confirmYes" class="px-3 py-1 rounded bg-red-600 text-white">确认</button>
</div>
</div>
</div>
<script>
// ---------- DOM ----------
const tabAnalyze = document.getElementById('tabAnalyze');
const tabManage = document.getElementById('tabManage');
const panelAnalyze = document.getElementById('panelAnalyze');
const panelManage = document.getElementById('panelManage');
const fileInput = document.getElementById('fileInput');
const suspectSelect = document.getElementById('suspectSelect');
const analyzeBtn = document.getElementById('analyzeBtn');
const refreshBtn = document.getElementById('refreshBtn');
const statusText = document.getElementById('statusText');
const resultsContainer = document.getElementById('resultsContainer');
const globalSearch = document.getElementById('globalSearch');
const displayCount = document.getElementById('displayCount');
const statsBox = document.getElementById('statsBox');
const statAddress = document.getElementById('statAddress');
const statOrders = document.getElementById('statOrders');
const statSuspects = document.getElementById('statSuspects');
const miniAddr = document.getElementById('miniAddr');
const miniOrders = document.getElementById('miniOrders');
const miniSuspects = document.getElementById('miniSuspects');
// 管理面板 DOM
const manageSelect = document.getElementById('manageSelect');
const createFileBtn = document.getElementById('createFileBtn');
const newFileName = document.getElementById('newFileName');
const manageSearch = document.getElementById('manageSearch');
const addRecordBtn = document.getElementById('addRecordBtn');
const recordsList = document.getElementById('recordsList');
// record modal
const recordModal = document.getElementById('recordModal');
const recordModalTitle = document.getElementById('recordModalTitle');
const closeRecordModal = document.getElementById('closeRecordModal');
const cancelRecord = document.getElementById('cancelRecord');
const saveRecord = document.getElementById('saveRecord');
const f_name = document.getElementById('f_name');
const f_phone = document.getElementById('f_phone');
const f_address = document.getElementById('f_address');
const f_store = document.getElementById('f_store');
const f_order_id = document.getElementById('f_order_id');
const f_info_url = document.getElementById('f_info_url');
const f_note = document.getElementById('f_note');
// confirm modal
const confirmModal = document.getElementById('confirmModal');
const confirmText = document.getElementById('confirmText');
const confirmYes = document.getElementById('confirmYes');
const confirmNo = document.getElementById('confirmNo');
// state
let currentSuspectFile = null;
let currentRecords = [];
let editingIndex = -1;
let latestResults = { fullMatches: [], cityMatches: [], totalOrders: 0 };
// ---------- Utils ----------
function show(el){ el.classList.remove('hidden'); el.style.display='flex'; }
function hide(el){ el.classList.add('hidden'); el.style.display='none'; }
async function fetchJson(url, opts={}) {
const r = await fetch(url, opts);
return r.json();
}
// ---------- Tabs ----------
tabAnalyze.addEventListener('click', () => {
panelAnalyze.classList.remove('hidden');
panelManage.classList.add('hidden');
tabAnalyze.classList.remove('text-gray-600');
tabManage.classList.remove('bg-gradient-to-r','from-blue-50','to-purple-50');
tabAnalyze.classList.add('bg-gradient-to-r','from-blue-50','to-purple-50');
});
tabManage.addEventListener('click', () => {
panelManage.classList.remove('hidden');
panelAnalyze.classList.add('hidden');
tabAnalyze.classList.remove('bg-gradient-to-r','from-blue-50','to-purple-50');
tabManage.classList.add('bg-gradient-to-r','from-blue-50','to-purple-50');
});
// ---------- Load suspect files ----------
async function loadSuspectOptions(){
try{
const res = await fetchJson('/api/suspects');
const files = res.files || [];
suspectSelect.innerHTML = files.map(f => `<option value="${f}">${f}</option>`).join('');
manageSelect.innerHTML = files.map(f => `<option value="${f}">${f}</option>`).join('');
if(files.length){
suspectSelect.value = files[0];
manageSelect.value = files[0];
currentSuspectFile = files[0];
} else {
currentSuspectFile = null;
}
} catch(err){
console.error(err);
}
}
refreshBtn.addEventListener('click', loadSuspectOptions);
// ---------- Analyze ----------
analyzeBtn.addEventListener('click', async () => {
if(!fileInput.files.length){ alert('请选择 .xlsx 文件'); return; }
if(!suspectSelect.value){ alert('请选择恶意客户库'); return; }
const form = new FormData();
form.append('file', fileInput.files[0]);
form.append('suspectFile', suspectSelect.value);
statusText.textContent = '分析中...';
analyzeBtn.disabled = true;
resultsContainer.innerHTML = '';
try{
const r = await fetch('/api/analyze', { method: 'POST', body: form });
const data = await r.json();
if(data.error){ statusText.textContent = '错误:' + data.error; analyzeBtn.disabled=false; return; }
latestResults = data;
statusText.textContent = `分析完成:共 ${data.totalOrders} 条订单`;
renderResults(data);
} catch(err){
console.error(err);
statusText.textContent = '网络或服务异常';
} finally {
analyzeBtn.disabled = false;
}
});
// ---------- Render Results (三层结构) ----------
function renderResults(data){
resultsContainer.innerHTML = '';
const full = data.fullMatches || [];
const city = data.cityMatches || [];
let totalCustomers = 0;
let totalOrders = data.totalOrders || 0;
let totalAddresses = full.length;
// helper: create address/city section (address default expanded)
function createTopSection(title, groups, isCity=false){
if(!groups || groups.length===0) return;
const section = document.createElement('section');
section.className = 'bg-white rounded-2xl p-4 shadow-lg';
const header = document.createElement('div');
header.className = 'flex items-center justify-between cursor-pointer';
header.innerHTML = `<div class="text-lg font-semibold">${isCity ? '🏙️ 城市级匹配:' : '📍 地址匹配:'} ${title}</div>
<div class="text-sm text-gray-500">(点击收起/展开) <span class="ml-2 rotate">▶</span></div>`;
const content = document.createElement('div');
content.className = 'mt-3 space-y-3';
// 默认展开地址层content visible; rotate open
content.style.display = '';
header.querySelector('.rotate').classList.add('open');
header.addEventListener('click', () => {
if(content.style.display === 'none'){ content.style.display=''; header.querySelector('.rotate').classList.add('open'); }
else { content.style.display='none'; header.querySelector('.rotate').classList.remove('open'); }
});
// groups: array of { address|city, customers:[...] }
groups.forEach(g => {
// each g has .address or .city and .customers
const areaTitle = isCity ? g.city : g.address;
const areaBlock = document.createElement('div');
areaBlock.className = 'p-3 bg-gray-50 border border-gray-100 rounded-lg';
const areaHeader = document.createElement('div');
areaHeader.className = 'text-sm font-medium text-gray-700 mb-2';
areaHeader.textContent = areaTitle;
const customersWrap = document.createElement('div');
customersWrap.className = 'space-y-2';
g.customers.forEach(c => {
totalCustomers++;
const custCard = document.createElement('div');
custCard.className = 'bg-white rounded-lg border p-3 flex flex-col gap-2 shadow-sm';
// customer header (click to toggle orders)
const custHeader = document.createElement('div');
custHeader.className = 'flex items-center justify-between cursor-pointer';
const left = document.createElement('div');
left.innerHTML = `<div class="text-red-600 font-semibold">${c.name || '—'} <span class="text-xs text-gray-500">(${c.phone||'—'})</span></div>
<div class="text-xs text-gray-500">${c.address || ''}</div>`;
const right = document.createElement('div');
right.innerHTML = `<div class="text-xs text-gray-500">注册:${c.registertime || '-'}</div>
<div class="text-xs text-gray-500">${(c.order_id||[]).length ? ('订单:' + (Array.isArray(c.order_id)? c.order_id.join(',') : c.order_id)) : ''}</div>`;
const arrow = document.createElement('span');
arrow.className = 'rotate text-gray-500';
arrow.textContent = '▶';
right.appendChild(arrow);
custHeader.appendChild(left);
custHeader.appendChild(right);
const details = document.createElement('div');
details.className = 'mt-2 ml-2 border-l border-gray-100 pl-3 hidden';
// note / link
const noteP = document.createElement('div');
noteP.className = 'text-sm text-gray-600 mb-2';
noteP.innerHTML = `📝 ${c.note || '-'} &nbsp; <a href="${c.info_url||'#'}" target="_blank" class="text-blue-500 underline text-xs ml-2">🔗 详情</a>`;
// orders list
const ordersWrap = document.createElement('div');
ordersWrap.className = 'grid gap-2';
(c.matched_orders || []).forEach(o => {
const orderEl = document.createElement('div');
orderEl.className = 'flex items-center justify-between bg-blue-50 border border-blue-100 rounded p-2 text-sm';
orderEl.innerHTML = `<div>📦 <strong>${o.orderId}</strong> &nbsp; <span class="text-gray-600 text-xs">(${o.orderName||''}/${o.orderPhone||''})</span></div>
<div class="text-blue-600 text-sm">${o.score} 分 <span class="text-xs text-gray-500">(${o.percentage}%)</span></div>`;
ordersWrap.appendChild(orderEl);
});
details.appendChild(noteP);
details.appendChild(ordersWrap);
// default: customer level COLLAPSED (only address expanded as requested)
custHeader.addEventListener('click', () => {
if(details.classList.contains('hidden')) {
details.classList.remove('hidden');
arrow.classList.add('open');
} else {
details.classList.add('hidden');
arrow.classList.remove('open');
}
});
custCard.appendChild(custHeader);
custCard.appendChild(details);
customersWrap.appendChild(custCard);
});
areaBlock.appendChild(areaHeader);
areaBlock.appendChild(customersWrap);
content.appendChild(areaBlock);
});
section.appendChild(header);
section.appendChild(content);
resultsContainer.appendChild(section);
}
// render both
if(full && full.length){
createTopSection('全部地址(默认展开)', full, false);
}
if(city && city.length){
createTopSection('城市汇总', city, true);
}
// stats
statAddress.textContent = (data.fullMatches || []).length;
statOrders.textContent = data.totalOrders || 0;
statSuspects.textContent = totalCustomers;
miniAddr.textContent = statAddress.textContent;
miniOrders.textContent = statOrders.textContent;
miniSuspects.textContent = statSuspects.textContent;
// show stats box
statsBox.classList.remove('hidden');
// display count
displayCount.textContent = (totalCustomers + (data.fullMatches||[]).length);
}
// Search filter (client-side)
globalSearch.addEventListener('input', (e) => {
const q = e.target.value.trim().toLowerCase();
if(!q){
// re-render last results
if(latestResults && latestResults.totalOrders) renderResults(latestResults);
return;
}
// filter by scanning latestResults and keep only nodes that match
const filtered = { fullMatches: [], cityMatches: [], totalOrders: latestResults.totalOrders||0 };
function filterGroups(groups){
const out = [];
(groups||[]).forEach(g => {
const customers = (g.customers||[]).map(c => {
const matched_orders = (c.matched_orders||[]).filter(o => {
return (''+o.orderId).toLowerCase().includes(q) ||
(''+o.orderName).toLowerCase().includes(q) ||
(''+o.orderPhone).toLowerCase().includes(q);
});
const custMatch = (''+c.name).toLowerCase().includes(q) ||
(''+c.phone).toLowerCase().includes(q) ||
(''+c.address).toLowerCase().includes(q) ||
(''+(c.order_id||[]).join(',')).toLowerCase().includes(q);
// include customer if customer fields match OR some matched_orders match
if(custMatch || matched_orders.length) {
// clone with filtered orders
const clone = Object.assign({}, c);
clone.matched_orders = matched_orders.length ? matched_orders : c.matched_orders || [];
return clone;
}
return null;
}).filter(Boolean);
if(customers.length) out.push(Object.assign({}, g, { customers }));
});
return out;
}
filtered.fullMatches = filterGroups(latestResults.fullMatches);
filtered.cityMatches = filterGroups(latestResults.cityMatches);
renderResults(filtered);
});
// ---------- Management (CRUD) ----------
async function loadRecordsForSelected(){
const fname = manageSelect.value;
if(!fname) { recordsList.innerHTML = '<div class="text-sm text-gray-500">无文件</div>'; return; }
currentSuspectFile = fname;
try{
const res = await fetchJson('/api/suspects/' + encodeURIComponent(fname));
if(res.error){ recordsList.innerHTML = '<div class="text-sm text-red-600">加载失败</div>'; return; }
currentRecords = res.data || [];
renderRecordsList(currentRecords);
} catch(err){
console.error(err);
recordsList.innerHTML = '<div class="text-sm text-red-600">加载失败</div>';
}
}
function renderRecordsList(list){
const q = (manageSearch.value||'').trim().toLowerCase();
recordsList.innerHTML = '';
if(!list.length) recordsList.innerHTML = '<div class="text-sm text-gray-500">空记录</div>';
list.forEach((r, idx) => {
const text = `${r.name||''} ${r.phone||''} ${r.address||''}`.toLowerCase();
if(q && text.indexOf(q) === -1) return;
const row = document.createElement('div');
row.className = 'p-3 rounded-lg border flex items-start justify-between gap-3 bg-white';
row.innerHTML = `<div>
<div class="font-medium">${r.name || '—'} <span class="text-xs text-gray-500">(${r.phone||''})</span></div>
<div class="text-xs text-gray-600">${r.address || ''}</div>
<div class="text-xs text-gray-400 mt-1">注册:${r.registertime || '-'}</div>
<div class="text-xs text-gray-500 mt-1">订单:${r.order_id || ''}</div>
</div>
<div class="flex flex-col gap-2 items-end">
<button class="editBtn text-indigo-600 text-sm">编辑</button>
<button class="delBtn text-red-600 text-sm">删除</button>
</div>`;
// handlers
row.querySelector('.editBtn').addEventListener('click', () => openEditRecord(idx));
row.querySelector('.delBtn').addEventListener('click', () => confirmDelete(idx));
recordsList.appendChild(row);
});
}
manageSelect.addEventListener('change', loadRecordsForSelected);
manageSearch.addEventListener('input', () => renderRecordsList(currentRecords));
// create library file
createFileBtn.addEventListener('click', async () => {
const name = (newFileName.value||'').trim();
if(!name) { alert('请输入文件名'); return; }
try{
const res = await fetchJson('/api/suspects/' + encodeURIComponent(name), {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ data: [] })
});
if(res.error) throw new Error(res.error);
await loadSuspectOptions();
alert('创建成功');
} catch(err){
alert('创建失败:' + err.message);
}
});
// add / edit record
addRecordBtn.addEventListener('click', openAddRecord);
closeRecordModal.addEventListener('click', ()=> hide(recordModal));
cancelRecord.addEventListener('click', ()=> hide(recordModal));
function openAddRecord(){
editingIndex = -1;
recordModalTitle.textContent = '添加记录';
f_name.value=''; f_phone.value=''; f_address.value=''; f_store.value=''; f_order_id.value=''; f_info_url.value=''; f_note.value='';
show(recordModal);
}
function openEditRecord(idx){
editingIndex = idx;
const r = currentRecords[idx];
if(!r) return;
recordModalTitle.textContent = '编辑记录';
f_name.value = r.name || '';
f_phone.value = r.phone || '';
f_address.value = r.address || '';
f_store.value = r.store || '';
f_order_id.value = r.order_id || '';
f_info_url.value = r.info_url || '';
f_note.value = r.note || '';
show(recordModal);
}
saveRecord.addEventListener('click', async () => {
if(!currentSuspectFile){ alert('请先选择或创建客户库'); return; }
const payload = {
name: (f_name.value||'').trim(),
phone: (f_phone.value||'').trim(),
address: (f_address.value||'').trim(),
store: (f_store.value||'').trim(),
order_id: (f_order_id.value||'').trim(),
note: (f_note.value||'').trim(),
info_url: (f_info_url.value||'').trim()
};
try {
if(editingIndex === -1){
const res = await fetchJson('/api/suspects/' + encodeURIComponent(currentSuspectFile) + '/records', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
if(res.error) throw new Error(res.error);
alert('添加成功');
} else {
const res = await fetchJson('/api/suspects/' + encodeURIComponent(currentSuspectFile) + '/records/' + editingIndex, {
method: 'PUT',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
if(res.error) throw new Error(res.error);
alert('更新成功');
}
hide(recordModal);
await loadRecordsForSelected();
} catch(err){
alert('保存失败:' + err.message);
}
});
// delete record with confirm
function confirmDelete(idx){
confirmText.textContent = '确认删除该记录?该操作不可恢复。';
show(confirmModal);
confirmYes.onclick = async () => {
try{
const res = await fetchJson('/api/suspects/' + encodeURIComponent(currentSuspectFile) + '/records/' + idx, { method: 'DELETE' });
if(res.error) throw new Error(res.error);
hide(confirmModal);
await loadRecordsForSelected();
} catch(err){
alert('删除失败:' + err.message);
}
};
confirmNo.onclick = () => hide(confirmModal);
}
// ---------- Init ----------
(async function init(){
await loadSuspectOptions();
// set default to analyze tab
tabAnalyze.click();
// set manage panel default data
await loadRecordsForSelected();
})();
</script>
</body>
</html>