Add task list page for Video Learning sessions
This commit adds a dedicated task list page to view and manage all video learning sessions, solving the issue where users couldn't find their background tasks after navigating away. Features: - New sessions.html page with card-based layout for all sessions - Real-time polling for session status updates (every 3 seconds) - Session control buttons (pause/resume/stop/delete) - localStorage integration for session persistence across page refreshes - Navigation links added to main page and video learning page - Empty state UI when no sessions exist New files: - dashboard/static/sessions.html - Task list page - dashboard/static/js/sessions.js - Sessions module with API calls - dashboard/static/css/sessions.css - Styling for sessions page Modified files: - dashboard/api/video_learning.py - Added /sessions/list endpoint - dashboard/static/index.html - Added "任务列表" button - dashboard/static/video-learning.html - Added "任务列表" button and localStorage Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -351,6 +351,29 @@ async def list_sessions() -> List[str]:
|
|||||||
return list(_active_sessions.keys())
|
return list(_active_sessions.keys())
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sessions/list")
|
||||||
|
async def list_all_sessions() -> List[Dict[str, Any]]:
|
||||||
|
"""获取所有会话的详细信息(包含进度、状态等)"""
|
||||||
|
result = []
|
||||||
|
for session_id, agent in _active_sessions.items():
|
||||||
|
try:
|
||||||
|
progress = agent.get_session_progress()
|
||||||
|
result.append({
|
||||||
|
"session_id": session_id,
|
||||||
|
"platform": progress.get("platform", ""),
|
||||||
|
"target_count": progress.get("target_count", 0),
|
||||||
|
"watched_count": progress.get("watched_count", 0),
|
||||||
|
"progress_percent": progress.get("progress_percent", 0),
|
||||||
|
"is_active": progress.get("is_active", False),
|
||||||
|
"is_paused": progress.get("is_paused", False),
|
||||||
|
"total_duration": progress.get("total_duration", 0),
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
# 跳过出错的会话
|
||||||
|
continue
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/sessions/{session_id}", response_model=Dict[str, str])
|
@router.delete("/sessions/{session_id}", response_model=Dict[str, str])
|
||||||
async def delete_session(session_id: str) -> Dict[str, str]:
|
async def delete_session(session_id: str) -> Dict[str, str]:
|
||||||
"""Delete a session and clean up device mapping."""
|
"""Delete a session and clean up device mapping."""
|
||||||
|
|||||||
232
dashboard/static/css/sessions.css
Normal file
232
dashboard/static/css/sessions.css
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
/**
|
||||||
|
* Video Learning Sessions Page Styles
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Sessions Grid */
|
||||||
|
.sessions-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Session Card */
|
||||||
|
.session-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-card.completed {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Session Header */
|
||||||
|
.session-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-platform {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-platform::before {
|
||||||
|
content: '';
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Session Status */
|
||||||
|
.session-status {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-status.status-active {
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-status.status-paused {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-status.status-completed {
|
||||||
|
background: #6b7280;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Session ID */
|
||||||
|
.session-id {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-id small {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-id code {
|
||||||
|
font-family: 'Monaco', 'Consolas', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Session Progress */
|
||||||
|
.session-progress {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 8px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Session Duration */
|
||||||
|
.session-duration {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Session Actions */
|
||||||
|
.session-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-actions .btn {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 80px 20px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state svg {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Styles */
|
||||||
|
.btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast Notifications */
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success {
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sessions-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,15 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Video Learning
|
Video Learning
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/static/sessions.html" class="btn btn-secondary">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7"></rect>
|
||||||
|
</svg>
|
||||||
|
任务列表
|
||||||
|
</a>
|
||||||
<button @click="refreshDevices" class="btn btn-secondary" :disabled="refreshing">
|
<button @click="refreshDevices" class="btn btn-secondary" :disabled="refreshing">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" :class="{ spinning: refreshing }">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" :class="{ spinning: refreshing }">
|
||||||
<polyline points="23 4 23 10 17 10"></polyline>
|
<polyline points="23 4 23 10 17 10"></polyline>
|
||||||
|
|||||||
120
dashboard/static/js/sessions.js
Normal file
120
dashboard/static/js/sessions.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Video Learning Sessions Module for AutoGLM Dashboard
|
||||||
|
*
|
||||||
|
* This module provides functionality for managing video learning sessions,
|
||||||
|
* including listing, controlling, and monitoring sessions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SessionsModule = {
|
||||||
|
// Current session state
|
||||||
|
sessions: [],
|
||||||
|
isLoading: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all sessions
|
||||||
|
*/
|
||||||
|
async loadSessions() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/video-learning/sessions/list');
|
||||||
|
this.sessions = response.data;
|
||||||
|
return this.sessions;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading sessions:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get session status
|
||||||
|
*/
|
||||||
|
async getSessionStatus(sessionId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/video-learning/sessions/${sessionId}/status`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting session status:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get session videos
|
||||||
|
*/
|
||||||
|
async getSessionVideos(sessionId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/video-learning/sessions/${sessionId}/videos`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting session videos:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Control session (pause/resume/stop)
|
||||||
|
*/
|
||||||
|
async controlSession(sessionId, action) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/api/video-learning/sessions/${sessionId}/control`, {
|
||||||
|
action: action
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error controlling session:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete session
|
||||||
|
*/
|
||||||
|
async deleteSession(sessionId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/api/video-learning/sessions/${sessionId}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting session:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format platform name
|
||||||
|
*/
|
||||||
|
getPlatformName(platform) {
|
||||||
|
const names = {
|
||||||
|
'douyin': '抖音',
|
||||||
|
'kuaishou': '快手',
|
||||||
|
'tiktok': 'TikTok'
|
||||||
|
};
|
||||||
|
return names[platform] || platform;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status text
|
||||||
|
*/
|
||||||
|
getStatusText(session) {
|
||||||
|
if (!session.is_active && !session.is_paused) return '已完成';
|
||||||
|
if (session.is_paused) return '已暂停';
|
||||||
|
if (session.is_active) return '进行中';
|
||||||
|
return '未知';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format duration
|
||||||
|
*/
|
||||||
|
formatDuration(seconds) {
|
||||||
|
if (!seconds) return '0s';
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${seconds.toFixed(1)}s`;
|
||||||
|
}
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
return `${minutes}m ${remainingSeconds.toFixed(1)}s`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export for use in other modules
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = SessionsModule;
|
||||||
|
}
|
||||||
330
dashboard/static/sessions.html
Normal file
330
dashboard/static/sessions.html
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>任务列表 - AutoGLM Dashboard</title>
|
||||||
|
<!-- Vue.js 3 -->
|
||||||
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
|
<!-- Axios for API requests -->
|
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||||
|
<!-- CSS -->
|
||||||
|
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/sessions.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<h1>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7"></rect>
|
||||||
|
</svg>
|
||||||
|
Video Learning 任务列表
|
||||||
|
</h1>
|
||||||
|
<div class="stats">
|
||||||
|
<span class="stat" v-if="sessions.length > 0">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7"></rect>
|
||||||
|
</svg>
|
||||||
|
{{ sessions.length }} 个会话
|
||||||
|
</span>
|
||||||
|
<span class="stat" v-if="activeCount > 0">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||||||
|
</svg>
|
||||||
|
{{ activeCount }} 活跃中
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button @click="refreshSessions" class="btn btn-secondary" :disabled="loading">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M23 4v6h-6"></path>
|
||||||
|
<path d="M1 20v-6h6"></path>
|
||||||
|
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
|
||||||
|
</svg>
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
<a href="/" class="btn btn-secondary">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||||
|
<polyline points="12 19 5 12 12 5"></polyline>
|
||||||
|
</svg>
|
||||||
|
返回主页
|
||||||
|
</a>
|
||||||
|
<a href="/static/video-learning.html" class="btn btn-primary">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
新建任务
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div class="empty-state" v-if="sessions.length === 0 && !loading">
|
||||||
|
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
<line x1="9" y1="3" x2="9" y2="21"></line>
|
||||||
|
<line x1="15" y1="3" x2="15" y2="21"></line>
|
||||||
|
<line x1="3" y1="9" x2="21" y2="9"></line>
|
||||||
|
<line x1="3" y1="15" x2="21" y2="15"></line>
|
||||||
|
</svg>
|
||||||
|
<h2>暂无任务</h2>
|
||||||
|
<p>还没有创建任何视频学习任务</p>
|
||||||
|
<a href="/static/video-learning.html" class="btn btn-primary">创建第一个任务</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sessions Grid -->
|
||||||
|
<div class="sessions-grid" v-if="sessions.length > 0">
|
||||||
|
<div v-for="session in sessions" :key="session.session_id"
|
||||||
|
class="session-card"
|
||||||
|
:class="{ 'completed': !session.is_active && !session.is_paused }">
|
||||||
|
<!-- Card Header -->
|
||||||
|
<div class="session-header">
|
||||||
|
<div class="session-platform">
|
||||||
|
{{ getPlatformName(session.platform) }}
|
||||||
|
</div>
|
||||||
|
<div class="session-status" :class="getStatusClass(session)">
|
||||||
|
{{ getStatusText(session) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Session ID -->
|
||||||
|
<div class="session-id">
|
||||||
|
<small>会话 ID</small>
|
||||||
|
<code>{{ formatSessionId(session.session_id) }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress -->
|
||||||
|
<div class="session-progress">
|
||||||
|
<div class="progress-info">
|
||||||
|
<span>进度: {{ session.watched_count }} / {{ session.target_count }}</span>
|
||||||
|
<span>{{ Math.round(session.progress_percent) }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" :style="{ width: session.progress_percent + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Duration -->
|
||||||
|
<div class="session-duration" v-if="session.total_duration > 0">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||||||
|
</svg>
|
||||||
|
{{ formatDuration(session.total_duration) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="session-actions">
|
||||||
|
<button @click="viewDetails(session)" class="btn btn-sm btn-secondary">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-11 11-11-11-11z"></path>
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
</svg>
|
||||||
|
详情
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="session.is_paused" @click="controlSession(session.session_id, 'resume')"
|
||||||
|
class="btn btn-sm btn-primary">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||||
|
</svg>
|
||||||
|
恢复
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-else-if="session.is_active" @click="controlSession(session.session_id, 'pause')"
|
||||||
|
class="btn btn-sm btn-warning">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="6" y="4" width="4" height="16"></rect>
|
||||||
|
<rect x="14" y="4" width="4" height="16"></rect>
|
||||||
|
</svg>
|
||||||
|
暂停
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="session.is_active || session.is_paused"
|
||||||
|
@click="controlSession(session.session_id, 'stop')"
|
||||||
|
class="btn btn-sm btn-danger">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="6" y="6" width="12" height="12"></rect>
|
||||||
|
</svg>
|
||||||
|
停止
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="!session.is_active && !session.is_paused"
|
||||||
|
@click="deleteSession(session.session_id)"
|
||||||
|
class="btn btn-sm btn-danger">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="3 6 5 6 21 6"></polyline>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||||
|
</svg>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Toast notifications -->
|
||||||
|
<div class="toast-container">
|
||||||
|
<div v-for="toast in toasts" :key="toast.id" class="toast" :class="toast.type">
|
||||||
|
{{ toast.message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/js/sessions.js"></script>
|
||||||
|
<script>
|
||||||
|
const { createApp } = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
sessions: [],
|
||||||
|
loading: false,
|
||||||
|
toasts: [],
|
||||||
|
toastIdCounter: 0,
|
||||||
|
pollInterval: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.loadSessions();
|
||||||
|
this.startPolling();
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
this.stopPolling();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadSessions() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/video-learning/sessions/list');
|
||||||
|
this.sessions = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.showToast('加载失败: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshSessions() {
|
||||||
|
await this.loadSessions();
|
||||||
|
this.showToast('已刷新', 'info');
|
||||||
|
},
|
||||||
|
|
||||||
|
async viewDetails(session) {
|
||||||
|
// 保存 session_id 到 localStorage
|
||||||
|
localStorage.setItem('current_session_id', session.session_id);
|
||||||
|
// 跳转到 Video Learning 详情页
|
||||||
|
window.location.href = '/static/video-learning.html?session=' + session.session_id;
|
||||||
|
},
|
||||||
|
|
||||||
|
async controlSession(sessionId, action) {
|
||||||
|
try {
|
||||||
|
await axios.post(`/api/video-learning/sessions/${sessionId}/control`, { action });
|
||||||
|
this.showToast(`会话已${action === 'pause' ? '暂停' : action === 'resume' ? '恢复' : '停止'}`, 'success');
|
||||||
|
await this.loadSessions();
|
||||||
|
} catch (error) {
|
||||||
|
this.showToast('操作失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteSession(sessionId) {
|
||||||
|
if (!confirm('确定要删除这个会话吗?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.delete(`/api/video-learning/sessions/${sessionId}`);
|
||||||
|
this.showToast('会话已删除', 'success');
|
||||||
|
await this.loadSessions();
|
||||||
|
} catch (error) {
|
||||||
|
this.showToast('删除失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startPolling() {
|
||||||
|
this.pollInterval = setInterval(() => {
|
||||||
|
this.loadSessions();
|
||||||
|
}, 3000); // 每 3 秒刷新一次
|
||||||
|
},
|
||||||
|
|
||||||
|
stopPolling() {
|
||||||
|
if (this.pollInterval) {
|
||||||
|
clearInterval(this.pollInterval);
|
||||||
|
this.pollInterval = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getPlatformName(platform) {
|
||||||
|
const names = {
|
||||||
|
'douyin': '抖音',
|
||||||
|
'kuaishou': '快手',
|
||||||
|
'tiktok': 'TikTok'
|
||||||
|
};
|
||||||
|
return names[platform] || platform;
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatusText(session) {
|
||||||
|
if (!session.is_active && !session.is_paused) return '已完成';
|
||||||
|
if (session.is_paused) return '已暂停';
|
||||||
|
if (session.is_active) return '进行中';
|
||||||
|
return '未知';
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatusClass(session) {
|
||||||
|
if (!session.is_active && !session.is_paused) return 'status-completed';
|
||||||
|
if (session.is_paused) return 'status-paused';
|
||||||
|
if (session.is_active) return 'status-active';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
|
formatSessionId(sessionId) {
|
||||||
|
// 只显示前 8 位和后 4 位
|
||||||
|
if (sessionId.length > 16) {
|
||||||
|
return sessionId.substring(0, 8) + '...' + sessionId.substring(sessionId.length - 4);
|
||||||
|
}
|
||||||
|
return sessionId;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDuration(seconds) {
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${seconds.toFixed(1)}s`;
|
||||||
|
}
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
return `${minutes}m ${remainingSeconds.toFixed(1)}s`;
|
||||||
|
},
|
||||||
|
|
||||||
|
showToast(message, type = 'info') {
|
||||||
|
const id = this.toastIdCounter++;
|
||||||
|
this.toasts.push({ id, message, type });
|
||||||
|
setTimeout(() => {
|
||||||
|
this.toasts = this.toasts.filter(t => t.id !== id);
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
activeCount() {
|
||||||
|
return this.sessions.filter(s => s.is_active).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).mount('#app');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -42,6 +42,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
|
<a href="/static/sessions.html" class="btn btn-secondary">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7"></rect>
|
||||||
|
</svg>
|
||||||
|
任务列表
|
||||||
|
</a>
|
||||||
<button @click="goBack" class="btn btn-secondary">
|
<button @click="goBack" class="btn btn-secondary">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<line x1="19" y1="12" x2="5" y2="12"></line>
|
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||||
@@ -285,6 +294,23 @@
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.loadDevices();
|
this.loadDevices();
|
||||||
this.setupVideoLearningEvents();
|
this.setupVideoLearningEvents();
|
||||||
|
|
||||||
|
// 检查 URL 是否有 session 参数
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const sessionParam = urlParams.get('session');
|
||||||
|
if (sessionParam) {
|
||||||
|
this.currentSessionId = sessionParam;
|
||||||
|
this.updateSessionStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 localStorage 中是否有保存的 session_id
|
||||||
|
if (!this.currentSessionId) {
|
||||||
|
const savedSessionId = localStorage.getItem('current_session_id');
|
||||||
|
if (savedSessionId) {
|
||||||
|
this.currentSessionId = savedSessionId;
|
||||||
|
this.updateSessionStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -318,6 +344,10 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.currentSessionId = createResult.session_id;
|
this.currentSessionId = createResult.session_id;
|
||||||
|
|
||||||
|
// 保存到 localStorage
|
||||||
|
localStorage.setItem('current_session_id', this.currentSessionId);
|
||||||
|
|
||||||
this.showToast('Session created! Starting...', 'success');
|
this.showToast('Session created! Starting...', 'success');
|
||||||
|
|
||||||
// Start session
|
// Start session
|
||||||
|
|||||||
Reference in New Issue
Block a user