1
0

feat: 添加直播统计可视化页面

This commit is contained in:
2026-02-20 23:03:26 +08:00
parent 4099d4f01a
commit 5f77234d04
4 changed files with 760 additions and 0 deletions

45
docs/echarts-v6.0.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

67
docs/index.html Normal file
View File

@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>直播统计可视化</title>
<link rel="stylesheet" href="./main.css">
</head>
<body>
<div class="container">
<h1>直播统计可视化</h1>
<div class="filter-section">
<label>开始日期:</label>
<input type="date" id="startDate">
<label>结束日期:</label>
<input type="date" id="endDate">
<button id="filterBtn">筛选</button>
</div>
<div class="stats-summary">
<div class="stat-card">
<h3>总直播时长</h3>
<div class="value" id="totalHours">0</div>
</div>
<div class="stat-card">
<h3>直播次数</h3>
<div class="value" id="totalCount">0</div>
</div>
<div class="stat-card">
<h3>不同内容数</h3>
<div class="value" id="uniqueContent">0</div>
</div>
<div class="stat-card">
<h3>平均时长</h3>
<div class="value" id="avgDuration">0</div>
</div>
</div>
<div class="charts-grid">
<div class="chart-container">
<h2>直播内容次数</h2>
<div id="contentChart" class="chart"></div>
</div>
<div class="chart-container">
<h2>每日直播次数</h2>
<div id="dailyChart" class="chart"></div>
</div>
<div class="chart-container">
<h2>直播时段分布</h2>
<div id="hourlyChart" class="chart"></div>
</div>
<div class="chart-container">
<h2>直播时长趋势</h2>
<div id="trendChart" class="chart"></div>
</div>
</div>
</div>
<div class="footer">
<span>本页面使用 TRAE 生成</span>
</div>
<script src="./echarts-v6.0.0.min.js"></script>
<script src="./main.js"></script>
</body>
</html>

177
docs/main.css Normal file
View File

@@ -0,0 +1,177 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #81C784 0%, #66BB6A 100%);
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 16px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 32px;
font-weight: 600;
}
.filter-section {
background: #f8f9fa;
padding: 20px;
border-radius: 12px;
margin-bottom: 30px;
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.filter-section label {
font-weight: 600;
color: #555;
font-size: 14px;
}
.filter-section input {
padding: 10px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.3s;
}
.filter-section input:focus {
border-color: #66BB6A;
}
.filter-section button {
padding: 10px 25px;
background: linear-gradient(135deg, #66BB6A 0%, #43A047 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: transform 0.2s, box-shadow 0.2s;
}
.filter-section button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 187, 106, 0.4);
}
.stats-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: linear-gradient(135deg, #66BB6A 0%, #43A047 100%);
color: white;
padding: 20px;
border-radius: 12px;
text-align: center;
}
.stat-card h3 {
font-size: 14px;
margin-bottom: 10px;
opacity: 0.9;
}
.stat-card .value {
font-size: 28px;
font-weight: 700;
}
.charts-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 25px;
margin-bottom: 25px;
}
.chart-container {
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.chart-container.full-width {
grid-column: 1 / -1;
}
.chart-container h2 {
font-size: 18px;
color: #333;
margin-bottom: 15px;
font-weight: 600;
}
.chart {
width: 100%;
height: 400px;
}
.footer {
text-align: center;
padding: 20px 0;
color: white;
font-size: 14px;
}
.chart.full-height {
height: 500px;
}
@media (max-width: 1024px) {
.charts-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
h1 {
font-size: 24px;
}
.filter-section {
flex-direction: column;
align-items: stretch;
}
.filter-section input,
.filter-section button {
width: 100%;
}
.stats-summary {
grid-template-columns: repeat(2, 1fr);
}
.chart {
height: 300px;
}
}

471
docs/main.js Normal file
View File

@@ -0,0 +1,471 @@
let allData = [];
let charts = {};
async function loadData() {
try {
let response = await fetch('../data.json');
let data = await response.json();
allData = data.timeline || [];
return allData;
} catch (error) {
console.error('加载数据失败:', error);
return [];
}
}
function parseDate(dateStr) {
return new Date(dateStr.replace(' ', 'T'));
}
function getDurationMinutes(startTime, endTime) {
let start = parseDate(startTime);
let end = parseDate(endTime);
return (end - start) / (1000 * 60);
}
function formatDate(date) {
let year = date.getFullYear();
let month = String(date.getMonth() + 1).padStart(2, '0');
let day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function filterDataByDateRange(data, startDate, endDate) {
if (!startDate && !endDate) return data;
return data.filter(item => {
let itemDate = parseDate(item.startTime);
if (startDate && itemDate < new Date(startDate)) return false;
if (endDate && itemDate > new Date(endDate + 'T23:59:59')) return false;
return true;
});
}
function extractContentItems(content) {
if (!content) return [];
return content.split('/').map(item => item.trim()).filter(item => item);
}
function analyzeContent(data) {
let contentStats = {};
let dailyStats = {};
let hourlyStats = {};
data.forEach(item => {
let duration = getDurationMinutes(item.startTime, item.endTime);
let startDate = parseDate(item.startTime);
let endDate = parseDate(item.endTime);
let startHour = startDate.getHours();
let startDay = formatDate(startDate);
let endDay = formatDate(endDate);
if (!dailyStats[startDay]) {
dailyStats[startDay] = { duration: 0, count: 0 };
}
dailyStats[startDay].count += 1;
if (startDay === endDay) {
dailyStats[startDay].duration += duration;
} else {
let endOfDay = new Date(startDate);
endOfDay.setHours(23, 59, 59, 999);
let firstDayDuration = (endOfDay - startDate) / (1000 * 60);
dailyStats[startDay].duration += firstDayDuration;
if (!dailyStats[endDay]) {
dailyStats[endDay] = { duration: 0, count: 0 };
}
dailyStats[endDay].count += 1;
let startOfNextDay = new Date(startDate);
startOfNextDay.setDate(startOfNextDay.getDate() + 1);
startOfNextDay.setHours(0, 0, 0, 0);
let secondDayDuration = (endDate - startOfNextDay) / (1000 * 60);
dailyStats[endDay].duration += secondDayDuration;
}
if (!hourlyStats[startHour]) {
hourlyStats[startHour] = { duration: 0, count: 0 };
}
hourlyStats[startHour].duration += duration;
hourlyStats[startHour].count += 1;
let contentItems = extractContentItems(item.content);
contentItems.forEach(content => {
if (!contentStats[content]) {
contentStats[content] = { duration: 0, count: 0 };
}
contentStats[content].duration += duration;
contentStats[content].count += 1;
});
});
return { contentStats, dailyStats, hourlyStats };
}
function initCharts() {
charts.contentChart = echarts.init(document.getElementById('contentChart'));
charts.dailyChart = echarts.init(document.getElementById('dailyChart'));
charts.hourlyChart = echarts.init(document.getElementById('hourlyChart'));
charts.trendChart = echarts.init(document.getElementById('trendChart'));
window.addEventListener('resize', () => {
Object.values(charts).forEach(chart => chart.resize());
});
}
function renderContentChart(contentStats) {
let sortedData = Object.entries(contentStats)
.sort((a, b) => b[1].count - a[1].count)
.slice(0, 15);
let option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function (params) {
let item = params[0];
return `${item.name}<br/>次数: ${item.value}`;
}
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
containLabel: true
},
xAxis: {
type: 'value',
name: '次数',
axisLabel: {
formatter: '{value} 次'
}
},
yAxis: {
type: 'category',
data: sortedData.map(d => d[0]),
inverse: true,
axisLabel: {
interval: 0,
width: 200,
overflow: 'truncate'
}
},
series: [{
name: '次数',
type: 'bar',
data: sortedData.map(d => d[1].count),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#66BB6A' },
{ offset: 1, color: '#43A047' }
])
}
}]
};
charts.contentChart.setOption(option);
}
function renderDailyChart(dailyStats) {
let sortedDates = Object.keys(dailyStats).sort();
let counts = sortedDates.map(date => dailyStats[date].count);
let option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function (params) {
let date = params[0].name;
let count = params[0].value;
return `${date}<br/>次数: ${count}`;
}
},
dataZoom: [
{
type: 'slider',
show: true,
xAxisIndex: [0],
start: 0,
end: 100
},
{
type: 'inside',
xAxisIndex: [0]
}
],
grid: {
left: '3%',
right: '4%',
bottom: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: sortedDates,
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: '次数',
axisLabel: {
formatter: '{value} 次'
}
},
series: [
{
name: '次数',
type: 'bar',
data: counts,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#66BB6A' },
{ offset: 1, color: '#43A047' }
])
}
}
]
};
charts.dailyChart.setOption(option);
}
function renderHourlyChart(hourlyStats) {
let hours = Array.from({ length: 24 }, (_, i) => i);
let durations = hours.map(h => hourlyStats[h] ? hourlyStats[h].duration : 0);
let counts = hours.map(h => hourlyStats[h] ? hourlyStats[h].count : 0);
let option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
formatter: function (params) {
let item0 = params[0];
let item1 = params[1];
let hour = item0 ? item0.name : '';
let duration = item0 ? item0.value : 0;
let count = item1 ? item1.value : 0;
let returns = `${hour}:00 - ${hour}:59<br/>时长: ${duration} 小时`;
if (count) {
returns += `<br/>次数: ${count}`;
}
return returns;
}
},
legend: {
data: ['时长(小时)', '次数'],
top: '8%'
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: hours.map(h => `${h}:00`),
boundaryGap: false
},
yAxis: [
{
type: 'value',
name: '时长(小时)',
position: 'left',
axisLabel: {
formatter: '{value}h'
}
},
{
type: 'value',
name: '次数',
position: 'right'
}
],
series: [
{
name: '时长(小时)',
type: 'line',
smooth: true,
data: durations.map(d => (d / 60).toFixed(2)),
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(102, 187, 106, 0.5)' },
{ offset: 1, color: 'rgba(102, 187, 106, 0.1)' }
])
},
itemStyle: {
color: '#4CAF50'
}
},
{
name: '次数',
type: 'bar',
yAxisIndex: 1,
data: counts,
itemStyle: {
color: '#81C784'
}
}
]
};
charts.hourlyChart.setOption(option);
}
function renderTrendChart(dailyStats) {
let sortedDates = Object.keys(dailyStats).sort();
let durations = sortedDates.map(date => dailyStats[date].duration);
let option = {
title: {
text: '直播时长趋势',
left: 'center'
},
tooltip: {
trigger: 'axis',
formatter: function (params) {
let date = params[0].name;
let duration = params[0].value;
return `${date}<br/>时长: ${duration} 小时`;
}
},
dataZoom: [
{
type: 'slider',
show: true,
xAxisIndex: [0],
start: 0,
end: 100
},
{
type: 'inside',
xAxisIndex: [0]
}
],
grid: {
left: '3%',
right: '4%',
bottom: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: sortedDates,
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: '时长(小时)',
axisLabel: {
formatter: '{value}h'
}
},
series: [{
name: '时长',
type: 'line',
smooth: true,
data: durations.map(d => (d / 60).toFixed(2)),
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(102, 187, 106, 0.5)' },
{ offset: 1, color: 'rgba(102, 187, 106, 0.1)' }
])
},
itemStyle: {
color: '#4CAF50'
},
}]
};
charts.trendChart.setOption(option);
}
function updateStats(data) {
let totalDuration = data.reduce((sum, item) => sum + getDurationMinutes(item.startTime, item.endTime), 0);
let totalHours = (totalDuration / 60).toFixed(1);
let totalCount = data.length;
let uniqueContents = new Set();
data.forEach(item => {
extractContentItems(item.content).forEach(content => uniqueContents.add(content));
});
let avgDuration = totalCount > 0 ? (totalDuration / totalCount / 60).toFixed(1) : 0;
document.getElementById('totalHours').textContent = totalHours;
document.getElementById('totalCount').textContent = totalCount;
document.getElementById('uniqueContent').textContent = uniqueContents.size;
document.getElementById('avgDuration').textContent = avgDuration;
}
function updateCharts(data) {
let { contentStats, dailyStats, hourlyStats } = analyzeContent(data);
renderContentChart(contentStats);
renderDailyChart(dailyStats);
renderHourlyChart(hourlyStats);
renderTrendChart(dailyStats);
updateStats(data);
}
async function init() {
await loadData();
initCharts();
updateCharts(allData);
let startDateInput = document.getElementById('startDate');
let endDateInput = document.getElementById('endDate');
let filterBtn = document.getElementById('filterBtn');
filterBtn.addEventListener('click', () => {
let startDate = startDateInput.value;
let endDate = endDateInput.value;
let filteredData = filterDataByDateRange(allData, startDate, endDate);
updateCharts(filteredData);
});
}
document.addEventListener('DOMContentLoaded', init);