Files
TalkingQ_Dashboard/scripts/dashboard.js
“yuanmengy” a225272d1e data&device ready
2026-06-05 17:11:56 +08:00

1051 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 = `
<div class="page-header">
<div>
<div class="page-title">数据统计</div>
<div class="page-desc" id="dashboardCompanyDesc">系统运行数据分析 · 实时监控 AI 玩具运营情况</div>
</div>
<div class="page-actions">
<select class="filter-select" id="dashDateRange">
<option value="7">近 7 天</option>
<option value="14">近 14 天</option>
<option value="30" selected>近 30 天</option>
</select>
<button class="btn btn-outline btn-sm" data-action="dashboard-refresh" onclick="refreshDashboard()">
<i data-lucide="refresh-cw"></i> 刷新
</button>
</div>
</div>
<!-- KPI Cards -->
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-icon"><i data-lucide="users"></i></div>
<div class="kpi-label">今日活跃设备</div>
<div class="kpi-value" id="kpi-dau">-</div>
<div class="kpi-change" id="kpi-dau-change"><i data-lucide="trending-up" style="width:12px;height:12px"></i> -</div>
</div>
<div class="kpi-card">
<div class="kpi-icon"><i data-lucide="message-circle"></i></div>
<div class="kpi-label">今日对话数</div>
<div class="kpi-value" id="kpi-sessions">-</div>
<div class="kpi-change" id="kpi-sessions-change"><i data-lucide="trending-up" style="width:12px;height:12px"></i> -</div>
</div>
<div class="kpi-card">
<div class="kpi-icon"><i data-lucide="bar-chart-2"></i></div>
<div class="kpi-label">总设备数</div>
<div class="kpi-value" id="kpi-devices">-</div>
</div>
<div class="kpi-card">
<div class="kpi-icon"><i data-lucide="zap"></i></div>
<div class="kpi-label">总对话数</div>
<div class="kpi-value" id="kpi-activation">-</div>
<div class="kpi-change" id="kpi-activation-change"><i data-lucide="trending-up" style="width:12px;height:12px"></i> -</div>
</div>
<div class="kpi-card">
<div class="kpi-icon"><i data-lucide="tablet-smartphone"></i></div>
<div class="kpi-label">平均每次对话互动</div>
<div class="kpi-value" id="kpi-turns">-</div>
<div class="kpi-change" id="kpi-turns-change"><i data-lucide="trending-up" style="width:12px;height:12px"></i> -</div>
</div>
<div class="kpi-card">
<div class="kpi-icon"><i data-lucide="flag"></i></div>
<div class="kpi-label">问题对话率</div>
<div class="kpi-value" id="kpi-badcase">-</div>
<div class="kpi-change" id="kpi-badcase-change"><i data-lucide="trending-down" style="width:12px;height:12px"></i> -</div>
</div>
</div>
<!-- Charts Row 1 -->
<div class="charts-grid">
<div class="chart-card full">
<div class="card-header">
<div>
<div class="card-title">对话记录统计</div>
<div class="card-subtitle" id="dialogTrendSubtitle">近 30 天每日对话次数趋势</div>
</div>
</div>
<canvas id="hourlyDauChart"></canvas>
</div>
</div>
<div class="charts-grid">
<div class="chart-card full">
<div class="card-header">
<div>
<div class="card-title">设备连接趋势</div>
<div class="card-subtitle" id="deviceTrendSubtitle">近 30 天按日期统计设备连接数量</div>
</div>
</div>
<canvas id="deviceTrendChart"></canvas>
</div>
</div>
<div class="charts-grid">
<div class="chart-card">
<div class="card-header">
<div class="card-title">热门关键词词云</div>
</div>
<canvas id="turnsChart"></canvas>
</div>
<div class="chart-card">
<div class="card-header">
<div>
<div class="card-title">热门话题排行</div>
<div class="card-subtitle">点击柱状图查看相关会话</div>
</div>
</div>
<canvas id="intentChart"></canvas>
</div>
</div>
<!-- Bottom Row -->
<div class="charts-grid">
<div class="chart-card full">
<div class="card-header">
<div class="card-title">设备连接详情</div>
</div>
<div id="deviceDetailTable"></div>
</div>
</div>
`;
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 = `
<i data-lucide="${isUp ? 'trending-up' : isDown ? 'trending-down' : 'minus'}" style="width:12px;height:12px"></i>
${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 => `
<span style="
font-size: ${word.size}px;
font-weight: 600;
color: ${word.color};
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
cursor: default;
transition: transform 0.2s, opacity 0.2s;
opacity: 0.85;
" onmouseover="this.style.opacity='1';this.style.transform='scale(1.1)'" onmouseout="this.style.opacity='0.85';this.style.transform='scale(1)'">
${word.text}
</span>
`).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 = `
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>设备ID</th>
<th>最后在线时间</th>
<th>对话次数</th>
</tr>
</thead>
<tbody>
${stats.deviceDetails.length ? stats.deviceDetails.map(d => `
<tr>
<td><code style="font-size:12px;background:var(--bg-light);padding:2px 6px;border-radius:4px">${d.deviceId}</code></td>
<td style="color:var(--text-secondary)">${d.lastOnline}</td>
<td><span class="badge">${d.conversationCount} 次</span></td>
</tr>
`).join('') : `
<tr>
<td colspan="3" style="padding:32px;text-align:center;color:var(--text-muted)">暂无设备数据</td>
</tr>
`}
</tbody>
</table>
</div>`;
}
// 刷新仪表盘
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;
}