1051 lines
43 KiB
JavaScript
1051 lines
43 KiB
JavaScript
/**
|
||
* 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;
|
||
}
|