Files
zydi-web/frontend/web.html
T
2025-01-23 09:05:45 +00:00

2490 lines
92 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="cls-2.js" type="module"></script>
<title>智能视频分析系统</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
:root {
--primary-color: #2962ff;
--secondary-color: #82b1ff;
--background-color: #f8f9fa;
--sidebar-color: #ffffff;
--text-primary: #2c3e50;
--text-secondary: #7f8c8d;
--success-color: #4caf50;
--warning-color: #ff9800;
--danger-color: #f44336;
--border-radius: 12px;
--card-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
--transition: all 0.3s ease;
--border-color: #e0e0e0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
body {
background-color: var(--background-color);
color: var(--text-primary);
}
.container {
display: flex;
min-height: 100vh;
}
/* 侧边栏样式 */
.sidebar {
background-color: var(--sidebar-color);
padding: 24px;
box-shadow: var(--card-shadow);
position: fixed;
width: 280px;
height: 100vh;
overflow-y: auto;
top: 0;
left: 0;
z-index: 100;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 24px;
border-bottom: 1px solid #eee;
margin-bottom: 24px;
}
.logo img {
width: 40px;
height: 40px;
}
.logo h1 {
font-size: 20px;
font-weight: 600;
color: var(--primary-color);
}
.camera-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.camera-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background-color: #fff;
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--transition);
border: 1px solid #eee;
}
.camera-item:hover {
transform: translateY(-2px);
box-shadow: var(--card-shadow);
}
.camera-item.active {
background-color: var(--primary-color);
color: white;
}
.camera-status {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--success-color);
}
/* 主内容区样式 */
.main-content {
flex: 1;
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
margin-left: 280px;
min-height: 100vh;
width: calc(100% - 280px);
}
/* 顶部工具栏 */
.toolbar {
display: flex;
gap: 16px;
background-color: white;
padding: 16px;
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
}
.search-box {
flex: 1;
position: relative;
}
.search-box i {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
}
.search-box input {
width: 100%;
padding: 12px 12px 12px 40px;
border: 1px solid #eee;
border-radius: var(--border-radius);
font-size: 14px;
transition: var(--transition);
}
.search-box input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(41, 98, 255, 0.1);
}
.date-picker input {
padding: 12px;
border: 1px solid #eee;
border-radius: var(--border-radius);
font-size: 14px;
}
.button {
padding: 12px 24px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--transition);
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.button i {
display: flex;
align-items: center;
font-size: 20px;
}
.button:hover {
background-color: var(--secondary-color);
}
/* 时间轴样式 */
.timeline-card {
background-color: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.timeline-content {
position: relative;
overflow-x: hidden;
background: white;
border-radius: var(--border-radius);
padding: 20px;
border: 1px solid var(--border-color);
}
.timeline {
position: relative;
width: 100%;
height: 100px;
background: var(--background-color);
border: 1px solid var(--border-color);
border-radius: 4px;
margin-top: 30px;
overflow: visible;
}
.hour-marker {
position: absolute;
width: 1px;
height: 100%;
background: rgba(0, 0, 0, 0.1);
top: 0;
}
.hour-label {
position: absolute;
top: -25px;
transform: translateX(-50%);
font-size: 12px;
color: var(--text-secondary);
}
.event {
position: absolute;
height: 8px;
transform: translateX(-50%);
border-radius: 3px;
cursor: pointer;
transition: all 0.2s ease;
opacity: 0.85;
z-index: 1;
}
.event:hover {
opacity: 1;
transform: translateX(-50%) scaleY(1.5);
z-index: 2;
}
.timeline-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.zoom-controls button {
background: white;
border: 1px solid var(--border-color);
color: var(--text-color);
padding: 8px 15px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
margin-right: 10px;
font-weight: 500;
}
.zoom-controls button:hover {
background: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
/* 滚动条样式 */
.timeline-content::-webkit-scrollbar {
height: 8px;
}
.timeline-content::-webkit-scrollbar-track {
background: var(--secondary-bg);
border-radius: 4px;
}
.timeline-content::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
}
.timeline-content::-webkit-scrollbar-thumb:hover {
background: #bbb;
}
/* 数据卡片网格 */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.stat-card {
background-color: white;
padding: 24px;
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
}
.stat-card h3 {
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 8px;
}
.stat-value {
font-size: 32px;
font-weight: 600;
color: var(--primary-color);
}
/* 图表容器 */
.chart-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
.chart-card {
background-color: white;
padding: 24px;
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
}
.chart-container {
height: 300px;
margin-top: 20px;
}
/* 事件列表 */
.event-list {
background-color: white;
padding: 24px;
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
}
.event-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.event-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #eee;
transition: var(--transition);
}
.event-list-item:hover {
background-color: #f8f9fa;
}
.event-info {
display: flex;
align-items: center;
gap: 12px;
}
.event-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--primary-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
}
.event-details {
display: flex;
flex-direction: column;
}
.event-time {
font-size: 14px;
color: var(--text-secondary);
}
.confidence-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
background-color: #e3f2fd;
color: var(--primary-color);
}
.highlight {
background-color: rgba(41, 98, 255, 0.1);
border-radius: 4px;
padding: 2px 4px;
}
/* 新增样式 */
.sidebar-divider {
height: 1px;
background-color: #eee;
margin: 24px 0;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--transition);
border: 1px solid #eee;
}
.nav-item:hover {
transform: translateY(-2px);
box-shadow: var(--card-shadow);
background-color: rgba(41, 98, 255, 0.1);
}
.nav-item.active {
background-color: var(--primary-color);
color: white;
}
.summary-section {
background-color: white;
border-radius: var(--border-radius);
padding: 24px;
box-shadow: var(--card-shadow);
margin-bottom: 24px;
}
.summary-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.insight-card {
background-color: #f8f9fa;
border-radius: var(--border-radius);
padding: 20px;
margin-bottom: 16px;
}
.insight-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--primary-color);
margin-bottom: 12px;
}
.recommendation {
background-color: #e3f2fd;
border-left: 4px solid var(--primary-color);
padding: 16px;
margin-top: 16px;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.trend-indicator {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
}
.trend-up {
color: var(--success-color);
}
.trend-down {
color: var(--danger-color);
}
/* 添加图例样式 */
.legend {
display: flex;
gap: 15px;
padding: 15px;
justify-content: center;
flex-wrap: wrap;
background: var(--secondary-bg);
border-radius: 8px;
margin-top: 15px;
}
.legend-item {
display: flex;
align-items: center;
font-size: 13px;
gap: 5px;
padding: 4px 6px;
background: white;
border-radius: 4px;
border: 1px solid var(--border-color);
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 3px;
}
.legend-container {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
padding: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: white;
border-radius: 20px;
border: 1px solid var(--border-color);
font-size: 13px;
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 3px;
}
.legend-item {
cursor: pointer;
opacity: 0.7;
transition: all 0.3s ease;
}
.legend-item.active {
opacity: 1;
}
.legend-item:not(.active) {
opacity: 0.4;
}
.legend-item:not(.active) .legend-color {
background-color: #ccc !important;
}
.recommendation-section {
margin-bottom: 15px;
}
.recommendation-section ul {
margin-top: 5px;
padding-left: 20px;
}
.recommendation-section li {
margin-bottom: 5px;
}
#total-events, #peak-hours, #abnormal-events {
line-height: 1.6;
}
.insight-card div {
margin-bottom: 10px;
}
.insight-card strong {
color: var(--primary-color);
}
.tooltip {
position: fixed;
background: white;
color: var(--text-color);
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid var(--border-color);
min-width: 150px;
}
.tooltip div {
margin: 2px 0;
line-height: 1.4;
}
.tooltip div:first-child {
color: var(--primary-color);
border-bottom: 1px solid #eee;
padding-bottom: 2px;
margin-bottom: 4px;
}
.recommendation-section {
margin-bottom: 15px;
}
.recommendation-section ul {
margin-top: 5px;
padding-left: 20px;
}
.recommendation-section li {
margin-bottom: 5px;
}
#total-events, #peak-hours, #abnormal-events {
line-height: 1.6;
}
.insight-card div {
margin-bottom: 10px;
}
.insight-card strong {
color: var(--primary-color);
}
/* 行为分析样式 */
#behavior-analysis {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 15px;
}
.behavior-category {
background-color: #f8f9fa;
border-radius: var(--border-radius);
padding: 15px;
border-left: 4px solid var(--primary-color);
}
.behavior-category h4 {
color: var(--primary-color);
margin-bottom: 10px;
font-size: 16px;
}
.behavior-category div {
margin-bottom: 8px;
font-size: 14px;
line-height: 1.5;
}
.behavior-category div:last-child {
margin-bottom: 0;
}
.behavior-category strong {
color: var(--text-secondary);
margin-right: 5px;
}
@media (max-width: 768px) {
#behavior-analysis {
grid-template-columns: 1fr;
}
}
.header-controls {
display: flex;
align-items: center;
gap: 16px;
}
.download-report {
display: flex;
align-items: center;
gap: 8px;
background-color: var(--primary-color);
}
.download-report:hover {
background-color: #2962ff;
}
.download-report i {
font-size: 20px;
}
.rotating {
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.toolbar button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* 添加人员图例样式 */
.person-legend {
margin-top: 15px;
padding: 12px;
background: var(--background-color);
border-radius: 8px;
}
.person-legend-container {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
}
.person-legend-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: white;
border-radius: 20px;
border: 1px solid var(--border-color);
font-size: 13px;
cursor: pointer;
transition: all 0.3s ease;
}
.person-legend-item .person-icon {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--primary-color);
color: white;
font-size: 12px;
}
.person-legend-item.active {
background-color: var(--primary-color);
color: white;
}
.person-legend-item.active .person-icon {
background-color: white;
color: var(--primary-color);
}
.person-legend-item:not(.active) {
opacity: 0.7;
}
</style>
</head>
<body>
<div class="container">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="logo">
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%232962ff'%3E%3Cpath d='M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z'/%3E%3C/svg%3E" alt="Logo">
<h1>智能视频分析</h1>
</div>
<div class="camera-list">
<div class="camera-item active">
<span class="camera-status"></span>
<span>摄像头 A01</span>
</div>
<div class="camera-item">
<span class="camera-status"></span>
<span>摄像头 B02</span>
</div>
<div class="camera-item">
<span class="camera-status"></span>
<span>摄像头 camera001</span>
</div>
<div class="camera-item">
<span class="camera-status"></span>
<span>摄像头 camera002</span>
</div>
<div class="camera-item">
<span class="camera-status"></span>
<span>摄像头 camera003</span>
</div>
</div>
<div class="sidebar-divider"></div>
<div class="nav-item daily-summary">
<i class="material-icons">analytics</i>
<span>每日行为分析</span>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<!-- 工具栏 -->
<div class="toolbar">
<div class="search-box">
<i class="material-icons">search</i>
<input type="text" placeholder="输入关键字搜索(多个关键字用;分隔)...">
</div>
<div class="date-picker">
<input type="date">
</div>
<button class="button" onclick="refreshData()">
<i class="material-icons">refresh</i>
刷新
</button>
<button class="button">
<i class="material-icons">filter_list</i>
筛选
</button>
</div>
<!-- 数据卡片 -->
<div class="dashboard-grid">
<div class="stat-card">
<h3>今日识别行为</h3>
<div class="stat-value">--</div>
</div>
<div class="stat-card">
<h3>异常行为</h3>
<div class="stat-value">--</div>
</div>
<div class="stat-card">
<h3>人数</h3>
<div class="stat-value">--</div>
</div>
</div>
<!-- 时间轴 -->
<div class="timeline-card">
<div class="timeline-controls">
<div class="zoom-controls">
<button onclick="adjustZoom(1)">放大</button>
<button onclick="adjustZoom(-1)">缩小</button>
<button onclick="adjustZoom('reset')">还原</button>
</div>
</div>
<div class="timeline-content">
<div class="timeline" id="timeline">
<!-- 时间标记和事件将通过 JavaScript 动态添加 -->
</div>
</div>
<!-- 添加人员图例容器 -->
<div class="person-legend" id="person-legend"></div>
<!-- 原有的行为图例 -->
<div class="legend" id="legend"></div>
</div>
<!-- 图表区域 -->
<div class="chart-grid">
<div class="chart-card">
<div class="card-header">
<h2>行为分布</h2>
</div>
<div class="chart-container" id="behaviorChart"></div>
</div>
<div class="chart-card">
<div class="card-header">
<h2>时段分析</h2>
</div>
<div class="chart-container" id="timeChart"></div>
</div>
</div>
<!-- 事件列表 -->
<div class="event-list">
<div class="event-list-header">
<h2>最新事件</h2>
</div>
<!-- 事件将通过 JavaScript 动态添加 -->
</div>
<!-- 每日总结分析 -->
<div class="summary-section">
<div class="summary-header">
<h2>每日行为分析</h2>
<div class="header-controls">
<div class="date-picker">
<input type="date">
</div>
<button class="button download-report" onclick="downloadReport()">
<i class="material-icons">download</i>
下载报告
</button>
</div>
</div>
<div class="insight-card">
<div class="insight-title">
<i class="material-icons">trending_up</i>
<h3>整体活动趋势</h3>
</div>
<p id="total-events">分析中...</p>
</div>
<div class="insight-card">
<div class="insight-title">
<i class="material-icons">access_time</i>
<h3>高峰时段分析</h3>
</div>
<p id="peak-hours">分析中...</p>
<div class="chart-container" id="peakTimeChart" style="height: 200px;"></div>
</div>
<div class="insight-card">
<div class="insight-title">
<i class="material-icons">warning</i>
<h3>异常行为分析</h3>
</div>
<p id="abnormal-events">分析中...</p>
</div>
<div class="insight-card">
<div class="insight-title">
<i class="material-icons">analytics</i>
<h3>行为分析</h3>
</div>
<div id="behavior-analysis">分析中...</div>
</div>
<div class="insight-card">
<div class="insight-title">
<i class="material-icons">tips_and_updates</i>
<h3>建议</h3>
</div>
<p id="recommendation-text">分析中...</p>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script>
const API_BASE_URL = 'https://dev.obscura.work/web';
// 获取摄像头数据
async function fetchCameraData(cameraId, date) {
try {
// 如果没有传入日期,使用当前日期
const currentDate = date || new Date().toISOString().split('T')[0].replace(/-/g, '');
const response = await fetch(`${API_BASE_URL}/${cameraId}/data?date=${currentDate}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('Error fetching camera data:', error);
return null;
}
}
// 解析视频分析数据
function parseVideoAnalysis(data) {
const behaviors = new Map();
const timeDistribution = new Map();
let totalEvents = 0;
let abnormalCount = 0;
// 初始化24小时时间分布
for (let i = 0; i < 24; i++) {
timeDistribution.set(i, 0);
}
// 遍历所有摄像头数据
Object.values(data).forEach(cameraData => {
Object.values(cameraData).forEach(record => {
if (record.video_analysis?.['qwen-7B']?.extracted_info) {
const info = record.video_analysis['qwen-7B'].extracted_info;
const timestamp = new Date(record.timestamp);
// 统计行为
info.actions.forEach(action => {
behaviors.set(action, (behaviors.get(action) || 0) + 1);
totalEvents++;
// 异常行为判断
if (isAbnormalBehavior(action)) {
abnormalCount++;
}
});
// 记录时间分布
const hour = timestamp.getHours();
timeDistribution.set(hour, (timeDistribution.get(hour) || 0) + 1);
}
});
});
return {
behaviors: Object.fromEntries(behaviors),
timeDistribution: Object.fromEntries(timeDistribution),
totalEvents,
abnormalCount
};
}
// 添加异常行为判断函数
function isAbnormalBehavior(action) {
// 定义异常行为关键词列表
const abnormalKeywords = [
'异常',
'可疑',
'打架',
'斗殴',
'摔倒',
'晕倒',
'昏倒',
'跌倒',
'滑倒',
'摔',
'踢',
'受伤',
'暴力',
'攻击',
'威胁',
'破坏',
'偷窃',
'抢夺',
'游荡',
'徘徊',
'尾随',
'骚扰'
];
// 检查行为是否包含任何异常关键词
return abnormalKeywords.some(keyword => action.includes(keyword));
}
// 更新统计卡片
function updateStatCards(stats, faceData) {
// 只更新两个卡片
document.querySelector('.stat-card:nth-child(1) .stat-value').textContent = stats.totalEvents;
document.querySelector('.stat-card:nth-child(2) .stat-value').textContent = stats.abnormalCount;
// 统计不同的人数
if (faceData?.data) {
const uniquePeople = new Set();
// 遍历所有摄像头数据
Object.values(faceData.data).forEach(cameraData => {
Object.values(cameraData).forEach(record => {
if (record?.face_analysis) {
// 遍历face_analysis中的所有人脸数据
Object.entries(record.face_analysis).forEach(([personId, faceInfo]) => {
if (personId && personId !== 'unknown') {
uniquePeople.add(personId);
}
});
}
});
});
document.querySelector('.stat-card:nth-child(3) .stat-value').textContent = uniquePeople.size;
} else {
document.querySelector('.stat-card:nth-child(3) .stat-value').textContent = '--';
}
}
// 添加全局变量
let currentZoom = 1;
let lastFetchedData = null;
let defaultTimelineWidth = 0;
let behaviorColors = new Map(); // 将 behaviorColors 提升为全局变量
let autoRefreshTimer = null;
function adjustZoom(delta) {
const timeline = document.getElementById('timeline');
const timelineContent = document.querySelector('.timeline-content');
if (delta === 'reset') {
// 重置缩放
currentZoom = 1;
timeline.style.width = '100%';
timelineContent.style.overflowX = 'hidden';
} else {
const newZoom = currentZoom + delta * 0.2;
if (newZoom >= 1 && newZoom <= 3) { // 限制最小缩放为1(不允许缩小)
currentZoom = newZoom;
// 计算新的时间轴宽度
const baseWidth = defaultTimelineWidth || timeline.parentElement.offsetWidth;
const newWidth = baseWidth * currentZoom;
timeline.style.width = `${newWidth}px`;
timelineContent.style.overflowX = currentZoom > 1 ? 'auto' : 'hidden';
}
}
if (lastFetchedData) {
updateTimeline(lastFetchedData);
}
}
// 修改处理人脸数据的函数
function processFaceData(faceData) {
const faceMap = new Map();
const personSet = new Set(['未知','unknown']); // 将 unknown 添加到人员集合中
if (faceData?.data) {
Object.values(faceData.data).forEach(imageData => {
Object.entries(imageData).forEach(([imageName, data]) => {
if (data.face_analysis) {
const timestamp = data.timestamp;
const faces = [];
// 检查是否有人脸分析数据
if (data.face_analysis && Object.keys(data.face_analysis).length > 0) {
// 遍历face_analysis中的所有人脸数据,包括 unknown
Object.entries(data.face_analysis).forEach(([personId, faceInfo]) => {
faces.push({
id: personId,
similarity: faceInfo.similarity,
fileName: faceInfo.file_name
});
personSet.add(personId);
});
} else {
// 如果没有人脸分析数据,添加"未知"
faces.push({
id: '未知',
similarity: null,
fileName: null
});
}
// 保存所有人脸数据到faceMap
if (faces.length > 0) {
faceMap.set(timestamp, faces);
}
}
});
});
}
return {
faceMap,
uniquePersons: Array.from(personSet)
};
}
// 修改更新时间轴的函数
function updateTimeline(data, faceData) {
const timeline = document.getElementById('timeline');
timeline.innerHTML = '';
// 处理人脸数据
const { faceMap, uniquePersons } = processFaceData(faceData);
// 更新人员图例
updatePersonLegend(uniquePersons);
// 存储默认宽度
if (!defaultTimelineWidth) {
defaultTimelineWidth = timeline.parentElement.offsetWidth;
}
// 计算时间轴的总宽度(像素)
const timelineWidth = timeline.offsetWidth;
// 定义行为类别的垂直位置(从上到下)
const categoryRows = {
'基础动作': 0, // 第一行
'日常生活': 1, // 第二行
'工作学习': 2, // 第三行
'社交活动': 3, // 第四行
'其他': 4, // 第五行
'异常行为': 5 // 第六行
};
// 修改行为分类的判断逻辑
function getBehaviorCategory(behavior) {
// 遍历 window.actions 中的所有类别
for (const [mainCategory, subCategories] of Object.entries(window.actions)) {
// 遍历每个主类别下的子类别
for (const [subCategory, behaviors] of Object.entries(subCategories)) {
// 使用完全匹配来检查行为
if (behaviors.includes(behavior)) {
return mainCategory;
}
}
}
return '其他';
}
// 添加小时刻度
for (let i = 0; i <= 24; i++) {
const marker = document.createElement('div');
marker.className = 'hour-marker';
marker.style.left = `${(i / 24) * 100}%`;
const label = document.createElement('div');
label.className = 'hour-label';
label.textContent = i.toString().padStart(2, '0') + ':00';
label.style.left = `${(i / 24) * 100}%`;
timeline.appendChild(marker);
timeline.appendChild(label);
}
// 处理事件数据
let eventCount = 0;
if (data) {
Object.values(data).forEach(cameraData => {
Object.entries(cameraData).forEach(([timestamp, value]) => {
if (value.video_analysis?.['qwen-7B']?.extracted_info) {
const eventTime = new Date(value.timestamp);
const hour = eventTime.getHours();
const minute = eventTime.getMinutes();
const actions = value.video_analysis['qwen-7B'].extracted_info.actions;
if (Array.isArray(actions)) {
eventCount += actions.length;
}
// 获取该时间点的人脸数据
const faces = faceMap.get(value.timestamp) || [];
const personIds = faces.length > 0
? faces.map(face => face.id).join(', ')
: '未知';
if (Array.isArray(actions) && actions.length > 0) {
actions.forEach(behavior => {
const category = getBehaviorCategory(behavior);
const event = document.createElement('div');
event.className = 'event';
event.setAttribute('data-behavior', behavior);
event.setAttribute('data-category', category);
event.setAttribute('data-person', personIds);
event.setAttribute('data-faces', JSON.stringify(faces));
const timePercentage = ((hour * 60 + minute) / (24 * 60)) * 100;
event.style.left = `${timePercentage}%`;
const rowIndex = categoryRows[category];
const baseOffset = 20;
const rowSpacing = 8;
event.style.top = `${baseOffset + (rowIndex * rowSpacing)}px`;
const baseWidth = 4;
event.style.width = `${baseWidth * currentZoom}px`;
event.style.backgroundColor = getBehaviorColor(behavior);
const time = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
event.setAttribute('data-time', time);
event.addEventListener('mouseover', showEnhancedTooltip);
event.addEventListener('mouseout', hideTooltip);
timeline.appendChild(event);
});
}
}
});
});
}
// 更新图例
updateLegend(behaviorColors);
}
// 添加筛选状态管理
let activeFilters = {
persons: new Set(), // 当前激活的人员
categories: new Set() // 当前激活的行为类别
};
function filterTimelineEvents() {
const events = document.querySelectorAll('.timeline .event');
// 获取激活的人员和行为类别
const activePersons = Array.from(document.querySelectorAll('.person-legend-item.active'))
.map(item => item.querySelector('span').textContent);
const activeCategories = Array.from(document.querySelectorAll('.legend-item.active'))
.map(item => item.querySelector('span').textContent);
let visibleCount = 0;
events.forEach(event => {
const eventPersons = event.getAttribute('data-person').split(', ');
const eventBehavior = event.getAttribute('data-behavior');
// 如果没有任何筛选条件被激活,显示所有事件
if (activePersons.length === 0 && activeCategories.length === 0) {
event.style.display = 'block';
event.style.opacity = '1';
visibleCount++;
return;
}
// 检查人员匹配
const matchesPerson = activePersons.length === 0 ||
eventPersons.some(person => activePersons.includes(person));
// 检查行为类别匹配
const matchesCategory = activeCategories.length === 0 ||
activeCategories.includes(eventBehavior);
if (matchesPerson && matchesCategory) {
event.style.display = 'block';
event.style.opacity = '1';
visibleCount++;
} else {
event.style.display = 'none';
event.style.opacity = '0';
}
});
}
function updateLegend(behaviorColors) {
const legend = document.getElementById('legend');
legend.innerHTML = '';
const sortedBehaviors = Array.from(behaviorColors.entries())
.sort((a, b) => a[0].localeCompare(b[0], 'zh'));
const legendContainer = document.createElement('div');
legendContainer.className = 'legend-container';
sortedBehaviors.forEach(([behavior, color]) => {
const legendItem = document.createElement('div');
legendItem.className = 'legend-item active';
legendItem.innerHTML = `
<div class="legend-color" style="background: ${color}"></div>
<span>${behavior}</span>
`;
legendItem.addEventListener('click', () => {
legendItem.classList.toggle('active');
filterTimelineEvents();
});
legendContainer.appendChild(legendItem);
});
legend.appendChild(legendContainer);
}
function updatePersonLegend(persons) {
const personLegend = document.getElementById('person-legend');
personLegend.innerHTML = '';
const legendContainer = document.createElement('div');
legendContainer.className = 'person-legend-container';
persons.sort().forEach(person => {
const legendItem = document.createElement('div');
legendItem.className = 'person-legend-item active';
legendItem.innerHTML = `
<div class="person-icon">${person.charAt(0).toUpperCase()}</div>
<span>${person}</span>
`;
legendItem.addEventListener('click', () => {
legendItem.classList.toggle('active');
filterTimelineEvents();
});
legendContainer.appendChild(legendItem);
});
personLegend.appendChild(legendContainer);
}
// 添加按人员筛选时间轴事件的函数
function filterTimelineByPerson() {
const events = document.querySelectorAll('.timeline .event');
const activePersons = new Set(
Array.from(document.querySelectorAll('.person-legend-item.active'))
.map(item => item.querySelector('span').textContent)
);
events.forEach(event => {
const person = event.getAttribute('data-person');
if (activePersons.has(person)) {
event.style.display = 'block';
} else {
event.style.display = 'none';
}
});
}
// 添加随机颜色生成函数
function getRandomColor() {
// 预定义的备用颜色
const fallbackColors = [
'#F6DE00', // 黄色
'#8470FF', // 紫色
'#00BCD4', // 青色
'#F4A460', // 深橙色
'#607D8B', // 蓝灰色
'#4CAF50' // 翠绿色
];
return fallbackColors[Math.floor(Math.random() * fallbackColors.length)];
}
// 修改获取行为颜色的逻辑
function getBehaviorColor(behavior) {
// 如果行为已经有对应的颜色,直接返回
if (behaviorColors.has(behavior)) {
return behaviorColors.get(behavior);
}
// 首先尝试从 cls 文件中匹配颜色
if (window.actions && window.actionColors) {
for (const [mainCategory, subCategories] of Object.entries(window.actions)) {
for (const [subCategory, behaviors] of Object.entries(subCategories)) {
if (behaviors.includes(behavior)) {
const color = window.actionColors[subCategory];
if (color) {
behaviorColors.set(behavior, color);
return color;
}
}
}
}
}
// 如果在 cls 文件中找不到匹配的颜色,使用预设的备用颜色
const color = getRandomColor();
behaviorColors.set(behavior, color);
return color;
}
// 更新图表
function updateCharts(stats) {
// 行为分布图表
const behaviorChart = echarts.init(document.getElementById('behaviorChart'));
const behaviorData = Object.entries(stats.behaviors).map(([name, value]) => ({
name,
value
}));
behaviorChart.setOption({
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
type: 'scroll', // 启用滚动
orient: 'vertical', // 改为垂直布局
right: 10, // 位于右侧
top: 'center', // 垂直居中
height: '80%', // 限制图例高度
textStyle: {
fontSize: 12 // 减小字体大小
},
pageButtonPosition: 'end', // 翻页按钮位置
pageButtonGap: 5, // 翻页按钮间距
pageFormatter: '{current}/{total}', // 页码格式
pageIconSize: 12, // 翻页按钮大小
},
series: [{
name: '行为分布',
type: 'pie',
radius: ['40%', '70%'],
center: ['40%', '50%'], // 将饼图向左移动
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '20',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: behaviorData
}]
});
// 时段分析图表
const timeChart = echarts.init(document.getElementById('timeChart'));
const hours = Array.from({length: 24}, (_, i) => `${i}:00`);
const timeData = hours.map(hour => stats.timeDistribution[parseInt(hour)] || 0);
timeChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: hours,
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value'
},
series: [{
name: '行为数量',
type: 'bar',
barWidth: '60%',
data: timeData,
itemStyle: {
color: '#2962ff'
}
}]
});
}
// 更新事件列表
function updateEventList(behaviorData, faceData) {
const eventList = document.querySelector('.event-list');
const eventListContent = document.createElement('div');
const faceMap = new Map();
// 处理人脸数据
if (faceData?.data) {
Object.values(faceData.data).forEach(cameraData => {
Object.entries(cameraData).forEach(([key, value]) => {
if (value.face_analysis) {
const timestamp = value.timestamp;
faceMap.set(timestamp, value.face_analysis);
}
});
});
}
// 处理行为数据并合并人脸数据
const events = [];
Object.values(behaviorData).forEach(cameraData => {
Object.entries(cameraData).forEach(([key, value]) => {
if (value.video_analysis?.['qwen-7B']?.extracted_info) {
const timestamp = value.timestamp;
const faceInfo = faceMap.get(timestamp);
// 处理人员名称显示
let personNames = '未知';
if (faceInfo) {
const names = Object.keys(faceInfo).sort();
if (names.length === 1) {
personNames = names[0]; // 单人直接显示
} else if (names.length > 1) {
personNames = names.join('; '); // 多人用分号连接
}
}
events.push({
type: 'behavior',
timestamp: new Date(timestamp),
info: value.video_analysis['qwen-7B'].extracted_info,
personNames: personNames
});
}
});
});
// 按时间降序排序
events.sort((a, b) => b.timestamp - a.timestamp);
// 显示最新的5个事件
events.slice(0, 5).forEach(event => {
const eventItem = document.createElement('div');
eventItem.className = 'event-list-item';
eventItem.innerHTML = `
<div class="event-info">
<div class="event-icon">
<i class="material-icons">${getActionIcon(event.info.actions[0])}</i>
</div>
<div class="event-details">
<strong>${event.info.actions.join(', ')}</strong>
<span class="event-time">${event.timestamp.toLocaleTimeString()}</span>
</div>
</div>
<div class="event-features">
<span class="confidence-badge">人数: ${event.info.num_people}</span>
<span class="confidence-badge">人员: ${event.personNames}</span>
</div>
`;
eventListContent.appendChild(eventItem);
});
// 清除现有事件列表并添加新的
const oldContent = eventList.querySelector('.event-list-header').nextElementSibling;
if (oldContent) {
oldContent.remove();
}
eventList.appendChild(eventListContent);
}
// 获取行为对应的图标
function getActionIcon(action) {
// 首先检查 action 是否存在
if (!action) {
return 'help_outline'; // 返回默认图标
}
const iconMap = {
'走': 'directions_walk',
'站': 'accessibility',
'跑': 'directions_run',
'坐': 'event_seat',
'工作': 'computer',
'吃': 'restaurant',
'喝水': 'local_drink'
};
// 现在安全地使用 includes
for (const [key, icon] of Object.entries(iconMap)) {
if (action.includes(key)) {
return icon;
}
}
return 'help_outline'; // 如果没有匹配的图标,返回默认图标
}
// 初始化页面
async function initPage() {
// 绑定摄像头切换事件
const cameraItems = document.querySelectorAll('.camera-item');
cameraItems.forEach((item, index) => {
item.addEventListener('click', async () => {
// 更新UI状态
cameraItems.forEach(cam => cam.classList.remove('active'));
item.classList.add('active');
// 设置日期为今天
const today = new Date().toISOString().split('T')[0];
const toolbarDatePicker = document.querySelector('.toolbar input[type="date"]');
if (toolbarDatePicker) {
toolbarDatePicker.value = today;
}
// 获取摄像头文本内容
const cameraText = item.querySelector('span:last-child').textContent;
// 从文本内容中提取摄像头编号(例如:"摄像头 A01" -> "A01"
const cameraId = cameraText.replace('摄像头 ', '').trim();
// 重新加载数据
await loadCameraData(cameraId);
});
});
// 绑定工具栏日期选择器事件(摄像头页面)
const toolbarDatePicker = document.querySelector('.toolbar input[type="date"]');
if (toolbarDatePicker) {
toolbarDatePicker.valueAsDate = new Date();
toolbarDatePicker.addEventListener('change', async () => {
const activeCamera = document.querySelector('.camera-item.active');
if (activeCamera) {
const cameraText = activeCamera.querySelector('span:last-child').textContent;
const cameraId = cameraText.replace('摄像头 ', '').trim();
await loadCameraData(cameraId, toolbarDatePicker.value.replace(/-/g, ''));
}
});
}
// 绑定搜索框事件
const searchInput = document.querySelector('.search-box input');
searchInput.addEventListener('input', handleSearch);
// 初始加载第一个摄像头的数据
const firstCamera = document.querySelector('.camera-item span:last-child');
const firstCameraId = firstCamera ? firstCamera.textContent.replace('摄像头 ', '').trim() : 'A01';
await loadCameraData(firstCameraId);
// 启动自动刷新
startAutoRefresh();
// 绑定窗口大小改变事件
window.addEventListener('resize', function() {
const charts = document.querySelectorAll('.chart-container');
charts.forEach(container => {
const chart = echarts.getInstanceByDom(container);
if (chart) {
chart.resize();
}
});
});
}
// 修改 loadCameraData 函数,传递人脸数据到 updateTimeline
async function loadCameraData(cameraId, date) {
clearPageData();
// 并行获取行为和人脸数据
const [behaviorData, faceData] = await Promise.all([
fetchCameraData(cameraId, date),
fetchFaceData(cameraId, date)
]);
if (behaviorData?.data) {
lastFetchedData = behaviorData.data;
const stats = parseVideoAnalysis(behaviorData.data);
updateStatCards(stats, faceData);
updateTimeline(behaviorData.data, faceData); // 传入人脸数据
updateCharts(stats);
updateEventList(behaviorData.data, faceData);
}
}
// 处理搜索
function handleSearch(e) {
const searchText = e.target.value.trim();
// 同时支持英文分号和中文分号
const searchKeywords = searchText.split(/[;]/).map(keyword => keyword.trim().toLowerCase()).filter(keyword => keyword !== '');
// 处理时间轴事件
const timelineEvents = document.querySelectorAll('.event');
timelineEvents.forEach(event => {
const behavior = event.getAttribute('data-behavior');
if (searchText === '') {
// 如果搜索框为空,显示所有事件
event.style.display = 'block';
event.style.opacity = '1';
event.style.transform = 'translateX(-50%)';
event.style.height = '8px'; // 恢复默认高度
} else {
// 检查是否匹配任一关键字
const isMatch = searchKeywords.some(keyword =>
behavior.toLowerCase().includes(keyword)
);
if (isMatch) {
event.style.display = 'block';
event.style.opacity = '1';
event.style.transform = 'translateX(-50%) scaleY(1.5)';
event.style.zIndex = '2';
} else {
event.style.display = 'none';
}
}
});
// 处理事件列表
const eventItems = document.querySelectorAll('.event-list-item');
eventItems.forEach(item => {
const behaviorElement = item.querySelector('.event-details strong');
const timeElement = item.querySelector('.event-time');
const behaviorText = behaviorElement.textContent;
const timeText = timeElement.textContent;
if (searchText === '') {
// 如果搜索框为空,显示所有事件
item.style.display = 'flex';
behaviorElement.textContent = behaviorText;
timeElement.textContent = timeText;
} else {
// 检查是否匹配任一关键字
const isMatch = searchKeywords.some(keyword =>
behaviorText.toLowerCase().includes(keyword) ||
timeText.toLowerCase().includes(keyword)
);
if (isMatch) {
item.style.display = 'flex';
// 高亮匹配的关键字
let highlightedBehavior = behaviorText;
let highlightedTime = timeText;
searchKeywords.forEach(keyword => {
if (keyword) {
const regex = new RegExp(`(${keyword})`, 'gi');
highlightedBehavior = highlightedBehavior.replace(regex, '<span class="highlight">$1</span>');
highlightedTime = highlightedTime.replace(regex, '<span class="highlight">$1</span>');
}
});
behaviorElement.innerHTML = highlightedBehavior;
timeElement.innerHTML = highlightedTime;
} else {
item.style.display = 'none';
}
}
});
}
function initPageSwitching() {
const dailySummaryNav = document.querySelector('.daily-summary');
const cameraItems = document.querySelectorAll('.camera-item');
const regularContent = document.querySelectorAll('.dashboard-grid, .timeline-card, .chart-grid, .event-list');
const summarySection = document.querySelector('.summary-section');
const toolbar = document.querySelector('.toolbar');
// 初始状态设置
summarySection.style.display = 'none';
// 获取前一天的日期
function getYesterday() {
const date = new Date();
date.setDate(date.getDate() - 1);
return date.toISOString().split('T')[0];
}
dailySummaryNav.addEventListener('click', async function() {
// 隐藏常规内容
regularContent.forEach(item => item.style.display = 'none');
// 隐藏工具栏
toolbar.style.display = 'none';
// 显示总结内容
summarySection.style.display = 'block';
// 更新导航状态
cameraItems.forEach(item => item.classList.remove('active'));
dailySummaryNav.classList.add('active');
// 获取当前选择的日期
const datePicker = summarySection.querySelector('input[type="date"]');
if (!datePicker.value) {
datePicker.value = getYesterday();
}
const selectedDate = datePicker.value;
// 获取并更新报告数据
const reportData = await fetchDailyReport(selectedDate);
if (reportData && reportData.data) {
updateDailySummary(reportData);
}
});
// 为总结页面的日期选择器添加事件监听
const summaryDatePicker = summarySection.querySelector('input[type="date"]');
if (summaryDatePicker) {
// 设置日期选择器的最大值为昨天
const yesterday = getYesterday();
summaryDatePicker.max = yesterday;
// 如果没有选择日期,默认设置为昨天
if (!summaryDatePicker.value) {
summaryDatePicker.value = yesterday;
}
summaryDatePicker.addEventListener('change', async function() {
const selectedDate = this.value;
const reportData = await fetchDailyReport(selectedDate);
if (reportData && reportData.data) {
updateDailySummary(reportData);
}
});
}
// 点击摄像头时的处理
cameraItems.forEach(item => {
item.addEventListener('click', function() {
// 显示工具栏
toolbar.style.display = 'flex';
// 显示常规内容,根据不同类型设置不同的display值
regularContent.forEach(content => {
if (content.classList.contains('dashboard-grid')) {
content.style.display = 'grid';
} else if (content.classList.contains('chart-grid')) {
content.style.display = 'grid';
} else {
content.style.display = 'block';
}
});
// 隐藏总结内容
summarySection.style.display = 'none';
// 更新导航状态
dailySummaryNav.classList.remove('active');
cameraItems.forEach(cam => cam.classList.remove('active'));
this.classList.add('active');
});
});
}
// 添加清除页面数据的函数
function clearPageData() {
// 清除行为颜色映射
behaviorColors.clear();
// 清除统计卡片数据
const statValues = document.querySelectorAll('.stat-value');
statValues.forEach(el => {
if (el) el.textContent = '--';
});
// 清除时间轴
const timeline = document.getElementById('timeline');
if (timeline) {
timeline.innerHTML = '';
}
// 清除图例
const legend = document.getElementById('legend');
if (legend) {
legend.innerHTML = '';
}
// 清除人员图例
const personLegend = document.getElementById('person-legend');
if (personLegend) {
personLegend.innerHTML = '';
}
// 清除图表数据
const chartIds = ['behaviorChart', 'timeChart', 'peakTimeChart'];
chartIds.forEach(id => {
const container = document.getElementById(id);
if (container) {
const chart = echarts.getInstanceByDom(container);
if (chart) {
chart.clear();
}
}
});
// 清除事件列表
const eventList = document.querySelector('.event-list');
if (eventList) {
const eventListContent = eventList.querySelector('.event-list-header')?.nextElementSibling;
if (eventListContent) {
eventListContent.remove();
}
}
}
// 添加 tooltip 相关函数
function showEnhancedTooltip(e) {
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
const time = e.target.getAttribute('data-time');
const behavior = e.target.getAttribute('data-behavior');
const faces = JSON.parse(e.target.getAttribute('data-faces') || '[]');
let faceInfo = '';
if (faces.length > 0) {
faceInfo = faces.map(face =>
`<div>人员:${face.id} (相似度: ${(face.similarity * 100).toFixed(1)}%)</div>`
).join('');
} else {
faceInfo = '<div>人员:未知</div>'; // 没有数据时显示"未知"
}
tooltip.innerHTML = `
<div style="font-weight: bold">${time}</div>
<div>行为:${behavior}</div>
${faceInfo}
`;
// 设置tooltip位置
tooltip.style.position = 'fixed';
tooltip.style.left = (e.clientX + 10) + 'px';
tooltip.style.top = (e.clientY + 10) + 'px';
// 移除旧的tooltip
const oldTooltip = document.querySelector('.tooltip');
if (oldTooltip) {
oldTooltip.remove();
}
document.body.appendChild(tooltip);
}
function hideTooltip() {
const tooltip = document.querySelector('.tooltip');
if (tooltip) {
tooltip.remove();
}
}
// 处理tooltip位置更新
document.addEventListener('mousemove', function(e) {
const tooltip = document.querySelector('.tooltip');
if (tooltip) {
tooltip.style.left = (e.clientX + 10) + 'px';
tooltip.style.top = (e.clientY + 10) + 'px';
}
});
// 添加获取每日报告数据的函数
async function fetchDailyReport(date) {
try {
// 验证日期:不能大于今天
const selectedDate = new Date(date);
const today = new Date();
today.setHours(23, 59, 59, 999); // 设置为今天的最后一刻
if (selectedDate > today) {
showErrorMessage('只能查询今天及以前的报告');
return null;
}
// 确保日期格式为 YYYY-MM-DD
const formattedDate = date.includes('-') ? date : date.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
const response = await fetch(`${API_BASE_URL}/report/${formattedDate}`);
if (!response.ok) {
throw new Error(response.status === 400 ? '无效的日期格式' : '网络请求失败');
}
const data = await response.json();
// 如果报告正在生成中,启动轮询
if (data.message === 'processing') {
startPolling(formattedDate);
return null;
}
// 检查报告是否真正生成完成
if (data.message === 'success' && data.data) {
return data;
} else if (data.message === 'no_data') {
showErrorMessage('暂无该日期的数据');
return null;
} else {
showErrorMessage('报告生成失败,请稍后重试');
return null;
}
} catch (error) {
console.error('Error fetching daily report:', error);
showErrorMessage(error.message);
return null;
}
}
// 添加轮询相关变量和函数
let pollingInterval = null;
const POLLING_DELAY = 5000; //5秒轮询一次
function startPolling(date) {
// 清除可能存在的轮询
if (pollingInterval) {
clearInterval(pollingInterval);
}
// 显示生成中的状态
const totalEventsElem = document.getElementById('total-events');
if (totalEventsElem) {
totalEventsElem.innerHTML = '<div class="loading">报告正在生成中,请稍候...</div>';
}
let retryCount = 0;
const maxRetries = 12; // 最多重试12次(1分钟)
// 开始轮询
pollingInterval = setInterval(async () => {
try {
retryCount++;
const response = await fetch(`${API_BASE_URL}/report/${date}`);
const data = await response.json();
if (data.message === 'success') {
// 报告生成完成
clearInterval(pollingInterval);
updateDailySummary(data);
} else if (data.message === 'error') {
// 生成失败
clearInterval(pollingInterval);
showErrorMessage(data.detail || '报告生成失败');
updateDailySummary(null);
} else if (data.message === 'no_data') {
// 无数据
clearInterval(pollingInterval);
showErrorMessage('暂无该日期的数据');
updateDailySummary(null);
} else if (data.message === 'processing') {
// 更新加载提示
if (totalEventsElem) {
totalEventsElem.innerHTML = `<div class="loading">报告正在生成中,请稍候...(${retryCount}/${maxRetries})</div>`;
}
// 如果超过最大重试次数
if (retryCount >= maxRetries) {
clearInterval(pollingInterval);
showErrorMessage('报告生成超时,请刷新重试');
updateDailySummary(null);
}
}
} catch (error) {
console.error('Polling error:', error);
clearInterval(pollingInterval);
showErrorMessage('获取报告状态失败');
updateDailySummary(null);
}
}, POLLING_DELAY);
}
// 在页面卸载时清除轮询
window.addEventListener('beforeunload', () => {
if (pollingInterval) {
clearInterval(pollingInterval);
}
});
// 添加错误提示函数
function showErrorMessage(message) {
// 创建一个临时的错误提示元素
const errorDiv = document.createElement('div');
errorDiv.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: #f44336;
color: white;
padding: 16px 24px;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
z-index: 1000;
display: flex;
align-items: center;
gap: 8px;
`;
errorDiv.innerHTML = `
<i class="material-icons">error</i>
<span>${message}</span>
`;
// 添加到body
document.body.appendChild(errorDiv);
// 3秒后自动移除
setTimeout(() => {
errorDiv.style.opacity = '0';
errorDiv.style.transition = 'opacity 0.3s ease';
setTimeout(() => errorDiv.remove(), 300);
}, 3000);
}
// 更新每日总结内容
function updateDailySummary(reportData) {
if (!reportData || !reportData.data) {
// 清空所有分析内容,显示暂无数据
const elements = ['total-events', 'peak-hours', 'abnormal-events', 'behavior-analysis', 'recommendation-text'];
elements.forEach(id => {
const elem = document.getElementById(id);
if (elem) {
elem.innerHTML = '<div class="no-data">暂无数据</div>';
}
});
return;
}
const data = reportData.data;
try {
// 更新整体活动趋势
const totalEventsElem = document.getElementById('total-events');
if (totalEventsElem && data.整体活动趋势) {
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '未知';
return `${dateStr.slice(0,4)}${dateStr.slice(4,6)}${dateStr.slice(6,8)}`;
};
// 格式化高峰时段
const formatPeakHours = (hours) => {
if (!hours || !hours.length) return '无';
return hours
.map(hour => hour.toString().padStart(2, '0') + ':00')
.sort((a, b) => a.localeCompare(b))
.join('、');
};
totalEventsElem.innerHTML = `
<div><strong>日期:</strong>${formatDate(data.整体活动趋势.日期)}</div>
<div><strong>摄像头数量:</strong>${data.整体活动趋势.摄像头数量 || 0}</div>
<div><strong>行为总数:</strong>${data.整体活动趋势.行为总数 || 0}</div>
<div><strong>异常行为数:</strong>${data.整体活动趋势.异常行为数 || 0}</div>
<div><strong>行为高峰时段:</strong>${formatPeakHours(data.整体活动趋势.行为高峰时段)}</div>
`;
}
// 更新高峰时段分析
const peakHoursElem = document.getElementById('peak-hours');
if (peakHoursElem && data.高峰时段分析) {
peakHoursElem.innerHTML = `
<div><strong>高峰时段:</strong>${data.高峰时段分析.高峰时段 || '无'}</div>
<div><strong>高峰时段行为:</strong>${data.高峰时段分析.高峰时段行为 || '无'}</div>
<div><strong>活动规律:</strong>${data.高峰时段分析.活动规律 || '无'}</div>
`;
}
// 更新异常行为分析
const abnormalEventsElem = document.getElementById('abnormal-events');
if (abnormalEventsElem && data.异常行为分析) {
abnormalEventsElem.innerHTML = `
<div><strong>异常行为:</strong>${data.异常行为分析.异常行为 || '无'}</div>
<div><strong>异常行为次数:</strong>${data.异常行为分析.异常行为次数 || 0}</div>
<div><strong>异常行为出现时间:</strong>${data.异常行为分析.异常行为出现时间 || '无'}</div>
<div><strong>异常行为地点:</strong>${data.异常行为分析.异常行为地点 || '无'}</div>
`;
}
// 更新行为分析
const behaviorAnalysisElem = document.getElementById('behavior-analysis');
if (behaviorAnalysisElem && data.行为分析) {
behaviorAnalysisElem.innerHTML = `
<div class="behavior-category">
<h4>基础动作分析</h4>
<div><strong>站立行为:</strong>${data.行为分析.基础动作?.站立行为 || '无数据'}</div>
<div><strong>行走行为:</strong>${data.行为分析.基础动作?.行走行为 || '无数据'}</div>
<div><strong>坐卧行为:</strong>${data.行为分析.基础动作?.坐卧行为 || '无数据'}</div>
<div><strong>其他基础动作:</strong>${data.行为分析.基础动作?.其他基础动作 || '无数据'}</div>
</div>
<div class="behavior-category">
<h4>日常生活分析</h4>
<div><strong>饮食情况:</strong>${data.行为分析.日常生活?.饮食情况 || '无数据'}</div>
<div><strong>休息情况:</strong>${data.行为分析.日常生活?.休息情况 || '无数据'}</div>
<div><strong>医疗情况:</strong>${data.行为分析.日常生活?.医疗情况 || '无数据'}</div>
</div>
<div class="behavior-category">
<h4>社交活动分析</h4>
<div><strong>交际情况:</strong>${data.行为分析.社交活动?.交际情况 || '无数据'}</div>
<div><strong>娱乐情况:</strong>${data.行为分析.社交活动?.娱乐情况 || '无数据'}</div>
<div><strong>情感表达:</strong>${data.行为分析.社交活动?.情感表达 || '无数据'}</div>
</div>
<div class="behavior-category">
<h4>工作学习分析</h4>
<div><strong>学习情况:</strong>${data.行为分析.工作学习?.学习情况 || '无数据'}</div>
<div><strong>工作情况:</strong>${data.行为分析.工作学习?.工作情况 || '无数据'}</div>
<div><strong>创作活动:</strong>${data.行为分析.工作学习?.创作活动 || '无数据'}</div>
</div>
<div class="behavior-category">
<h4>运动娱乐分析</h4>
<div><strong>运动情况:</strong>${data.行为分析.运动娱乐?.运动情况 || '无数据'}</div>
<div><strong>运动时长:</strong>${data.行为分析.运动娱乐?.运动时长 || '无数据'}</div>
<div><strong>运动强度:</strong>${data.行为分析.运动娱乐?.运动强度 || '无数据'}</div>
</div>
<div class="behavior-category">
<h4>其他行为分析</h4>
<div><strong>出现时间:</strong>${data.行为分析.其他行为?.出现时间 || '无数据'}</div>
<div><strong>出现次数:</strong>${data.行为分析.其他行为?.出现次数 || '无数据'}</div>
</div>
`;
}
// 更新建议
const recommendationElem = document.getElementById('recommendation-text');
if (recommendationElem && data.建议) {
recommendationElem.innerHTML = `
<div class="recommendation-section">
<strong>生活作息建议:</strong>
<ul>${(data.建议.生活作息 || []).map(item => `<li>${item}</li>`).join('') || '<li>暂无建议</li>'}</ul>
</div>
<div class="recommendation-section">
<strong>活动安排建议:</strong>
<ul>${(data.建议.活动安排 || []).map(item => `<li>${item}</li>`).join('') || '<li>暂无建议</li>'}</ul>
</div>
<div class="recommendation-section">
<strong>安全防护建议:</strong>
<ul>${(data.建议.安全防护 || []).map(item => `<li>${item}</li>`).join('') || '<li>暂无建议</li>'}</ul>
</div>
<div class="recommendation-section">
<strong>健康建议:</strong>
<ul>${(data.建议.健康建议 || []).map(item => `<li>${item}</li>`).join('') || '<li>暂无建议</li>'}</ul>
</div>
`;
}
// 更新图表数据
if (data.hourly_distribution) {
updatePeakTimeChart(data.hourly_distribution);
}
} catch (error) {
console.error('Error updating daily summary:', error);
showErrorMessage('更新数据时发生错误');
}
}
// 更新高峰时段图表
function updatePeakTimeChart(hourlyData) {
const peakTimeChart = echarts.init(document.getElementById('peakTimeChart'));
const hours = Array.from({length: 24}, (_, i) => `${i}:00`);
// 为每个摄像头生成一个独特的颜色
const colors = [
'#2962ff', // 蓝色
'#00c853', // 绿色
'#ff6d00', // 橙色
'#d50000', // 红色
'#6200ea', // 紫色
'#00bfa5', // 青色
];
// 处理每个摄像头的数据
const series = hourlyData.map((camera, index) => {
const cameraData = new Array(24).fill(0);
camera.data.forEach(hourData => {
const hour = parseInt(hourData.hour);
cameraData[hour] = hourData.count;
});
return {
name: camera.camera_id,
data: cameraData,
type: 'line',
smooth: true,
data: cameraData,
areaStyle: {
opacity: 0.3
},
lineStyle: {
width: 3
},
itemStyle: {
color: colors[index % colors.length]
}
};
});
peakTimeChart.setOption({
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: hours
},
yAxis: {
type: 'value',
name: '行为数量'
},
series: series
});
}
async function downloadReport() {
try {
const datePicker = document.querySelector('.summary-section input[type="date"]');
if (!datePicker.value) {
showErrorMessage('请先选择日期');
return;
}
// 使用带连字符的日期格式 YYYY-MM-DD
const formattedDate = datePicker.value;
console.log('准备下载报告,日期:', formattedDate);
// 构建下载URL
const downloadUrl = `${API_BASE_URL}/report/download/${formattedDate}`;
console.log('下载URL:', downloadUrl);
const response = await fetch(downloadUrl);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || '下载报告失败');
}
const data = await response.json();
if (!data || !data.data) {
throw new Error('报告数据为空,请先生成报告');
}
// 创建Blob对象
const blob = new Blob([JSON.stringify(data.data, null, 2)], { type: 'application/json' });
// 创建下载链接
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `行为分析报告_${datePicker.value}.json`; // 这里保留带连字符的日期作为文件名
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('下载报告时出错:', error);
showErrorMessage(error.message);
}
}
// 添加自动刷新功能
function startAutoRefresh() {
// 清除已存在的定时器
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
}
// 设置新的定时器,每小时刷新一次
autoRefreshTimer = setInterval(async () => {
const activeCamera = document.querySelector('.camera-item.active');
if (activeCamera) {
const cameraText = activeCamera.querySelector('span:last-child').textContent;
const cameraId = cameraText.replace('摄像头 ', '').trim();
const toolbarDatePicker = document.querySelector('.toolbar input[type="date"]');
const selectedDate = toolbarDatePicker ? toolbarDatePicker.value.replace(/-/g, '') : null;
await loadCameraData(cameraId, selectedDate);
}
}, 3600000); // 3600000毫秒 = 1小时
}
// 添加刷新数据的函数
async function refreshData() {
try {
// 获取当前选中的摄像头
const activeCamera = document.querySelector('.camera-item.active');
if (!activeCamera) {
showErrorMessage('请先选择摄像头');
return;
}
// 获取摄像头ID
const cameraText = activeCamera.querySelector('span:last-child').textContent;
const cameraId = cameraText.replace('摄像头 ', '').trim();
// 获取当前选择的日期
const toolbarDatePicker = document.querySelector('.toolbar input[type="date"]');
const selectedDate = toolbarDatePicker ? toolbarDatePicker.value.replace(/-/g, '') : null;
// 显示加载状态
const refreshButton = document.querySelector('.toolbar button');
const originalContent = refreshButton.innerHTML;
refreshButton.innerHTML = '<i class="material-icons rotating">refresh</i> 刷新中...';
refreshButton.disabled = true;
// 重新加载数据
await loadCameraData(cameraId, selectedDate);
// 恢复按钮状态
refreshButton.innerHTML = originalContent;
refreshButton.disabled = false;
} catch (error) {
console.error('刷新数据失败:', error);
showErrorMessage('刷新数据失败');
// 确保按钮状态恢复
const refreshButton = document.querySelector('.toolbar button');
refreshButton.innerHTML = '<i class="material-icons">refresh</i> 刷新';
refreshButton.disabled = false;
}
}
// 添加获取人脸数据的函数
async function fetchFaceData(cameraId, date) {
try {
const currentDate = date || new Date().toISOString().split('T')[0].replace(/-/g, '');
const response = await fetch(`${API_BASE_URL}/face/${cameraId}/data?date=${currentDate}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('Error fetching face data:', error);
return null;
}
}
window.onload = function() {
initPage();
initPageSwitching();
// 保留窗口大小改变事件监听
window.addEventListener('resize', function() {
const charts = document.querySelectorAll('.chart-container');
charts.forEach(container => {
const chart = echarts.getInstanceByDom(container);
if (chart) {
chart.resize();
}
});
});
};
// 在页面卸载时清除定时器
window.addEventListener('beforeunload', () => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
}
});
</script>
</body>
</html>