Files
wxsales/demo/web/static/index.html
T
freedakgmail 5926c73ff1 微信营销管理系统 - 益童宝销售管理平台
- 数据库: PostgreSQL schema + 3名销售/21客户/341消息/6成交
- 采集代理: mock_sync.py 模拟微信聊天同步
- AI分析: analyze.py 规则模式 + 千问LLM模式
- 后端: FastAPI 11个API接口
- 前端: 仪表盘/销售列表/客户列表/成交记录/交流分析/录入成交
- 交流分析: 全部客户对话概览 + LLM标准范式对话生成
- 部署: systemd + nginx, 已部署至 sale.all8ai.top
2026-07-14 22:31:29 +08:00

694 lines
29 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微信营销管理系统</title>
<link rel="stylesheet" href="/tailwind.min.css">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
.chat-bubble { max-width: 70%; padding: 8px 14px; border-radius: 12px; margin: 4px 0; }
.chat-sales { background: #95ec69; align-self: flex-start; }
.chat-customer { background: #fff; border: 1px solid #e5e7eb; align-self: flex-end; }
.chat-other { background: #f3f4f6; align-self: flex-end; }
</style>
</head>
<body class="bg-gray-50 text-gray-900">
<nav class="bg-white border-b shadow-sm sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 flex items-center h-14 gap-6">
<span class="font-bold text-lg text-green-600">益童宝销售管理系统</span>
<button onclick="showPanel('dashboard')" class="nav-btn text-sm hover:text-green-600">仪表盘</button>
<button onclick="showPanel('salespersons')" class="nav-btn text-sm hover:text-green-600">销售列表</button>
<button onclick="showPanel('customers')" class="nav-btn text-sm hover:text-green-600">客户列表</button>
<button onclick="showPanel('deals')" class="nav-btn text-sm hover:text-green-600">成交记录</button>
<button onclick="showPanel('analysis')" class="nav-btn text-sm hover:text-green-600">交流分析</button>
<button onclick="showPanel('deal-form')" class="nav-btn text-sm hover:text-green-600">录入成交</button>
</div>
</nav>
<main class="max-w-7xl mx-auto px-4 py-6">
<!-- 仪表盘 -->
<div id="dashboard-panel" class="panel">
<h2 class="text-xl font-bold mb-4">仪表盘</h2>
<div id="dashboard-cards" class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6"></div>
<h3 class="text-lg font-semibold mb-3">各销售数据对比</h3>
<div id="dashboard-sales" class="bg-white rounded-lg shadow p-4 overflow-x-auto">
<table class="w-full text-sm">
<thead><tr class="border-b text-left text-gray-500">
<th class="py-2">销售</th><th>团队</th><th>消息数</th><th>客户数</th><th>成交金额</th><th>最后同步</th>
</tr></thead>
<tbody id="dashboard-sales-tbody"></tbody>
</table>
</div>
</div>
<!-- 销售列表 -->
<div id="salespersons-panel" class="panel hidden">
<h2 class="text-xl font-bold mb-4">销售列表</h2>
<div class="bg-white rounded-lg shadow p-4 overflow-x-auto">
<table class="w-full text-sm">
<thead><tr class="border-b text-left text-gray-500">
<th class="py-2">销售</th><th>团队</th><th>微信号</th><th>联系人</th><th>客户</th><th>消息</th><th>成交</th><th>最后同步</th>
</tr></thead>
<tbody id="salespersons-tbody"></tbody>
</table>
</div>
</div>
<!-- 客户列表 -->
<div id="customers-panel" class="panel hidden">
<h2 class="text-xl font-bold mb-4">客户列表</h2>
<div class="flex gap-4 mb-4">
<select id="filter-salesperson" onchange="loadCustomers()" class="border rounded px-3 py-1.5 text-sm">
<option value="">全部销售</option>
</select>
<select id="filter-intent" onchange="loadCustomers()" class="border rounded px-3 py-1.5 text-sm">
<option value="">全部意向</option>
<option value="high">高意向</option>
<option value="medium">中意向</option>
<option value="low">低意向</option>
</select>
</div>
<div class="bg-white rounded-lg shadow p-4 overflow-x-auto">
<table class="w-full text-sm">
<thead><tr class="border-b text-left text-gray-500">
<th class="py-2">客户</th><th>销售</th><th>行业</th><th>意向</th><th>阶段</th><th>关键需求</th><th>最后沟通</th>
</tr></thead>
<tbody id="customers-tbody"></tbody>
</table>
</div>
</div>
<!-- 客户详情 -->
<div id="customer-detail-panel" class="panel hidden">
<button onclick="showPanel('customers')" class="text-sm text-green-600 mb-3">&larr; 返回客户列表</button>
<div id="detail-info" class="bg-white rounded-lg shadow p-6 mb-4"></div>
<div id="detail-summary" class="bg-white rounded-lg shadow p-6 mb-4"></div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="bg-white rounded-lg shadow p-6">
<h3 class="font-semibold mb-3">聊天记录</h3>
<div id="detail-messages" class="flex flex-col gap-1 max-h-96 overflow-y-auto"></div>
<div id="detail-pagination" class="mt-3 flex items-center gap-3"></div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<h3 class="font-semibold mb-3">成交记录</h3>
<div id="detail-deals"></div>
</div>
</div>
</div>
<!-- 成交记录列表 -->
<div id="deals-panel" class="panel hidden">
<h2 class="text-xl font-bold mb-4">成交记录</h2>
<div class="bg-white rounded-lg shadow p-4 overflow-x-auto">
<table class="w-full text-sm">
<thead><tr class="border-b text-left text-gray-500">
<th class="py-2">销售</th><th>客户</th><th>产品</th><th>金额</th><th>日期</th><th>状态</th>
</tr></thead>
<tbody id="deals-tbody"></tbody>
</table>
</div>
</div>
<!-- 交流分析 -->
<div id="analysis-panel" class="panel hidden">
<h2 class="text-xl font-bold mb-4">交流分析</h2>
<div class="flex gap-1 mb-4">
<button id="analysis-tab-list" onclick="analysisView='list';renderAnalysisView()" class="px-4 py-1.5 rounded text-sm bg-green-600 text-white">对话列表</button>
<button id="analysis-tab-paradigm" onclick="analysisView='paradigm';renderAnalysisView()" class="px-4 py-1.5 rounded text-sm bg-white border hover:bg-gray-50">标准范式</button>
</div>
<!-- 对话列表视图 -->
<div id="analysis-list-view">
<div class="flex gap-3 mb-4">
<select id="analysis-filter-sp" onchange="loadConversations()" class="border rounded px-3 py-1.5 text-sm">
<option value="">全部销售</option>
</select>
<select id="analysis-filter-stage" onchange="loadConversations()" class="border rounded px-3 py-1.5 text-sm">
<option value="">全部阶段</option>
<option value="建立联系">建立联系</option>
<option value="需求发现">需求发现</option>
<option value="报价">报价</option>
<option value="异议处理">异议处理</option>
<option value="成交">成交</option>
</select>
</div>
<div id="analysis-list" class="space-y-3"></div>
</div>
<!-- 标准范式视图 -->
<div id="analysis-paradigm-view" class="hidden">
<div class="bg-white rounded-lg shadow p-4 mb-4">
<p class="text-sm text-gray-500 mb-2">选择一个客户,基于其真实对话生成标准范式对话模板</p>
<div class="flex gap-3 items-center">
<select id="paradigm-customer-select" class="border rounded px-3 py-2 text-sm flex-1">
<option value="">请选择客户</option>
</select>
<button onclick="generateParadigm()" id="paradigm-btn" class="bg-green-600 text-white px-6 py-2 rounded text-sm hover:bg-green-700">生成范式对话</button>
</div>
</div>
<div id="paradigm-result"></div>
</div>
</div>
<!-- 录入成交 -->
<div id="deal-form-panel" class="panel hidden">
<h2 class="text-xl font-bold mb-4">录入成交</h2>
<div class="bg-white rounded-lg shadow p-6 max-w-lg">
<form onsubmit="submitDeal(event)" class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">销售 *</label>
<select id="deal-salesperson" required class="w-full border rounded px-3 py-2 text-sm" onchange="loadDealCustomers()">
<option value="">请选择</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">客户 *</label>
<select id="deal-customer" required class="w-full border rounded px-3 py-2 text-sm">
<option value="">请选择</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">产品名称 *</label>
<input id="deal-product" type="text" required placeholder="如:益童宝3盒套餐" class="w-full border rounded px-3 py-2 text-sm">
</div>
<div>
<label class="block text-sm font-medium mb-1">金额 *</label>
<input id="deal-amount" type="number" step="0.01" required placeholder="798" class="w-full border rounded px-3 py-2 text-sm">
</div>
<div>
<label class="block text-sm font-medium mb-1">成交日期 *</label>
<input id="deal-date" type="date" required class="w-full border rounded px-3 py-2 text-sm">
</div>
<div>
<label class="block text-sm font-medium mb-1">备注</label>
<textarea id="deal-notes" rows="2" class="w-full border rounded px-3 py-2 text-sm"></textarea>
</div>
<button type="submit" class="bg-green-600 text-white px-6 py-2 rounded text-sm hover:bg-green-700">提交</button>
<span id="deal-result" class="ml-4 text-sm text-green-600"></span>
</form>
</div>
</div>
</main>
<script>
const API = '/api';
let currentCustomerId = null;
let currentMsgPage = 1;
function formatYuan(val) {
return '¥' + (val || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
const intentLabels = {high: '高意向', medium: '中意向', low: '低意向'};
const dealStatusLabels = {closed: '已成交', pending: '待确认', cancelled: '已取消'};
// --- 路由 ---
function showPanel(name) {
document.querySelectorAll('.panel').forEach(p => p.classList.add('hidden'));
const panel = document.getElementById(name + '-panel');
if (panel) panel.classList.remove('hidden');
if (name === 'dashboard') loadDashboard();
if (name === 'salespersons') loadSalespersons();
if (name === 'customers') loadCustomers();
if (name === 'deals') loadDeals();
if (name === 'analysis') loadAnalysis();
if (name === 'deal-form') loadDealForm();
}
// --- API 封装 ---
async function api(path, options) {
const res = await fetch(API + path, options);
if (!res.ok) throw new Error(`API ${path}: ${res.status}`);
return res.json();
}
// --- 仪表盘 ---
async function loadDashboard() {
const data = await api('/dashboard');
const cards = document.getElementById('dashboard-cards');
cards.innerHTML = [
{label: '总消息数', value: data.total_messages, color: 'blue'},
{label: '活跃客户', value: data.active_customers, color: 'green'},
{label: '本月成交', value: formatYuan(data.monthly_deal_amount), color: 'orange'},
{label: '销售人数', value: data.salespersons.length, color: 'purple'},
].map(c => `
<div class="bg-white rounded-lg shadow p-4">
<div class="text-2xl font-bold text-${c.color}-600">${c.value}</div>
<div class="text-sm text-gray-500 mt-1">${c.label}</div>
</div>
`).join('');
const tbody = document.getElementById('dashboard-sales-tbody');
tbody.innerHTML = data.salespersons.map(s => `
<tr class="border-b hover:bg-gray-50">
<td class="py-2 font-medium">${s.name}</td>
<td>${s.team}</td>
<td>${s.message_count}</td>
<td>${s.customer_count}</td>
<td>${formatYuan(s.deal_amount)}</td>
<td class="text-gray-400 text-xs">${s.last_synced_at ? new Date(s.last_synced_at).toLocaleString('zh-CN') : '-'}</td>
</tr>
`).join('');
}
// --- 销售列表 ---
async function loadSalespersons() {
const data = await api('/salespersons');
const tbody = document.getElementById('salespersons-tbody');
tbody.innerHTML = data.map(s => `
<tr class="border-b hover:bg-gray-50">
<td class="py-2 font-medium">${s.name}</td>
<td>${s.team}</td>
<td class="text-gray-500">${s.wx_account}</td>
<td>${s.contact_count}</td>
<td>${s.customer_count}</td>
<td>${s.message_count}</td>
<td>${formatYuan(s.deal_amount)}</td>
<td class="text-gray-400 text-xs">${s.last_synced_at ? new Date(s.last_synced_at).toLocaleString('zh-CN') : '-'}</td>
</tr>
`).join('');
}
// --- 客户列表 ---
async function loadCustomers() {
const sp = document.getElementById('filter-salesperson').value;
const intent = document.getElementById('filter-intent').value;
let path = '/customers?';
if (sp) path += `salesperson_id=${sp}&`;
if (intent) path += `intent_level=${intent}&`;
const data = await api(path);
// 填充销售筛选
const spFilter = document.getElementById('filter-salesperson');
if (spFilter.options.length <= 1) {
const spData = await api('/salespersons');
spData.forEach(s => {
const opt = document.createElement('option');
opt.value = s.id; opt.textContent = s.name;
spFilter.appendChild(opt);
});
}
const intentColors = {high: 'red', medium: 'yellow', low: 'gray'};
const tbody = document.getElementById('customers-tbody');
tbody.innerHTML = data.map(c => `
<tr class="border-b hover:bg-gray-50 cursor-pointer" onclick="showCustomerDetail(${c.id})">
<td class="py-2 font-medium text-green-700">${c.customer_name || c.contact_display_name}</td>
<td>${c.salesperson_name}</td>
<td>${c.industry || '-'}</td>
<td><span class="px-2 py-0.5 rounded text-xs bg-${intentColors[c.intent_level] || 'gray'}-100 text-${intentColors[c.intent_level] || 'gray'}-700">${intentLabels[c.intent_level] || c.intent_level || '-'}</span></td>
<td>${c.stage || '-'}</td>
<td class="text-xs text-gray-500">${(c.key_needs || []).join('、')}</td>
<td class="text-gray-400 text-xs">${c.last_message_at ? new Date(c.last_message_at).toLocaleDateString('zh-CN') : '-'}</td>
</tr>
`).join('');
}
// --- 客户详情 ---
async function showCustomerDetail(id) {
currentCustomerId = id;
currentMsgPage = 1;
showPanel('customer-detail');
const customer = await api(`/customers/${id}`);
const info = document.getElementById('detail-info');
info.innerHTML = `
<h3 class="text-lg font-bold mb-3">${customer.customer_name || customer.contact_display_name}</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div><span class="text-gray-500">销售:</span> ${customer.salesperson_name}</div>
<div><span class="text-gray-500">行业:</span> ${customer.industry || '-'}</div>
<div><span class="text-gray-500">意向:</span> <span class="font-medium">${intentLabels[customer.intent_level] || customer.intent_level || '-'}</span></div>
<div><span class="text-gray-500">阶段:</span> <span class="font-medium">${customer.stage || '-'}</span></div>
</div>
<div class="mt-3 text-sm">
<span class="text-gray-500">关键需求:</span> ${(customer.key_needs || []).join('、') || '-'}
</div>
<div class="mt-2 text-sm text-gray-400">识别原因: ${customer.reason || '-'}</div>
`;
const summary = document.getElementById('detail-summary');
summary.innerHTML = `
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold">AI 沟通摘要</h3>
<span class="text-xs text-gray-400">分析时间: ${customer.last_analysis ? new Date(customer.last_analysis).toLocaleString('zh-CN') : '-'}</span>
</div>
<div class="bg-gray-50 border border-gray-100 rounded-lg p-4 mb-4">
<p class="text-sm text-gray-700 leading-relaxed">${customer.summary || '暂无摘要'}</p>
</div>
${(customer.key_points || []).length > 0 ? `
<div class="mb-4">
<h4 class="text-sm font-semibold mb-2 ${customer.stage === '成交' ? 'text-green-700' : 'text-orange-700'}">${customer.stage === '成交' ? '关键成交点' : '需要突破的问题点'}</h4>
<ul class="space-y-1.5">
${(customer.key_points || []).map(p => `
<li class="flex gap-2 items-start text-sm">
<span class="${customer.stage === '成交' ? 'text-green-500' : 'text-orange-500'} shrink-0">▸</span>
<span class="text-gray-700">${p}</span>
</li>
`).join('')}
</ul>
</div>
` : ''}
<div class="space-y-3 text-sm border-t pt-3">
<div class="flex gap-2 items-start">
<span class="text-gray-500 shrink-0 w-14">异议:</span>
<span class="text-gray-700">${(customer.objections || []).join('、') || '无'}</span>
</div>
<div class="flex gap-2 items-start">
<span class="text-gray-500 shrink-0 w-14">下一步:</span>
<span class="text-gray-700">${customer.next_action || '-'}</span>
</div>
</div>
`;
await loadCustomerMessages();
await loadCustomerDeals();
}
async function loadCustomerMessages() {
const data = await api(`/customers/${currentCustomerId}/messages?page=${currentMsgPage}&page_size=50`);
const container = document.getElementById('detail-messages');
container.innerHTML = data.messages.map(m => {
const isSales = m.sender_display_name && ['张伟','李娜','王强'].includes(m.sender_display_name);
const cls = isSales ? 'chat-sales' : 'chat-customer';
const content = m.message_type === 'text' ? m.normalized_content : m.normalized_content || `[${m.message_type}]`;
return `
<div class="flex flex-col">
<span class="text-xs text-gray-400 mb-0.5">${m.sender_display_name || '未知'} · ${m.created_at ? new Date(m.created_at).toLocaleString('zh-CN') : ''}</span>
<div class="chat-bubble ${cls} text-sm">${escapeHtml(content)}</div>
</div>
`;
}).join('');
const pagination = document.getElementById('detail-pagination');
const totalPages = Math.ceil(data.total / 50);
pagination.innerHTML = `
<span class="text-sm text-gray-500">${data.total} 条消息,第 ${currentMsgPage}/${totalPages} 页</span>
${currentMsgPage > 1 ? `<button onclick="prevPage()" class="text-sm text-green-600">上一页</button>` : ''}
${currentMsgPage < totalPages ? `<button onclick="nextPage()" class="text-sm text-green-600">下一页</button>` : ''}
`;
}
function prevPage() { currentMsgPage--; loadCustomerMessages(); }
function nextPage() { currentMsgPage++; loadCustomerMessages(); }
async function loadCustomerDeals() {
const data = await api(`/customers/${currentCustomerId}/deals`);
const container = document.getElementById('detail-deals');
if (data.length === 0) {
container.innerHTML = '<p class="text-sm text-gray-400">暂无成交记录</p>';
return;
}
container.innerHTML = data.map(d => `
<div class="border-b py-2 text-sm">
<div class="flex justify-between">
<span class="font-medium">${d.product_name}</span>
<span class="font-bold text-green-600">${formatYuan(d.amount)}</span>
</div>
<div class="text-xs text-gray-400 mt-1">${d.deal_date} · ${dealStatusLabels[d.status] || d.status} · ${d.salesperson_name}</div>
${d.notes ? `<div class="text-xs text-gray-500 mt-1">${d.notes}</div>` : ''}
</div>
`).join('');
}
// --- 成交记录列表 ---
async function loadDeals() {
const data = await api('/deals');
const tbody = document.getElementById('deals-tbody');
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="py-4 text-center text-gray-400">暂无成交记录</td></tr>';
return;
}
tbody.innerHTML = data.map(d => `
<tr class="border-b hover:bg-gray-50">
<td class="py-2">${d.salesperson_name}</td>
<td>${d.customer_name || '-'}</td>
<td>${d.product_name}</td>
<td class="font-bold text-green-600">${formatYuan(d.amount)}</td>
<td>${d.deal_date}</td>
<td><span class="px-2 py-0.5 rounded text-xs ${d.status==='closed'?'bg-green-100 text-green-700':'bg-yellow-100 text-yellow-700'}">${dealStatusLabels[d.status] || d.status}</span></td>
</tr>
`).join('');
}
// --- 录入成交 ---
async function loadDealForm() {
const spSelect = document.getElementById('deal-salesperson');
spSelect.innerHTML = '<option value="">请选择</option>';
const spData = await api('/salespersons');
spData.forEach(s => {
const opt = document.createElement('option');
opt.value = s.id; opt.textContent = s.name;
spSelect.appendChild(opt);
});
document.getElementById('deal-date').value = new Date().toISOString().slice(0, 10);
document.getElementById('deal-result').textContent = '';
}
async function loadDealCustomers() {
const spId = document.getElementById('deal-salesperson').value;
const custSelect = document.getElementById('deal-customer');
custSelect.innerHTML = '<option value="">请选择</option>';
if (!spId) return;
const data = await api(`/customers?salesperson_id=${spId}`);
data.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.customer_name || c.contact_display_name;
custSelect.appendChild(opt);
});
}
async function submitDeal(event) {
event.preventDefault();
const body = {
salesperson_id: parseInt(document.getElementById('deal-salesperson').value),
customer_id: parseInt(document.getElementById('deal-customer').value),
product_name: document.getElementById('deal-product').value,
amount: parseFloat(document.getElementById('deal-amount').value),
deal_date: document.getElementById('deal-date').value,
notes: document.getElementById('deal-notes').value || null,
};
try {
const result = await api('/deals', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body),
});
document.getElementById('deal-result').textContent = '✓ 已创建,ID: ' + result.id;
document.getElementById('deal-product').value = '';
document.getElementById('deal-amount').value = '';
document.getElementById('deal-notes').value = '';
} catch (e) {
document.getElementById('deal-result').textContent = '✗ 失败: ' + e.message;
}
}
// --- 交流分析 ---
let analysisView = 'list';
let conversationsData = [];
async function loadAnalysis() {
analysisView = 'list';
renderAnalysisView();
await loadConversations();
}
function renderAnalysisView() {
const listTab = document.getElementById('analysis-tab-list');
const paradigmTab = document.getElementById('analysis-tab-paradigm');
const listView = document.getElementById('analysis-list-view');
const paradigmView = document.getElementById('analysis-paradigm-view');
if (analysisView === 'list') {
listTab.className = 'px-4 py-1.5 rounded text-sm bg-green-600 text-white';
paradigmTab.className = 'px-4 py-1.5 rounded text-sm bg-white border hover:bg-gray-50';
listView.classList.remove('hidden');
paradigmView.classList.add('hidden');
} else {
listTab.className = 'px-4 py-1.5 rounded text-sm bg-white border hover:bg-gray-50';
paradigmTab.className = 'px-4 py-1.5 rounded text-sm bg-green-600 text-white';
listView.classList.add('hidden');
paradigmView.classList.remove('hidden');
populateParadigmSelect();
}
}
async function loadConversations() {
const sp = document.getElementById('analysis-filter-sp').value;
const stage = document.getElementById('analysis-filter-stage').value;
let path = '/conversations?';
if (sp) path += `salesperson_id=${sp}&`;
if (stage) path += `stage=${encodeURIComponent(stage)}&`;
try {
conversationsData = await api(path);
renderAnalysisList();
populateParadigmSelect();
} catch (e) {
document.getElementById('analysis-list').innerHTML = `<p class="text-sm text-red-500">加载失败: ${e.message}</p>`;
}
// 填充销售筛选
const spSelect = document.getElementById('analysis-filter-sp');
if (spSelect.options.length <= 1) {
const sps = await api('/salespersons');
sps.forEach(s => {
const opt = document.createElement('option');
opt.value = s.id;
opt.textContent = s.name;
spSelect.appendChild(opt);
});
}
}
const STAGE_COLORS = {
'成交': 'bg-green-100 text-green-700',
'异议处理': 'bg-orange-100 text-orange-700',
'报价': 'bg-blue-100 text-blue-700',
'需求发现': 'bg-purple-100 text-purple-700',
'建立联系': 'bg-gray-100 text-gray-700',
};
function renderAnalysisList() {
const container = document.getElementById('analysis-list');
if (conversationsData.length === 0) {
container.innerHTML = '<p class="text-sm text-gray-400">暂无交流分析数据</p>';
return;
}
container.innerHTML = conversationsData.map(c => `
<div class="bg-white rounded-lg shadow p-4">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-3">
<span class="font-medium text-sm">${c.customer_name}</span>
<span class="text-xs text-gray-400">${c.salesperson_name}</span>
<span class="text-xs text-gray-400">${c.msg_count} 条消息</span>
</div>
<span class="px-2 py-0.5 rounded text-xs ${STAGE_COLORS[c.stage] || STAGE_COLORS['建立联系']}">${c.stage || '-'}</span>
</div>
<p class="text-sm text-gray-600 mb-2">${c.summary || '暂无摘要'}</p>
${(c.key_points || []).length > 0 ? `
<div class="mb-2">
<span class="text-xs font-semibold ${c.stage === '成交' ? 'text-green-700' : 'text-orange-700'}">${c.stage === '成交' ? '关键成交点' : '需要突破的问题点'}</span>
<ul class="mt-1 space-y-0.5">
${(c.key_points || []).map(p => `<li class="text-xs text-gray-600 flex gap-1"><span class="${c.stage === '成交' ? 'text-green-500' : 'text-orange-500'}">▸</span><span>${escapeHtml(p)}</span></li>`).join('')}
</ul>
</div>
` : ''}
<div class="flex gap-4 text-xs text-gray-500 border-t pt-2 mt-2">
<span>异议: ${(c.objections || []).join('、') || '无'}</span>
<span>下一步: ${c.next_action || '-'}</span>
</div>
</div>
`).join('');
}
function populateParadigmSelect() {
const sel = document.getElementById('paradigm-customer-select');
if (!sel) return;
const currentVal = sel.value;
sel.innerHTML = '<option value="">请选择客户</option>';
conversationsData.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = `${c.customer_name} (${c.salesperson_name}, ${c.stage})`;
sel.appendChild(opt);
});
if (currentVal) sel.value = currentVal;
}
async function generateParadigm() {
const customerId = document.getElementById('paradigm-customer-select').value;
if (!customerId) {
document.getElementById('paradigm-result').innerHTML = '<p class="text-sm text-orange-500">请先选择客户</p>';
return;
}
const btn = document.getElementById('paradigm-btn');
const result = document.getElementById('paradigm-result');
btn.disabled = true;
btn.textContent = '生成中...';
result.innerHTML = '<p class="text-sm text-gray-400">正在调用千问 LLM 生成标准范式对话,请稍候...</p>';
try {
const data = await api(`/conversations/${customerId}/paradigm`, { method: 'POST' });
renderParadigm(data);
} catch (e) {
result.innerHTML = `<p class="text-sm text-red-500">生成失败: ${e.message}</p>`;
} finally {
btn.disabled = false;
btn.textContent = '生成范式对话';
}
}
function renderParadigm(data) {
const result = document.getElementById('paradigm-result');
const stageColors = {
'成交': 'border-green-400 bg-green-50',
'异议处理': 'border-orange-400 bg-orange-50',
'报价': 'border-blue-400 bg-blue-50',
'需求发现': 'border-purple-400 bg-purple-50',
'建立联系': 'border-gray-300 bg-gray-50',
};
result.innerHTML = `
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-bold mb-1">标准范式对话模板</h3>
<p class="text-sm text-gray-500 mb-4">客户类型: ${data.customer_type || '-'}</p>
<div class="space-y-4 mb-6">
${(data.stages || []).map((s, i) => `
<div class="border-l-4 ${stageColors[s.stage] || stageColors['建立联系']} pl-4 py-3">
<div class="flex items-center gap-2 mb-2">
<span class="bg-gray-700 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs font-bold">${i + 1}</span>
<span class="font-semibold text-sm">${s.stage}</span>
</div>
<p class="text-xs text-gray-500 mb-2">目标: ${s.goal || '-'}</p>
<div class="mb-2">
<span class="text-xs font-medium text-green-700">销售话术:</span>
<p class="text-sm text-gray-700 mt-1 bg-green-50 rounded p-2">${escapeHtml(s.sales_script || '-')}</p>
</div>
<div class="mb-2">
<span class="text-xs font-medium text-gray-600">预期回应:</span>
<p class="text-sm text-gray-600 mt-1">${escapeHtml(s.expected_response || '-')}</p>
</div>
<div>
<span class="text-xs font-medium text-blue-600">技巧提示:</span>
<p class="text-sm text-gray-600 mt-1">${escapeHtml(s.tips || '-')}</p>
</div>
</div>
`).join('')}
</div>
${(data.key_techniques || []).length > 0 ? `
<div class="mb-4">
<h4 class="text-sm font-semibold mb-2 text-green-700">核心销售技巧</h4>
<ul class="space-y-1">
${(data.key_techniques || []).map(t => `<li class="text-sm text-gray-700 flex gap-2"><span class="text-green-500">✓</span><span>${escapeHtml(t)}</span></li>`).join('')}
</ul>
</div>
` : ''}
${(data.improvement_points || []).length > 0 ? `
<div class="mb-4">
<h4 class="text-sm font-semibold mb-2 text-orange-700">可改进的点</h4>
<ul class="space-y-1">
${(data.improvement_points || []).map(t => `<li class="text-sm text-gray-700 flex gap-2"><span class="text-orange-500">!</span><span>${escapeHtml(t)}</span></li>`).join('')}
</ul>
</div>
` : ''}
</div>
`;
}
// --- 工具 ---
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s || '';
return div.innerHTML;
}
// --- 初始化 ---
showPanel('dashboard');
</script>
</body>
</html>