diff --git a/demo/web/server.py b/demo/web/server.py
index 0969b8c..fb360e9 100644
--- a/demo/web/server.py
+++ b/demo/web/server.py
@@ -243,21 +243,25 @@ def list_salespersons():
def list_customers(
salesperson_id: int | None = Query(None),
intent_level: str | None = Query(None),
+ product_id: int | None = Query(None),
):
conn = get_conn()
try:
with conn.cursor() as cur:
sql = """
- SELECT cu.id, cu.salesperson_id, s.name AS salesperson_name,
+ SELECT DISTINCT cu.id, cu.salesperson_id, s.name AS salesperson_name,
cu.customer_name, cu.industry, cu.intent_level,
cu.key_needs, cu.stage, cu.last_analysis,
c.display_name AS contact_display_name,
- max(m.created_at) AS last_message_at
+ max(m.created_at) AS last_message_at,
+ string_agg(DISTINCT p.category, ', ') AS product_categories
FROM customer cu
JOIN salesperson s ON s.id = cu.salesperson_id
JOIN contact c ON c.id = cu.contact_id
LEFT JOIN conversation conv ON conv.contact_id = c.id
LEFT JOIN message m ON m.conversation_id = conv.id
+ LEFT JOIN deal d ON d.customer_id = cu.id
+ LEFT JOIN product p ON p.id = d.product_id
"""
conditions = []
params = []
@@ -267,6 +271,9 @@ def list_customers(
if intent_level is not None:
conditions.append("cu.intent_level = %s")
params.append(intent_level)
+ if product_id is not None:
+ conditions.append("d.product_id = %s")
+ params.append(product_id)
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += """
@@ -286,6 +293,7 @@ def list_customers(
"last_analysis": r[8].isoformat() if r[8] else None,
"contact_display_name": r[9],
"last_message_at": r[10].isoformat() if r[10] else None,
+ "product_categories": r[11],
}
for r in cur.fetchall()
]
diff --git a/demo/web/static/index.html b/demo/web/static/index.html
index 5d8a9fd..7146930 100644
--- a/demo/web/static/index.html
+++ b/demo/web/static/index.html
@@ -156,15 +156,11 @@ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-
+
-
-
-
- | 客户 | 销售 | 行业 | 意向 | 阶段 | 关键需求 | 最后沟通 |
-
-
-
-
+
@@ -539,6 +535,7 @@ async function loadSalespersons() {
async function loadCustomers() {
const sp = document.getElementById('filter-salesperson').value;
const intent = document.getElementById('filter-intent').value;
+ const cat = document.getElementById('filter-product-category').value;
let path = '/customers?';
if (sp) path += `salesperson_id=${sp}&`;
if (intent) path += `intent_level=${intent}&`;
@@ -556,19 +553,70 @@ async function loadCustomers() {
});
}
+ // 填充产品线筛选
+ const catFilter = document.getElementById('filter-product-category');
+ if (catFilter.options.length <= 1) {
+ const prodData = await api('/products/categories');
+ prodData.forEach(c => {
+ const opt = document.createElement('option');
+ opt.value = c; opt.textContent = c;
+ catFilter.appendChild(opt);
+ });
+ }
+
+ // 按产品线筛选
+ const filtered = cat ? data.filter(c => (c.product_categories || '').includes(cat)) : data;
+
+ // 按产品线分组:有 product_categories 的按类别分,没有的归入"未关联产品"
+ const groups = {};
+ filtered.forEach(c => {
+ const cats = (c.product_categories || '').split(', ').filter(x => x);
+ if (cats.length === 0) cats.push('未关联产品');
+ cats.forEach(g => {
+ if (!groups[g]) groups[g] = [];
+ if (!groups[g].find(x => x.id === c.id)) groups[g].push(c);
+ });
+ });
+
const intentColors = {high: 'red', medium: 'yellow', low: 'gray'};
- const tbody = document.getElementById('customers-tbody');
- tbody.innerHTML = data.map(c => `
-
- | ${c.customer_name || c.contact_display_name} |
- ${c.salesperson_name} |
- ${c.industry || '-'} |
- ${intentLabels[c.intent_level] || c.intent_level || '-'} |
- ${c.stage || '-'} |
- ${(c.key_needs || []).join('、')} |
- ${c.last_message_at ? new Date(c.last_message_at).toLocaleDateString('zh-CN') : '-'} |
-
- `).join('');
+ const container = document.getElementById('customers-grouped');
+ const groupNames = Object.keys(groups).sort((a, b) => a === '未关联产品' ? 1 : -1);
+
+ if (groupNames.length === 0) {
+ container.innerHTML = '暂无客户数据
';
+ return;
+ }
+
+ container.innerHTML = groupNames.map(gname => {
+ const customers = groups[gname];
+ const headerColor = gname === '未关联产品' ? 'gray' : 'indigo';
+ return `
+
+
+
+
+ | 客户 | 销售 | 行业 | 意向 | 阶段 | 关键需求 | 最后沟通 |
+
+
+ ${customers.map(c => `
+
+ | ${c.customer_name || c.contact_display_name} |
+ ${c.salesperson_name} |
+ ${c.industry || '-'} |
+ ${intentLabels[c.intent_level] || c.intent_level || '-'} |
+ ${c.stage || '-'} |
+ ${(c.key_needs || []).join('、')} |
+ ${c.last_message_at ? new Date(c.last_message_at).toLocaleDateString('zh-CN') : '-'} |
+
+ `).join('')}
+
+
+
+ `;
+ }).join('');
}
// --- 客户详情 ---