/** * TalkingQ Platform - Dashboard Page */ // 按公司统计的数据模型 const companyStats = { 'all': { name: '全部公司', kpi: { dau: 12847, dauChange: 8.3, sessions: 38204, sessionsChange: 5.1, devices: 9321, devicesChange: 120, totalSessions: 485203, totalChange: 2.1, avgTurns: 7.4, turnsChange: 0.3, badcaseRate: 1.2, badcaseChange: -0.3, }, // DAU趋势数据 (30天) dauData: [12000, 12200, 11800, 12500, 12100, 12800, 13200, 13000, 12700, 13400, 12900, 13500, 13800, 13200, 13600, 14000, 13500, 13800, 14200, 13800, 14000, 14400, 13900, 14100, 14500, 14000, 14200, 14600, 14200, 12847], // 每小时对话分布(24小时) hourlyData: [120,80,60,40,35,42,150,380,520,610,580,640,700,620,590,610,720,850,900,880,750,600,420,250], // 24小时每小时平均活跃用户折线图数据 hourlyActiveUsers: [4200, 3800, 3500, 3200, 2900, 2800, 3200, 4100, 5600, 6800, 7200, 7500, 7800, 7200, 6900, 7100, 8200, 9500, 10200, 9800, 8500, 7200, 5800, 4800], // 设备连接趋势数据(按天) deviceTrend7: [8200, 8350, 8100, 8480, 8620, 8750, 8921], deviceTrend14: [7800, 7950, 8100, 8020, 8150, 8280, 8350, 8420, 8560, 8680, 8750, 8820, 8880, 8921], deviceTrend30: [7200, 7350, 7280, 7450, 7520, 7680, 7750, 7820, 7950, 8020, 8150, 8220, 8350, 8420, 8480, 8550, 8620, 8750, 8820, 8880, 8921, 8950, 8980, 8921, 8950, 8980, 8921, 8921, 8921, 8921], // 设备详情数据 deviceDetails: [ { deviceId: 'DEV-202401-001234', lastOnline: '2026-05-13 14:32:15', conversationCount: 156 }, { deviceId: 'DEV-202401-005678', lastOnline: '2026-05-13 14:28:42', conversationCount: 89 }, { deviceId: 'DEV-202402-003456', lastOnline: '2026-05-13 14:15:30', conversationCount: 234 }, { deviceId: 'DEV-202402-007890', lastOnline: '2026-05-13 13:58:21', conversationCount: 67 }, { deviceId: 'DEV-202403-001234', lastOnline: '2026-05-13 13:42:08', conversationCount: 178 }, { deviceId: 'DEV-202403-005678', lastOnline: '2026-05-13 12:35:19', conversationCount: 45 }, { deviceId: 'DEV-202404-003456', lastOnline: '2026-05-13 11:20:55', conversationCount: 312 }, { deviceId: 'DEV-202404-007890', lastOnline: '2026-05-13 10:15:33', conversationCount: 98 }, ], // 对话轮次分布 turnsData: [8.2, 15.4, 19.1, 28.6, 18.3, 10.4], turnsLabels: ['1-2', '3-4', '5-6', '7-9', '10-14', '15+'], // OTA状态 otaData: [76.3, 8.2, 12.1, 3.4], // 功能使用分布 intentData: [24.1, 18.3, 15.7, 9.2, 7.8, 6.5, 5.9, 4.8, 4.1, 3.6], intentLabels: ['讲故事', '唱儿歌', '问答学习', '天气查询', '闹钟设置', '游戏互动', '英语学习', '百科问答', '情感陪伴', '其他'], // 语言分布 langData: [68.4, 12.3, 11.2, 5.1, 3.0], langLabels: ['中文(普通话)', '中文(粤语)', '英文', '日文', '其他'], // 热门关键词词云 wordCloud: [ { text: '白雪公主', size: 42, color: '#722ed1' }, { text: '小猪佩奇', size: 38, color: '#1890ff' }, { text: '儿歌', size: 35, color: '#52c41a' }, { text: '天气预报', size: 30, color: '#fa8c16' }, { text: '三只小猪', size: 28, color: '#eb2f96' }, { text: '英语学习', size: 26, color: '#13c2c2' }, { text: '睡前故事', size: 32, color: '#722ed1' }, { text: '十万个为什么', size: 24, color: '#52c41a' }, { text: '脑筋急转弯', size: 22, color: '#1890ff' }, { text: '唐诗', size: 20, color: '#fa8c16' }, { text: '公主城堡', size: 26, color: '#eb2f96' }, { text: '动物世界', size: 24, color: '#13c2c2' }, { text: '唱首歌', size: 18, color: '#52c41a' }, { text: '做游戏', size: 16, color: '#1890ff' }, ], }, 'company_a': { name: '智慧星科技', kpi: { dau: 5200, dauChange: 12.5, sessions: 15800, sessionsChange: 8.2, devices: 3200, devicesChange: 85, totalSessions: 198500, totalChange: 4.5, avgTurns: 8.2, turnsChange: 0.5, badcaseRate: 0.8, badcaseChange: -0.2, }, dauData: [4800, 4900, 5000, 4950, 5100, 5050, 5200, 5150, 5300, 5250, 5400, 5350, 5450, 5380, 5500, 5420, 5550, 5480, 5600, 5520, 5650, 5580, 5700, 5620, 5750, 5680, 5800, 5720, 5850, 5200], hourlyData: [80,50,40,30,25,35,120,300,450,520,510,560,600,550,520,540,620,720,780,750,640,500,350,200], hourlyActiveUsers: [1800, 1650, 1500, 1400, 1250, 1200, 1400, 1800, 2400, 2900, 3100, 3200, 3300, 3050, 2950, 3050, 3500, 4000, 4300, 4100, 3600, 3100, 2500, 2100], deviceTrend7: [2800, 2900, 2850, 2980, 3050, 3120, 3200], deviceTrend14: [2600, 2700, 2800, 2750, 2850, 2920, 2980, 3020, 3080, 3120, 3150, 3180, 3190, 3200], deviceTrend30: [2400, 2500, 2480, 2550, 2600, 2680, 2750, 2800, 2880, 2920, 2980, 3020, 3080, 3120, 3150, 3180, 3190, 3200, 3180, 3200, 3200, 3200, 3200, 3200, 3200, 3200, 3200, 3200, 3200, 3200], deviceDetails: [ { deviceId: 'DEV-A-2024-001234', lastOnline: '2026-05-13 14:30:25', conversationCount: 203 }, { deviceId: 'DEV-A-2024-005678', lastOnline: '2026-05-13 14:22:18', conversationCount: 156 }, { deviceId: 'DEV-A-2024-003456', lastOnline: '2026-05-13 14:05:42', conversationCount: 287 }, { deviceId: 'DEV-A-2024-007890', lastOnline: '2026-05-13 13:45:33', conversationCount: 98 }, { deviceId: 'DEV-A-2025-001234', lastOnline: '2026-05-13 13:28:11', conversationCount: 175 }, { deviceId: 'DEV-A-2025-005678', lastOnline: '2026-05-13 12:15:47', conversationCount: 67 }, { deviceId: 'DEV-A-2025-003456', lastOnline: '2026-05-13 11:08:22', conversationCount: 342 }, { deviceId: 'DEV-A-2025-007890', lastOnline: '2026-05-13 10:02:55', conversationCount: 124 }, ], turnsData: [6.5, 12.8, 18.2, 30.1, 20.4, 12.0], turnsLabels: ['1-2', '3-4', '5-6', '7-9', '10-14', '15+'], otaData: [82.5, 6.1, 9.2, 2.2], intentData: [28.5, 20.1, 18.2, 6.5, 5.2, 4.8, 8.5, 4.2, 2.5, 1.5], intentLabels: ['讲故事', '唱儿歌', '问答学习', '天气查询', '闹钟设置', '游戏互动', '英语学习', '百科问答', '情感陪伴', '其他'], langData: [72.1, 8.5, 14.2, 3.2, 2.0], langLabels: ['中文(普通话)', '中文(粤语)', '英文', '日文', '其他'], // 热门关键词词云 wordCloud: [ { text: '钢铁侠', size: 40, color: '#c41a1a' }, { text: '蜘蛛侠', size: 36, color: '#1890ff' }, { text: '迪士尼', size: 32, color: '#722ed1' }, { text: '超级英雄', size: 30, color: '#eb2f96' }, { text: '星球大战', size: 28, color: '#faad14' }, { text: '英语对话', size: 26, color: '#52c41a' }, { text: '公主故事', size: 34, color: '#eb2f96' }, { text: '科幻故事', size: 24, color: '#13c2c2' }, { text: '动画片', size: 22, color: '#1890ff' }, { text: '玩具总动员', size: 26, color: '#fa8c16' }, { text: '知识问答', size: 20, color: '#52c41a' }, { text: '唱跳', size: 18, color: '#722ed1' }, ], }, 'company_b': { name: '未来玩具厂', kpi: { dau: 4100, dauChange: 5.2, sessions: 12500, sessionsChange: 3.8, devices: 1500, devicesChange: 45, totalSessions: 156800, totalChange: 1.8, avgTurns: 6.8, turnsChange: 0.2, badcaseRate: 1.5, badcaseChange: -0.4, }, dauData: [3800, 3850, 3900, 3880, 3950, 3920, 4000, 3980, 4050, 4020, 4100, 4080, 4150, 4120, 4200, 4180, 4250, 4220, 4300, 4280, 4350, 4320, 4400, 4380, 4450, 4420, 4500, 4480, 4550, 4100], hourlyData: [60,40,30,25,20,28,90,220,380,450,440,480,520,480,460,470,540,650,700,680,560,420,300,150], hourlyActiveUsers: [1350, 1200, 1100, 1000, 900, 950, 1100, 1400, 1900, 2300, 2500, 2600, 2700, 2500, 2400, 2500, 2800, 3200, 3500, 3300, 2900, 2500, 1900, 1550], deviceTrend7: [1320, 1380, 1350, 1400, 1440, 1470, 1500], deviceTrend14: [1250, 1300, 1320, 1350, 1380, 1400, 1420, 1440, 1460, 1470, 1480, 1490, 1495, 1500], deviceTrend30: [1150, 1200, 1180, 1220, 1250, 1280, 1300, 1320, 1350, 1380, 1400, 1420, 1440, 1450, 1460, 1470, 1480, 1490, 1495, 1498, 1500, 1500, 1500, 1500, 1500, 1500, 1500, 1500, 1500, 1500], deviceDetails: [ { deviceId: 'DEV-B-2024-001234', lastOnline: '2026-05-13 14:25:33', conversationCount: 145 }, { deviceId: 'DEV-B-2024-005678', lastOnline: '2026-05-13 14:12:55', conversationCount: 78 }, { deviceId: 'DEV-B-2024-003456', lastOnline: '2026-05-13 13:55:18', conversationCount: 198 }, { deviceId: 'DEV-B-2024-007890', lastOnline: '2026-05-13 13:35:42', conversationCount: 56 }, { deviceId: 'DEV-B-2025-001234', lastOnline: '2026-05-13 13:18:25', conversationCount: 167 }, { deviceId: 'DEV-B-2025-005678', lastOnline: '2026-05-13 12:05:33', conversationCount: 43 }, { deviceId: 'DEV-B-2025-003456', lastOnline: '2026-05-13 11:42:17', conversationCount: 234 }, { deviceId: 'DEV-B-2025-007890', lastOnline: '2026-05-13 10:28:44', conversationCount: 89 }, ], turnsData: [10.2, 18.4, 20.1, 26.2, 16.1, 9.0], turnsLabels: ['1-2', '3-4', '5-6', '7-9', '10-14', '15+'], otaData: [74.8, 9.5, 11.8, 3.9], intentData: [22.1, 22.5, 12.3, 10.8, 8.5, 7.2, 4.5, 5.8, 4.2, 2.1], intentLabels: ['讲故事', '唱儿歌', '问答学习', '天气查询', '闹钟设置', '游戏互动', '英语学习', '百科问答', '情感陪伴', '其他'], langData: [65.2, 18.5, 8.5, 5.8, 2.0], langLabels: ['中文(普通话)', '中文(粤语)', '英文', '日文', '其他'], // 热门关键词词云 wordCloud: [ { text: '小猪佩奇', size: 44, color: '#eb2f96' }, { text: '汪汪队', size: 38, color: '#1890ff' }, { text: '儿歌', size: 35, color: '#52c41a' }, { text: '巧虎', size: 30, color: '#fa8c16' }, { text: '超级飞侠', size: 28, color: '#722ed1' }, { text: '睡前故事', size: 26, color: '#13c2c2' }, { text: '海底小纵队', size: 32, color: '#1890ff' }, { text: '成语故事', size: 24, color: '#c41a1a' }, { text: '童谣', size: 22, color: '#52c41a' }, { text: '认知游戏', size: 20, color: '#fa8c16' }, { text: '认识动物', size: 26, color: '#722ed1' }, { text: '认颜色', size: 18, color: '#eb2f96' }, ], }, 'company_c': { name: '童趣电子', kpi: { dau: 3547, dauChange: 3.1, sessions: 9904, sessionsChange: 2.5, devices: 800, devicesChange: 25, totalSessions: 129903, totalChange: 0.8, avgTurns: 6.2, turnsChange: 0.1, badcaseRate: 2.1, badcaseChange: -0.5, }, dauData: [3200, 3250, 3300, 3280, 3350, 3320, 3400, 3380, 3450, 3420, 3500, 3480, 3550, 3520, 3600, 3580, 3650, 3620, 3700, 3680, 3750, 3720, 3800, 3780, 3850, 3820, 3900, 3880, 3950, 3547], hourlyData: [50,35,25,20,18,22,70,180,280,350,340,380,420,390,370,380,450,550,600,580,470,320,220,120], hourlyActiveUsers: [1050, 950, 850, 800, 750, 720, 850, 1050, 1350, 1600, 1700, 1800, 1850, 1700, 1650, 1700, 1950, 2300, 2500, 2400, 2100, 1800, 1400, 1150], deviceTrend7: [710, 730, 720, 750, 770, 785, 800], deviceTrend14: [680, 700, 710, 720, 735, 745, 755, 765, 775, 780, 785, 790, 795, 800], deviceTrend30: [620, 640, 635, 650, 660, 675, 685, 695, 705, 715, 725, 735, 745, 755, 765, 775, 780, 785, 790, 795, 800, 800, 800, 800, 800, 800, 800, 800, 800, 800], deviceDetails: [ { deviceId: 'DEV-C-2024-001234', lastOnline: '2026-05-13 14:18:45', conversationCount: 112 }, { deviceId: 'DEV-C-2024-005678', lastOnline: '2026-05-13 14:05:22', conversationCount: 67 }, { deviceId: 'DEV-C-2024-003456', lastOnline: '2026-05-13 13:48:35', conversationCount: 156 }, { deviceId: 'DEV-C-2024-007890', lastOnline: '2026-05-13 13:22:18', conversationCount: 45 }, { deviceId: 'DEV-C-2025-001234', lastOnline: '2026-05-13 13:05:42', conversationCount: 98 }, { deviceId: 'DEV-C-2025-005678', lastOnline: '2026-05-13 12:28:55', conversationCount: 38 }, { deviceId: 'DEV-C-2025-003456', lastOnline: '2026-05-13 11:35:28', conversationCount: 187 }, { deviceId: 'DEV-C-2025-007890', lastOnline: '2026-05-13 10:42:15', conversationCount: 72 }, ], turnsData: [12.5, 20.2, 22.1, 23.5, 14.2, 7.5], turnsLabels: ['1-2', '3-4', '5-6', '7-9', '10-14', '15+'], otaData: [68.5, 10.2, 15.8, 5.5], intentData: [18.2, 15.5, 20.5, 12.5, 10.2, 8.8, 3.2, 5.5, 3.8, 1.8], intentLabels: ['讲故事', '唱儿歌', '问答学习', '天气查询', '闹钟设置', '游戏互动', '英语学习', '百科问答', '情感陪伴', '其他'], langData: [58.5, 22.5, 10.5, 6.5, 2.0], langLabels: ['中文(普通话)', '中文(粤语)', '英文', '日文', '其他'], // 热门关键词词云 wordCloud: [ { text: '熊出没', size: 42, color: '#52c41a' }, { text: '大头儿子', size: 36, color: '#1890ff' }, { text: '儿歌', size: 34, color: '#fa8c16' }, { text: '睡前故事', size: 30, color: '#722ed1' }, { text: '认识数字', size: 28, color: '#eb2f96' }, { text: '学英语', size: 26, color: '#13c2c2' }, { text: '西游记', size: 32, color: '#fa8c16' }, { text: '神话故事', size: 24, color: '#c41a1a' }, { text: '儿童谜语', size: 22, color: '#1890ff' }, { text: '自然科普', size: 20, color: '#52c41a' }, { text: '安全教育', size: 26, color: '#722ed1' }, { text: '音乐互动', size: 18, color: '#eb2f96' }, ], }, }; // 保存图表实例 let _dashboardCharts = {}; let dashboardSelectedPeriod = 30; let dashboardRefreshSeed = 0; // 颜色配置 const chartColors = { company_a: { primary: '#722ed1', primaryLight: 'rgba(114,46,209,0.12)' }, company_b: { primary: '#52c41a', primaryLight: 'rgba(82,196,26,0.12)' }, company_c: { primary: '#fa8c16', primaryLight: 'rgba(250,140,22,0.12)' }, all: { primary: '#1890ff', primaryLight: 'rgba(24,144,255,0.12)' }, }; function getDashboardCompanyId() { if (window.__getCurrentCompanyId) return window.__getCurrentCompanyId(); return typeof window.currentCompany === 'string' ? window.currentCompany : 'all'; } registerPage('dashboard', function(container) { container.innerHTML = `
今日活跃设备
-
-
今日对话数
-
-
总设备数
-
总对话数
-
-
平均每次对话互动
-
-
问题对话率
-
-
对话记录统计
近 30 天每日对话次数趋势
设备连接趋势
近 30 天按日期统计设备连接数量
热门关键词词云
热门话题排行
点击柱状图查看相关会话
设备连接详情
`; lucide.createIcons({ elements: [container] }); // 初始化图表 initDashboardCharts(); syncDashboardPeriodControls(dashboardSelectedPeriod); bindDashboardDateRange(); // 更新当前公司描述 updateDashboardCompanyDesc(); // 绑定公司切换事件 window.removeEventListener('companyChanged', handleCompanyChange); window.addEventListener('companyChanged', handleCompanyChange); window.removeEventListener('devicesChanged', handleDeviceDataChange); window.addEventListener('devicesChanged', handleDeviceDataChange); }); // 处理公司切换 function handleCompanyChange() { updateDashboardCompanyDesc(); updateDashboardData(); } function handleDeviceDataChange() { updateDashboardCompanyDesc(); updateDashboardData(); } function updateDashboardCompanyDesc() { const desc = document.getElementById('dashboardCompanyDesc'); const companyId = getDashboardCompanyId(); const company = companyId === 'all' ? { id: 'all', name: '全部公司' } : (window.__getCompanyById ? window.__getCompanyById(companyId) : window.companies?.find(c => c.id === companyId)); const appScope = window.__dashboardAppScope; if (desc && company) { desc.textContent = appScope?.companyId === companyId ? `${company.name} · ${appScope.name} · 智能体数据分析` : `${company.name} · 实时监控 AI 玩具运营情况`; } } function getDashboardStats(companyId) { const base = companyStats[companyId] || createEmptyDashboardStats(companyId); const liveStats = applyLiveDeviceStats(base, companyId); if (!dashboardRefreshSeed) return liveStats; return applyDashboardRefreshSnapshot(liveStats, dashboardRefreshSeed); } function createEmptyDashboardStats(companyId) { const company = window.__getCompanyById ? window.__getCompanyById(companyId) : null; return { name: company?.name || '新公司', kpi: { dau: 0, dauChange: 0, sessions: 0, sessionsChange: 0, devices: 0, devicesChange: 0, totalSessions: 0, totalChange: 0, avgTurns: 0, turnsChange: 0, badcaseRate: 0, badcaseChange: 0, }, dauData: Array.from({ length: 30 }, () => 0), hourlyActiveUsers: Array.from({ length: 24 }, () => 0), deviceTrend7: Array.from({ length: 7 }, () => 0), deviceTrend14: Array.from({ length: 14 }, () => 0), deviceTrend30: Array.from({ length: 30 }, () => 0), deviceDetails: [], wordCloud: [], intentLabels: [], intentData: [], }; } function getLiveDashboardDevices(companyId) { if (typeof window.__getAllDevices !== 'function') return null; const devices = window.__getAllDevices(); if (companyId === 'all') return devices; return devices.filter(device => device.companyId === companyId); } function applyLiveDeviceStats(base, companyId) { const liveDevices = getLiveDashboardDevices(companyId); if (!liveDevices) return base; const totalDevices = liveDevices.length; const onlineDevices = liveDevices.filter(device => device.status === 'online').length; const deviceDetails = liveDevices.slice(0, 8).map(device => ({ deviceId: device.id, lastOnline: device.lastSeen || '-', conversationCount: Math.max(0, Math.round((device.battery || 0) / 8) + (device.status === 'online' ? 8 : 1)), })); const withLiveDeviceTrend = (series = []) => { const targetLength = series.length || 7; if (totalDevices === 0) return Array.from({ length: targetLength }, () => 0); return Array.from({ length: targetLength }, (_, index) => { const progress = targetLength <= 1 ? 1 : index / (targetLength - 1); const baseline = Math.max(0, Math.round(totalDevices * (0.82 + progress * 0.18))); return index === targetLength - 1 ? totalDevices : baseline; }); }; const isNewCompany = !companyStats[companyId]; return { ...base, kpi: { ...base.kpi, devices: totalDevices, dau: isNewCompany ? onlineDevices : base.kpi.dau, sessions: isNewCompany ? onlineDevices * 3 : base.kpi.sessions, totalSessions: isNewCompany ? onlineDevices * 18 : base.kpi.totalSessions, avgTurns: isNewCompany ? (onlineDevices ? 5.2 : 0) : base.kpi.avgTurns, }, deviceTrend7: withLiveDeviceTrend(base.deviceTrend7), deviceTrend14: withLiveDeviceTrend(base.deviceTrend14), deviceTrend30: withLiveDeviceTrend(base.deviceTrend30), deviceDetails: deviceDetails.length ? deviceDetails : [], }; } function applyDashboardRefreshSnapshot(base, seed) { const wave = Math.sin(seed * 1.37); const smallWave = Math.cos(seed * 0.91); const deviceDelta = Math.max(1, Math.round(18 + seed * 3 + wave * 10)); const dauDelta = Math.round(120 + wave * 70); const sessionDelta = Math.round(420 + smallWave * 180); const totalDelta = Math.round(900 + seed * 140 + wave * 260); const avgTurnsDelta = Number((smallWave * 0.18).toFixed(1)); const badcaseDelta = Number((Math.sin(seed * 0.63) * 0.18).toFixed(1)); return { ...base, kpi: { ...base.kpi, dau: Math.max(0, base.kpi.dau + dauDelta), dauChange: Number((base.kpi.dauChange + wave * 0.6).toFixed(1)), sessions: Math.max(0, base.kpi.sessions + sessionDelta), sessionsChange: Number((base.kpi.sessionsChange + smallWave * 0.5).toFixed(1)), devices: Math.max(0, base.kpi.devices + deviceDelta), devicesChange: deviceDelta, totalSessions: Math.max(0, base.kpi.totalSessions + totalDelta), totalChange: Number((base.kpi.totalChange + wave * 0.3).toFixed(1)), avgTurns: Math.max(1, Number((base.kpi.avgTurns + avgTurnsDelta).toFixed(1))), turnsChange: Number((base.kpi.turnsChange + avgTurnsDelta).toFixed(1)), badcaseRate: Math.max(0, Number((base.kpi.badcaseRate + badcaseDelta).toFixed(1))), badcaseChange: Number((base.kpi.badcaseChange - badcaseDelta).toFixed(1)), }, dauData: refreshSeries(base.dauData, seed, 160), hourlyActiveUsers: refreshSeries(base.hourlyActiveUsers, seed, 90), deviceTrend7: refreshSeries(base.deviceTrend7, seed, 36), deviceTrend14: refreshSeries(base.deviceTrend14, seed, 32), deviceTrend30: refreshSeries(base.deviceTrend30, seed, 28), intentData: refreshPercentSeries(base.intentData, seed, 1.1), langData: normalizePercentSeries(refreshPercentSeries(base.langData, seed, 0.5)), wordCloud: (base.wordCloud || []).map((word, index) => ({ ...word, size: Math.max(16, Math.round(word.size + Math.sin((index + 1) * seed * 0.37) * 3)), })), deviceDetails: (base.deviceDetails || []).map((item, index) => ({ ...item, lastOnline: index === 0 ? '刚刚' : item.lastOnline, conversationCount: Math.max(0, item.conversationCount + Math.round(Math.sin((index + 1) * seed) * 8 + seed)), })), }; } function refreshSeries(series = [], seed, amplitude) { return series.map((value, index) => { const delta = Math.round(Math.sin((index + 1) * 0.62 + seed) * amplitude + seed * 3); return Math.max(0, value + delta); }); } function refreshPercentSeries(series = [], seed, amplitude) { return series.map((value, index) => { const delta = Math.sin((index + 1) * 0.78 + seed) * amplitude; return Number(Math.max(0.1, value + delta).toFixed(1)); }); } function normalizePercentSeries(series = []) { const total = series.reduce((sum, value) => sum + value, 0); if (!total) return series; return series.map(value => Number((value * 100 / total).toFixed(1))); } // 更新 KPI 数据 function updateDashboardData() { const companyId = getDashboardCompanyId(); const stats = getDashboardStats(companyId); // 更新 KPI 数值 document.getElementById('kpi-dau').textContent = `${formatNumber(stats.kpi.dau)} 台`; document.getElementById('kpi-sessions').textContent = `${formatNumber(stats.kpi.sessions)} 次`; document.getElementById('kpi-devices').textContent = `${formatNumber(stats.kpi.devices)} 台`; document.getElementById('kpi-activation').textContent = `${formatNumber(stats.kpi.totalSessions)} 次`; document.getElementById('kpi-turns').textContent = `${stats.kpi.avgTurns.toFixed(1)} 轮`; document.getElementById('kpi-badcase').textContent = stats.kpi.badcaseRate.toFixed(1) + '%'; // 更新 KPI 变化 updateKpiChange('kpi-dau', stats.kpi.dauChange, { unit: '%', suffix: '较昨日' }); updateKpiChange('kpi-sessions', stats.kpi.sessionsChange, { unit: '%', suffix: '较昨日' }); updateKpiChange('kpi-activation', stats.kpi.totalChange, { unit: '%', suffix: '较昨日' }); updateKpiChange('kpi-turns', stats.kpi.turnsChange, { unit: '轮', suffix: '较昨日' }); updateKpiChange('kpi-badcase', stats.kpi.badcaseChange, { unit: '%', suffix: '较昨日', inverse: true }); // 更新简洁统计卡片 const statDau = document.getElementById('stat-dau'); const statDevices = document.getElementById('stat-devices'); const statSessions = document.getElementById('stat-sessions'); const statTotal = document.getElementById('stat-total'); if (statDau) statDau.textContent = `${stats.kpi.dau.toLocaleString()} 台`; if (statDevices) statDevices.textContent = `${stats.kpi.devices.toLocaleString()} 台`; if (statSessions) statSessions.textContent = `${stats.kpi.sessions.toLocaleString()} 次`; if (statTotal) statTotal.textContent = `${stats.kpi.totalSessions.toLocaleString()} 次`; // 更新图表 updateDashboardCharts(companyId); } function updateKpiChange(id, change, options = {}) { const el = document.getElementById(id + '-change'); if (!el) return; const { unit = '%', absolute = false, inverse = false, suffix = '' } = options; const isUp = inverse ? change < 0 : change > 0; const isDown = inverse ? change > 0 : change < 0; const sign = change > 0 ? '+' : ''; const value = absolute ? `${sign}${change}` : `${sign}${change.toFixed(1)}`; const unitText = unit ? ` ${unit}` : ''; const suffixText = suffix ? ` ${suffix}` : ''; el.className = 'kpi-change ' + (isUp ? 'up' : isDown ? 'down' : ''); el.innerHTML = ` ${value}${unitText}${suffixText}${isDown ? ' 持续改善中' : ''} `; lucide.createIcons({ nodes: [el] }); } function formatNumber(num) { if (num >= 10000) { return (num / 10000).toFixed(1) + '万'; } return num.toLocaleString(); } // 渲染词云 function renderWordCloud(containerId, words) { const container = document.getElementById(containerId); if (!container || !words) return; // 创建词云容器 let cloudContainer = container.parentElement.querySelector('.wordcloud-container'); if (!cloudContainer) { cloudContainer = document.createElement('div'); cloudContainer.className = 'wordcloud-container'; cloudContainer.style.cssText = ` display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 12px 18px; padding: 20px; min-height: 200px; align-content: center; `; container.style.display = 'none'; container.parentElement.appendChild(cloudContainer); } // 渲染词云文字 cloudContainer.innerHTML = words.map(word => ` ${word.text} `).join(''); } // 更新图表数据 function updateDashboardCharts(companyId) { const stats = getDashboardStats(companyId); const colors = chartColors[companyId] || chartColors['all']; const period = getCurrentTrendPeriod(); syncDashboardPeriodControls(period); // 更新对话记录趋势图表 if (_dashboardCharts.hourlyDau) { _dashboardCharts.hourlyDau.data.datasets[0].borderColor = colors.primary; _dashboardCharts.hourlyDau.data.datasets[0].backgroundColor = colors.primaryLight; _dashboardCharts.hourlyDau.data.labels = genLabels(period); _dashboardCharts.hourlyDau.data.datasets[0].data = getConversationTrendData(stats, period); _dashboardCharts.hourlyDau.update(); } // 更新设备连接趋势图表(保持当前选中的周期) updateDeviceTrendChart(companyId, period); const keywordCloud = getWordCloudData(stats, period); const topicRanking = getTopicRankingData(stats, period); // 更新热门关键词词云 renderWordCloud('turnsChart', keywordCloud); // 更新热门话题排行图表 if (_dashboardCharts.intent) { _dashboardCharts.intent.data.datasets[0].data = topicRanking.map(item => item.score); _dashboardCharts.intent.data.labels = topicRanking.map(item => item.text); _dashboardCharts.intent.data.datasets[0].backgroundColor = colors.primaryLight.replace('0.12', '0.7'); _dashboardCharts.intent.update(); } // 更新设备详情表格 updateDeviceDetailTable(companyId); } // 获取当前设备连接趋势的周期 function getCurrentTrendPeriod() { return dashboardSelectedPeriod; } // 更新设备连接趋势图表 function updateDeviceTrendChart(companyId, period) { const stats = getDashboardStats(companyId); const normalizedPeriod = normalizeDashboardPeriod(period); const data = getDeviceTrendData(stats, normalizedPeriod); dashboardSelectedPeriod = normalizedPeriod; syncDashboardPeriodControls(normalizedPeriod); const labels = genLabels(normalizedPeriod); if (_dashboardCharts.deviceTrend) { _dashboardCharts.deviceTrend.data.labels = labels; _dashboardCharts.deviceTrend.data.datasets[0].data = data; _dashboardCharts.deviceTrend.update(); } } function normalizeDashboardPeriod(period) { const value = Number.parseInt(period, 10); return [7, 14, 30].includes(value) ? value : 30; } function getDeviceTrendData(stats, period) { if (period === 7) return stats.deviceTrend7 || []; if (period === 14) return stats.deviceTrend14 || stats.deviceTrend7 || []; return stats.deviceTrend30 || stats.deviceTrend14 || stats.deviceTrend7 || []; } function getDauTrendData(stats, period) { const source = stats.dauData || []; if (source.length >= period) return source.slice(-period); return padSeries(source, period); } function getConversationTrendData(stats, period) { const activeDeviceSeries = getDauTrendData(stats, period); const todayActiveDevices = activeDeviceSeries[activeDeviceSeries.length - 1] || stats.kpi.dau || 1; const todaySessions = stats.kpi.sessions || 0; const sessionsPerDevice = todayActiveDevices ? todaySessions / todayActiveDevices : 3; return activeDeviceSeries.map((value, index) => { if (index === activeDeviceSeries.length - 1) { return Math.max(0, Math.round(todaySessions)); } const progress = activeDeviceSeries.length <= 1 ? 1 : index / (activeDeviceSeries.length - 1); const weekdayWave = 1 + Math.sin((index + 1) * 0.9) * 0.06 + Math.cos((index + 1) * 0.37) * 0.04; const recentLift = 0.92 + progress * 0.08; return Math.max(0, Math.round(value * sessionsPerDevice * weekdayWave * recentLift)); }); } function getWordCloudData(stats, period) { const factor = getPeriodFactor(period); return (stats.wordCloud || []).map((word, index) => ({ ...word, size: Math.max(16, Math.round(word.size + Math.cos((index + 1) * factor) * (period === 7 ? 4 : period === 14 ? 2 : 0))), })); } function getTopicRankingData(stats, period) { const labels = stats.intentLabels || []; const factor = getPeriodFactor(period); const values = (stats.intentData || []).map((value, index) => { const adjusted = value + Math.sin((index + 1) * factor) * (period === 7 ? 1.8 : period === 14 ? 1.1 : 0.4); return Math.max(0.1, adjusted); }); const total = values.reduce((sum, value) => sum + value, 0) || 1; return labels.map((label, index) => ({ text: label, score: Number(((values[index] || 0) * 100 / total).toFixed(1)), })).sort((a, b) => b.score - a.score); } function openDashboardTopicSessions(topic) { if (!topic) return; const company = window.getCurrentCompany ? window.getCurrentCompany() : { id: getDashboardCompanyId(), name: companyStats[getDashboardCompanyId()]?.name || '全部公司' }; const payload = { topic, companyId: getDashboardCompanyId(), companyName: company.name, period: normalizeDashboardPeriod(dashboardSelectedPeriod), source: 'dashboard-topic-ranking', createdAt: Date.now(), }; sessionStorage.setItem('talkingq_session_topic_drilldown', JSON.stringify(payload)); Toast.info(`正在打开「${topic}」相关会话`, 900); navigateTo('session'); } function getPeriodFactor(period) { if (period === 7) return 0.9; if (period === 14) return 0.55; return 0.2; } function padSeries(source, length) { if (!source.length) return Array.from({ length }, () => 0); const result = []; for (let i = 0; i < length; i++) { result.push(source[i % source.length]); } return result; } function updateDashboardPeriod(period) { dashboardSelectedPeriod = normalizeDashboardPeriod(period); syncDashboardPeriodControls(dashboardSelectedPeriod); updateDashboardData(); } function syncDashboardPeriodControls(period) { const normalizedPeriod = normalizeDashboardPeriod(period); const select = document.getElementById('dashDateRange'); if (select) select.value = String(normalizedPeriod); const subtitle = document.getElementById('deviceTrendSubtitle'); if (subtitle) { subtitle.textContent = `近 ${normalizedPeriod} 天按日期统计设备连接数量`; } const dialogSubtitle = document.getElementById('dialogTrendSubtitle'); if (dialogSubtitle) { dialogSubtitle.textContent = `近 ${normalizedPeriod} 天每日对话次数趋势`; } } function bindDashboardDateRange() { const select = document.getElementById('dashDateRange'); if (!select) return; select.addEventListener('change', () => { updateDashboardPeriod(select.value); }); } // 更新设备连接详情表格 function updateDeviceDetailTable(companyId) { const stats = getDashboardStats(companyId); const tableEl = document.getElementById('deviceDetailTable'); if (!tableEl) return; tableEl.innerHTML = `
${stats.deviceDetails.length ? stats.deviceDetails.map(d => ` `).join('') : ` `}
设备ID 最后在线时间 对话次数
${d.deviceId} ${d.lastOnline} ${d.conversationCount} 次
暂无设备数据
`; } // 刷新仪表盘 function refreshDashboard() { const refreshBtn = document.querySelector('[data-action="dashboard-refresh"]'); if (refreshBtn) refreshBtn.disabled = true; Toast.info('数据刷新中...', 600); setTimeout(() => { dashboardRefreshSeed += 1; updateDashboardData(); if (refreshBtn) refreshBtn.disabled = false; Toast.success('数据已更新', 900); }, 250); } function initDashboardCharts() { const gridColor = 'rgba(0,0,0,0.05)'; const currentCompany = getDashboardCompanyId(); const stats = getDashboardStats(currentCompany); const colors = chartColors[currentCompany] || chartColors['all']; if (_dashboardCharts.lang) { _dashboardCharts.lang.destroy(); _dashboardCharts.lang = null; } const defaultOptions = { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { grid: { color: gridColor }, ticks: { font: { size: 11 }, color: '#8c8c8c' } }, y: { grid: { color: gridColor }, ticks: { font: { size: 11 }, color: '#8c8c8c' } }, }, }; // 对话记录趋势图表 const hourlyDauCtx = document.getElementById('hourlyDauChart'); if (hourlyDauCtx) { const period = normalizeDashboardPeriod(dashboardSelectedPeriod); _dashboardCharts.hourlyDau = new Chart(hourlyDauCtx, { type: 'line', data: { labels: genLabels(period), datasets: [{ label: '对话次数', data: getConversationTrendData(stats, period), borderColor: colors.primary, backgroundColor: colors.primaryLight, fill: true, tension: 0.4, pointRadius: 3, pointHoverRadius: 6, borderWidth: 2, }], }, options: { ...defaultOptions, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false, callbacks: { label: ctx => ` ${Number(ctx.raw).toLocaleString()} 次对话` } }, }, scales: { x: { grid: { color: gridColor }, ticks: { font: { size: 11 }, color: '#8c8c8c' } }, y: { grid: { color: gridColor }, ticks: { font: { size: 11 }, color: '#8c8c8c', callback: v => v.toLocaleString() } }, }, }, }); } // 设备连接趋势折线图 const deviceTrendCtx = document.getElementById('deviceTrendChart'); if (deviceTrendCtx) { const period = normalizeDashboardPeriod(dashboardSelectedPeriod); const labels = genLabels(period); _dashboardCharts.deviceTrend = new Chart(deviceTrendCtx, { type: 'line', data: { labels, datasets: [{ label: '设备数', data: getDeviceTrendData(stats, period), borderColor: colors.primary, backgroundColor: colors.primaryLight, fill: true, tension: 0.4, pointRadius: 3, pointHoverRadius: 6, borderWidth: 2, }], }, options: { ...defaultOptions, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false, callbacks: { label: ctx => ` ${ctx.raw} 台设备` } }, }, scales: { x: { grid: { color: gridColor }, ticks: { font: { size: 11 }, color: '#8c8c8c' } }, y: { grid: { color: gridColor }, ticks: { font: { size: 11 }, color: '#8c8c8c', callback: v => v.toLocaleString() } }, }, }, }); } // Turns distribution - 词云效果 const turnsCtx = document.getElementById('turnsChart'); if (turnsCtx) { // 清空现有图表 turnsCtx.getContext('2d').clearRect(0, 0, turnsCtx.width, turnsCtx.height); // 销毁旧图表实例 if (_dashboardCharts.turns) { _dashboardCharts.turns.destroy(); _dashboardCharts.turns = null; } } const period = normalizeDashboardPeriod(dashboardSelectedPeriod); const keywordCloud = getWordCloudData(stats, period); const topicRanking = getTopicRankingData(stats, period); // 渲染词云 renderWordCloud('turnsChart', keywordCloud); // Topic Ranking Bar const intentCtx = document.getElementById('intentChart'); if (intentCtx) { _dashboardCharts.intent = new Chart(intentCtx, { type: 'bar', data: { labels: topicRanking.map(item => item.text), datasets: [{ label: '占比', data: topicRanking.map(item => item.score), backgroundColor: colors.primaryLight.replace('0.12', '0.7'), borderRadius: 4, borderSkipped: false, }], }, options: { ...defaultOptions, indexAxis: 'y', plugins: { legend: { display: false }, tooltip: { callbacks: { label: ctx => ` 占比 ${ctx.raw}%` } }, }, onClick: (event, elements, chart) => { if (!elements.length) return; const topic = chart.data.labels[elements[0].index]; openDashboardTopicSessions(topic); }, onHover: (event, elements) => { const target = event?.native?.target; if (target) target.style.cursor = elements.length ? 'pointer' : 'default'; }, scales: { x: { grid: { color: gridColor }, ticks: { callback: v => v + '%', font: { size: 11 }, color: '#8c8c8c' } }, y: { grid: { display: false }, ticks: { font: { size: 11 }, color: '#595959' } }, }, }, }); } // 设备连接详情表格 updateDeviceDetailTable(currentCompany); // 初始化时更新 KPI updateDashboardData(); } function genLabels(n) { const labels = []; const today = new Date(); for (let i = n - 1; i >= 0; i--) { const d = new Date(today); d.setDate(d.getDate() - i); labels.push(`${d.getMonth()+1}/${d.getDate()}`); } return labels; }