| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- # -*- coding: utf-8 -*-
- new_transfers = ''' async function renderTransfersPage() {
- els.pageTitle.textContent = '客户交接看板';
- const page = document.createElement('div');
- page.className = 'page';
- page.innerHTML = `
- <div class="dashboard-grid" id="tf-dashboard">
- <div class="kpi-row" id="tf-kpi-row"></div>
- <div class="chart-card">
- <h3 class="chart-title"><span>交接漏斗</span></h3>
- <div class="chart-container" id="tf-funnel-chart"></div>
- </div>
- <div class="chart-card">
- <h3 class="chart-title"><span>交接结果分布</span></h3>
- <div class="chart-container" id="tf-result-chart"></div>
- </div>
- <div class="chart-card full">
- <h3 class="chart-title"><span>近7日交接执行趋势</span></h3>
- <div class="chart-container" id="tf-trend-chart"></div>
- </div>
- <div class="operation-toolbar">
- <div class="toolbar-left">
- <button class="btn" id="btn-new-transfer">新建交接预览</button>
- </div>
- <div class="toolbar-right">
- <button class="btn btn-sm btn-secondary" id="btn-tf-refresh">刷新</button>
- <button class="btn btn-sm btn-secondary" id="btn-tf-rebuild">重建索引</button>
- </div>
- </div>
- <div class="data-table-card">
- <div class="card-header">
- <div>
- <h3 class="card-title">交接包列表</h3>
- <div class="card-subtitle">预览包与执行记录</div>
- </div>
- </div>
- <div class="filter-bar" style="margin-bottom:16px">
- <select id="tf-filter-status">
- <option value="">全部状态</option>
- <option value="DRAFT">草稿</option>
- <option value="EXECUTED">已执行</option>
- </select>
- <input id="tf-filter-user" type="text" placeholder="按顾问 userId 搜索" style="width:200px" />
- </div>
- <div id="tf-transfer-table-wrap"></div>
- <div id="tf-pagination"></div>
- </div>
- </div>
- `;
- els.content.appendChild(page);
- await loadTransfersDashboard(page);
- dashboardRefreshTimer = setInterval(() => {
- if (state.page === 'transfers') loadTransfersDashboard(page, true);
- }, 30000);
- page.querySelector('#btn-tf-refresh').addEventListener('click', () => loadTransfersDashboard(page));
- page.querySelector('#btn-tf-rebuild').addEventListener('click', async () => {
- try {
- await api('POST', '/api/dashboard/rebuild');
- toast('索引已重建');
- loadTransfersDashboard(page);
- } catch (err) {
- toast(err.message || '重建失败', 'error');
- }
- });
- page.querySelector('#btn-new-transfer').addEventListener('click', () => openTransferModal(page));
- page.querySelector('#tf-filter-status').addEventListener('change', () => loadTransfersTable(page));
- page.querySelector('#tf-filter-user').addEventListener('input', () => loadTransfersTable(page));
- }
- async function loadTransfersDashboard(page, silent = false) {
- if (!silent) page.querySelector('#tf-transfer-table-wrap').innerHTML = '<div class="skeleton" style="height:200px"></div>';
- try {
- const [summaryRes, transfersRes] = await Promise.all([
- api('GET', '/api/dashboard/summary'),
- api('GET', '/api/dashboard/transfers')
- ]);
- const summary = summaryRes.data || {};
- const transfers = transfersRes.data || {};
- renderTransfersKpi(page, summary);
- renderTransfersCharts(page, summary, transfers);
- state.transfers = { ...state.transfers, ...transfers };
- loadTransfersTable(page);
- } catch (err) {
- if (!silent) toast(err.message || '加载看板失败', 'error');
- }
- }
- function renderTransfersKpi(page, summary) {
- const wrap = page.querySelector('#tf-kpi-row');
- const t = summary.transfers || {};
- const kpi = [
- { label: '交接预览包', value: formatNumber(t.previews) },
- { label: '已执行交接', value: formatNumber(t.executions) },
- { label: '交接成功率', value: (t.successRate || 0) + '%', sub: '成功记录 / 总记录' },
- { label: '近7日交接', value: formatNumber(t.recent7d), sub: '最近7天执行数' }
- ];
- wrap.innerHTML = renderKpiRow(kpi);
- }
- function renderTransfersCharts(page, summary, transfers) {
- const t = summary.transfers || {};
- initEChart('tf-funnel-chart', {
- tooltip: { trigger: 'item', formatter: '{b}: {c}' },
- series: [{
- type: 'funnel',
- left: '10%',
- width: '80%',
- label: { show: true, position: 'inside' },
- data: [
- { name: '预览包', value: t.previews || 0, itemStyle: { color: '#fa8c16' } },
- { name: '已执行', value: t.executions || 0, itemStyle: { color: '#1890ff' } },
- { name: '成功', value: Math.round(((t.successRate || 0) / 100) * (t.executions || 0)), itemStyle: { color: '#52c41a' } }
- ]
- }]
- });
- const executions = transfers.executions || [];
- const success = executions.reduce((sum, e) => sum + (e.success || 0), 0);
- const failed = executions.reduce((sum, e) => sum + (e.failed || 0), 0);
- initEChart('tf-result-chart', {
- tooltip: { trigger: 'item' },
- legend: { bottom: 0 },
- series: [{
- type: 'pie',
- radius: ['40%', '65%'],
- center: ['50%', '45%'],
- data: [
- { name: '成功', value: success, itemStyle: { color: '#52c41a' } },
- { name: '失败', value: failed, itemStyle: { color: '#ff4d4f' } }
- ]
- }]
- });
- const days = last7Days();
- const counts = countByDay(executions, 'executedAt');
- initEChart('tf-trend-chart', {
- tooltip: { trigger: 'axis' },
- xAxis: { type: 'category', data: days.map(d => formatDate(d)) },
- yAxis: { type: 'value' },
- series: [{ name: '交接执行', type: 'bar', data: counts, itemStyle: { color: '#fa8c16' } }]
- });
- }
- function loadTransfersTable(page) {
- const wrap = page.querySelector('#tf-transfer-table-wrap');
- const transfers = state.transfers || {};
- const statusFilter = page.querySelector('#tf-filter-status').value;
- const userFilter = page.querySelector('#tf-filter-user').value.trim().toLowerCase();
- let items = [
- ...(transfers.previews || []).map(p => ({ ...p, type: 'preview' })),
- ...(transfers.executions || []).map(e => ({ ...e, type: 'execution' }))
- ];
- let filtered = items.filter(item => {
- if (statusFilter && item.status !== statusFilter) return false;
- if (userFilter) {
- const text = `${item.fromUserId || ''} ${item.toUserId || ''}`.toLowerCase();
- if (!text.includes(userFilter)) return false;
- }
- return true;
- });
- filtered.sort((a, b) => (b.createdAt || b.executedAt || '').localeCompare(a.createdAt || a.executedAt || ''));
- const pageSize = 20;
- const currentPage = state.transfers.tablePage || 1;
- const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
- if (currentPage > totalPages) state.transfers.tablePage = totalPages;
- const start = (state.transfers.tablePage - 1) * pageSize;
- const pageItems = filtered.slice(start, start + pageSize);
- wrap.innerHTML = `
- <div class="table-wrap">
- <table>
- <thead>
- <tr>
- <th>类型</th>
- <th>原顾问</th>
- <th>新顾问</th>
- <th>记录数</th>
- <th>状态</th>
- <th>创建/执行时间</th>
- <th>操作</th>
- </tr>
- </thead>
- <tbody>
- ${pageItems.length ? pageItems.map(item => `
- <tr>
- <td>${item.type === 'preview' ? '<span class="badge badge-warning">预览</span>' : '<span class="badge badge-info">执行</span>'}</td>
- <td>${escapeHtml(item.fromUserId || '-')}</td>
- <td>${escapeHtml(item.toUserId || '-')}</td>
- <td>${formatNumber(item.itemCount || item.total || 0)}</td>
- <td>${item.status === 'EXECUTED' ? '<span class="badge badge-success">已执行</span>' : '<span class="badge badge-warning">草稿</span>'}</td>
- <td>${formatDateTime(item.createdAt || item.executedAt)}</td>
- <td>
- ${item.type === 'preview' ? `<button class="btn btn-sm" onclick="window.dashboardExecuteTransfer('${escapeHtml(item.filePath || '')}')">执行</button>` : `<button class="btn btn-sm btn-secondary" onclick="window.dashboardTransferDetail('${escapeHtml(item.filePath || '')}')">详情</button>`}
- </td>
- </tr>
- `).join('') : '<tr><td colspan="7" style="text-align:center;color:var(--text-muted);padding:40px">暂无交接记录</td></tr>'}
- </tbody>
- </table>
- </div>
- `;
- const pagination = page.querySelector('#tf-pagination');
- pagination.innerHTML = renderPagination(state.transfers.tablePage, filtered.length, pageSize);
- pagination.querySelectorAll('button').forEach(btn => {
- btn.addEventListener('click', () => {
- state.transfers.tablePage = Number(btn.dataset.page);
- loadTransfersTable(page);
- });
- });
- }
- function openTransferModal(page) {
- const body = `
- <div class="form-group" style="margin-bottom:16px">
- <label>原顾问 userId</label>
- <input id="new-tf-from" type="text" placeholder="fromUserId" />
- </div>
- <div class="form-group" style="margin-bottom:16px">
- <label>新顾问 userId</label>
- <input id="new-tf-to" type="text" placeholder="toUserId" />
- </div>
- <div class="form-group" style="margin-bottom:16px">
- <label>externalUserIds(每行一个,可选)</label>
- <textarea id="new-tf-external" rows="4" placeholder="externalUserId1\nexternalUserId2"></textarea>
- </div>
- <div class="form-group">
- <label>已确认群 roomIds(逗号分隔,可选)</label>
- <input id="new-tf-rooms" type="text" placeholder="roomId1,roomId2" />
- </div>
- `;
- openModal('新建交接预览', body, `
- <button class="btn btn-secondary" onclick="window.dashboardCloseModal()">取消</button>
- <button class="btn" id="btn-confirm-new-transfer">生成预览</button>
- `);
- document.getElementById('btn-confirm-new-transfer').addEventListener('click', async () => {
- const fromUserId = document.getElementById('new-tf-from').value.trim();
- const toUserId = document.getElementById('new-tf-to').value.trim();
- const externalUserIds = document.getElementById('new-tf-external').value.split(/\n|\r/).map(s => s.trim()).filter(Boolean);
- const roomIds = document.getElementById('new-tf-rooms').value.split(/[,,]/).map(s => s.trim()).filter(Boolean);
- if (!fromUserId || !toUserId) {
- toast('请输入原顾问和新顾问 userId', 'warning');
- return;
- }
- if (!externalUserIds.length && !roomIds.length) {
- toast('请输入 externalUserIds 或 roomIds', 'warning');
- return;
- }
- try {
- await api('POST', '/api/transfers/preview', { fromUserId, toUserId, externalUserIds, roomIds });
- toast('预览已生成');
- closeModal();
- loadTransfersDashboard(page);
- } catch (err) {
- toast(err.message || '生成失败', 'error');
- }
- });
- }
- window.dashboardExecuteTransfer = (filePath) => {
- const body = `
- <div class="form-group" style="margin-bottom:16px">
- <label>预览文件路径</label>
- <input id="exec-tf-file" type="text" value="${escapeHtml(filePath || '')}" placeholder="留空使用最新预览" />
- </div>
- <div class="form-group" style="margin-bottom:16px">
- <label>欢迎语</label>
- <input id="exec-tf-welcome" type="text" value="您好,我是您的新服务顾问,后续由我为您服务。" />
- </div>
- <div class="checkbox-row">
- <input id="exec-tf-remove" type="checkbox" />
- <label for="exec-tf-remove">交接后移除原顾问</label>
- </div>
- `;
- openModal('执行交接', body, `
- <button class="btn btn-secondary" onclick="window.dashboardCloseModal()">取消</button>
- <button class="btn" id="btn-confirm-exec-transfer">执行</button>
- `);
- document.getElementById('btn-confirm-exec-transfer').addEventListener('click', () => {
- const previewFile = document.getElementById('exec-tf-file').value.trim() || undefined;
- const welcomeText = document.getElementById('exec-tf-welcome').value.trim();
- const removeOldBroker = document.getElementById('exec-tf-remove').checked;
- closeModal();
- startJob('execute-transfer', '/api/transfers/execute', { guid: getGuid(), previewFile, welcomeText, removeOldBroker }, () => loadTransfersDashboard(document.querySelector('#tf-dashboard')?.closest('.page')));
- });
- };
- window.dashboardTransferDetail = async (filePath) => {
- if (!filePath) return;
- try {
- openModal('交接详情', `
- <div class="detail-panel">${escapeHtml(JSON.stringify({ filePath }, null, 2))}</div>
- <div style="margin-top:12px">${renderFileLink(filePath, '下载文件')}</div>
- `, `<button class="btn btn-secondary" onclick="window.dashboardCloseModal()">关闭</button>`);
- } 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 = 2156
- end = 2347
- new_lines = lines[:start] + [new_transfers + '\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('renderTransfersPage replaced successfully')
|