# -*- coding: utf-8 -*- new_customer_ops = ''' async function renderCustomerOpsPage() { els.pageTitle.textContent = '客户运营看板'; const page = document.createElement('div'); page.className = 'page'; page.innerHTML = `

好友状态分布全部客户

近7日运营趋势加好友 / 建群

客户列表

支持按好友状态、画像、标签筛选
`; els.content.appendChild(page); await loadCustomerOpsDashboard(page); dashboardRefreshTimer = setInterval(() => { if (state.page === 'customer-ops') loadCustomerOpsDashboard(page, true); }, 30000); page.querySelector('#btn-co-refresh').addEventListener('click', () => loadCustomerOpsDashboard(page)); page.querySelector('#btn-co-rebuild').addEventListener('click', async () => { try { await api('POST', '/api/dashboard/rebuild'); toast('索引已重建'); loadCustomerOpsDashboard(page); } catch (err) { toast(err.message || '重建失败', 'error'); } }); page.querySelector('#btn-import-customers').addEventListener('click', () => openImportCustomersModal(page)); page.querySelector('#co-filter-keyword').addEventListener('input', () => loadCustomerOpsTable(page)); page.querySelector('#co-filter-status').addEventListener('change', () => loadCustomerOpsTable(page)); page.querySelector('#co-filter-portrait').addEventListener('change', () => loadCustomerOpsTable(page)); page.querySelector('#co-filter-tag').addEventListener('change', () => loadCustomerOpsTable(page)); page.querySelector('#btn-batch-add').addEventListener('click', () => { const selected = getSelectedCustomers(page); if (!selected.length) return; const body = { customers: selected.map(c => ({ phone: c.phone, name: c.name })), guid: getGuid() }; startJob('batch-add-friends', '/api/customer-ops/batch-add-friends', body, () => loadCustomerOpsDashboard(page)); }); page.querySelector('#btn-check-friends').addEventListener('click', () => { const selected = getSelectedCustomers(page); if (!selected.length) return; const body = { phones: selected.map(c => c.phone).filter(Boolean), guid: getGuid() }; startJob('check-friend-status', '/api/customer-ops/check-friend-status', body, () => loadCustomerOpsDashboard(page)); }); page.querySelector('#btn-auto-group').addEventListener('click', () => { const selected = getSelectedCustomers(page).filter(c => c.friendRequestStatus === 'ACCEPTED' && c.groupStatus !== 'CREATED'); if (!selected.length) { toast('请选中已是好友且未建群的客户', 'warning'); return; } const body = { memberList: selected.map(c => c.externalUserId).filter(Boolean), guid: getGuid() }; startJob('auto-create-group', '/api/customer-ops/auto-create-group', body, () => loadCustomerOpsDashboard(page)); }); } async function loadCustomerOpsDashboard(page, silent = false) { if (!silent) page.querySelector('#co-customer-table-wrap').innerHTML = '
'; try { const [summaryRes, customersRes, tagsRes] = await Promise.all([ api('GET', '/api/dashboard/summary'), api('GET', '/api/dashboard/customers'), api('GET', '/api/dashboard/tags') ]); const summary = summaryRes.data || {}; const customers = Object.values(customersRes.data?.customers || {}); const tags = tagsRes.data?.allTags || []; renderCustomerOpsKpi(page, summary); renderCustomerOpsCharts(page, summary, customers); renderCustomerOpsTagFilter(page, tags); state.customerOps.allCustomers = customers; loadCustomerOpsTable(page); } catch (err) { if (!silent) toast(err.message || '加载看板失败', 'error'); } } function renderCustomerOpsKpi(page, summary) { const wrap = page.querySelector('#co-kpi-row'); const c = summary.customers || {}; const friendStatus = c.friendStatusCounts || {}; const kpi = [ { label: '本地客户总数', value: formatNumber(c.total) }, { label: '已是好友', value: formatNumber(friendStatus.ACCEPTED || 0), sub: '已通过好友申请' }, { label: '待通过', value: formatNumber(friendStatus.PENDING || 0), sub: '等待对方确认' }, { label: '自动建群', value: formatNumber(c.autoCreatedGroups || 0), sub: '已创建客户群' }, { label: '画像覆盖率', value: (c.portraitCoverage || 0) + '%', sub: '已生成画像客户占比' }, { label: '今日新增', value: formatNumber(friendStatus.ACCEPTED || 0), sub: '今日通过好友' } ]; wrap.innerHTML = renderKpiRow(kpi); } function renderCustomerOpsCharts(page, summary, customers) { const friendStatus = summary.customers?.friendStatusCounts || {}; initEChart('co-friend-chart', { tooltip: { trigger: 'item' }, legend: { bottom: 0 }, series: [{ type: 'pie', radius: ['45%', '70%'], center: ['50%', '45%'], data: [ { name: '已是好友', value: friendStatus.ACCEPTED || 0, itemStyle: { color: '#52c41a' } }, { name: '待通过', value: friendStatus.PENDING || 0, itemStyle: { color: '#faad14' } }, { name: '未找到', value: friendStatus.NOT_FOUND || 0, itemStyle: { color: '#ff4d4f' } }, { name: '失败', value: friendStatus.FAILED || 0, itemStyle: { color: '#722ed1' } }, { name: '未知', value: friendStatus.UNKNOWN || 0, itemStyle: { color: '#8c8c8c' } } ] }] }); const days = last7Days(); const addCounts = countByDay(customers.filter(c => c.lastAddAttemptAt), 'lastAddAttemptAt'); const groupCounts = countByDay(customers.filter(c => c.autoCreatedAt), 'autoCreatedAt'); initEChart('co-trend-chart', { tooltip: { trigger: 'axis' }, legend: { bottom: 0 }, xAxis: { type: 'category', data: days.map(d => formatDate(d)) }, yAxis: { type: 'value' }, series: [ { name: '加好友', type: 'bar', data: addCounts, itemStyle: { color: '#fa8c16' } }, { name: '建群', type: 'line', data: groupCounts, itemStyle: { color: '#1890ff' } } ] }); } function renderCustomerOpsTagFilter(page, tags) { const select = page.querySelector('#co-filter-tag'); const current = select.value; select.innerHTML = '' + tags.map(t => ``).join(''); select.value = current; } function getSelectedCustomers(page) { const checkboxes = page.querySelectorAll('#co-customer-table-wrap input[type="checkbox"]:checked'); return Array.from(checkboxes).map(cb => { const idx = Number(cb.dataset.index); return state.customerOps.filteredCustomers?.[idx]; }).filter(Boolean); } function loadCustomerOpsTable(page) { const wrap = page.querySelector('#co-customer-table-wrap'); const customers = state.customerOps.allCustomers || []; const keyword = page.querySelector('#co-filter-keyword').value.trim().toLowerCase(); const status = page.querySelector('#co-filter-status').value; const portrait = page.querySelector('#co-filter-portrait').value; const tag = page.querySelector('#co-filter-tag').value; let filtered = customers.filter(c => { if (keyword) { const text = `${c.phone || ''} ${c.name || ''} ${c.externalUserId || ''}`.toLowerCase(); if (!text.includes(keyword)) return false; } if (status && c.friendRequestStatus !== status) return false; if (portrait === 'true' && !c.hasPortrait) return false; if (portrait === 'false' && c.hasPortrait) return false; if (tag && !(Array.isArray(c.tags) && c.tags.includes(tag))) return false; return true; }); state.customerOps.filteredCustomers = filtered; const pageSize = 20; const currentPage = state.customerOps.tablePage || 1; const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); if (currentPage > totalPages) state.customerOps.tablePage = totalPages; const start = (state.customerOps.tablePage - 1) * pageSize; const pageItems = filtered.slice(start, start + pageSize); const friendBadge = (status) => { const map = { ACCEPTED: ['badge-success', '已是好友'], PENDING: ['badge-warning', '待通过'], NOT_FOUND: ['badge-error', '未找到'], FAILED: ['badge-error', '失败'] }; const [cls, text] = map[status] || ['badge-default', '未知']; return `${escapeHtml(text)}`; }; wrap.innerHTML = `
${pageItems.length ? pageItems.map((c, i) => ` `).join('') : ''}
手机号 姓名 externalUserId 好友状态 建群状态 画像 标签 最后操作 操作
${escapeHtml(c.phone || '-')} ${escapeHtml(c.name || '-')} ${escapeHtml(c.externalUserId || '-')} ${friendBadge(c.friendRequestStatus)} ${c.groupStatus === 'CREATED' ? '已建群' : '未建群'} ${c.hasPortrait ? '已生成' : '未生成'} ${Array.isArray(c.tags) && c.tags.length ? c.tags.map(t => `${escapeHtml(t)}`).join('') : '-'} ${formatDateTime(c.updatedAt)}
暂无客户数据
`; const pagination = page.querySelector('#co-pagination'); pagination.innerHTML = renderPagination(state.customerOps.tablePage, filtered.length, pageSize); pagination.querySelectorAll('button').forEach(btn => { btn.addEventListener('click', () => { state.customerOps.tablePage = Number(btn.dataset.page); loadCustomerOpsTable(page); }); }); const selectAll = wrap.querySelector('#co-select-all'); const rowCheckboxes = wrap.querySelectorAll('tbody input[type="checkbox"]'); selectAll?.addEventListener('change', () => { rowCheckboxes.forEach(cb => cb.checked = selectAll.checked); updateCustomerOpsActionButtons(page); }); rowCheckboxes.forEach(cb => cb.addEventListener('change', () => updateCustomerOpsActionButtons(page))); updateCustomerOpsActionButtons(page); } function updateCustomerOpsActionButtons(page) { const selected = getSelectedCustomers(page); page.querySelector('#btn-batch-add').disabled = !selected.length; page.querySelector('#btn-check-friends').disabled = !selected.length; page.querySelector('#btn-auto-group').disabled = !selected.some(c => c.friendRequestStatus === 'ACCEPTED' && c.groupStatus !== 'CREATED'); } function openImportCustomersModal(page) { const body = `
`; openModal('导入客户', body, ` `); document.getElementById('btn-confirm-import').addEventListener('click', async () => { const text = document.getElementById('import-customer-text').value; const file = document.getElementById('import-customer-file').files[0]; const greeting = document.getElementById('import-greeting').value; let customers = parseCustomerText(text); if (file) { const base64 = await readFileBase64(file); const upload = await api('POST', '/api/upload', { data: base64, name: file.name }); const filePath = upload.data?.path; if (filePath) { const res = await api('POST', '/api/customer-ops/batch-add-friends', { filePath, defaultGreeting: greeting, guid: getGuid() }); toast(res.assistantMessage || '导入任务已启动'); closeModal(); loadCustomerOpsDashboard(page); return; } } if (!customers.length) { toast('请输入有效客户', 'warning'); return; } closeModal(); startJob('batch-add-friends', '/api/customer-ops/batch-add-friends', { customers, defaultGreeting: greeting, guid: getGuid() }, () => loadCustomerOpsDashboard(page)); }); } window.dashboardCustomerDetail = async (externalUserId) => { if (!externalUserId) return; try { const res = await api('POST', '/api/customer-ops/customer-profile', { externalUserId, guid: getGuid() }); const data = res.data || {}; const portrait = data.portrait; openModal('客户详情', `
externalUserId:${escapeHtml(externalUserId)}
匹配联系人:${formatNumber(data.contactList?.length || 0)}
相关群:${formatNumber(data.rooms?.length || 0)}
${escapeHtml(JSON.stringify(portrait || data.contact || {}, null, 2))}
`, ``); } catch (err) { toast(err.message || '查询失败', 'error'); } }; ''' with open('d:/caidawork/openclaw-voc-skill/claude-code/claude-code-qiwe-assistant/mcp/src/dashboard/app.js', 'r', encoding='utf-8') as f: lines = f.readlines() start = 1425 end = 1865 new_lines = lines[:start] + [new_customer_ops + '\n'] + lines[end:] with open('d:/caidawork/openclaw-voc-skill/claude-code/claude-code-qiwe-assistant/mcp/src/dashboard/app.js', 'w', encoding='utf-8') as f: f.writelines(new_lines) print('renderCustomerOpsPage replaced successfully')