/**
* 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 = `
`;
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 = `
| 设备ID |
最后在线时间 |
对话次数 |
${stats.deviceDetails.length ? stats.deviceDetails.map(d => `
${d.deviceId} |
${d.lastOnline} |
${d.conversationCount} 次 |
`).join('') : `
| 暂无设备数据 |
`}
`;
}
// 刷新仪表盘
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;
}