5926c73ff1
- 数据库: PostgreSQL schema + 3名销售/21客户/341消息/6成交 - 采集代理: mock_sync.py 模拟微信聊天同步 - AI分析: analyze.py 规则模式 + 千问LLM模式 - 后端: FastAPI 11个API接口 - 前端: 仪表盘/销售列表/客户列表/成交记录/交流分析/录入成交 - 交流分析: 全部客户对话概览 + LLM标准范式对话生成 - 部署: systemd + nginx, 已部署至 sale.all8ai.top
694 lines
29 KiB
HTML
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">← 返回客户列表</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>
|