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>
331 lines
15 KiB
HTML
331 lines
15 KiB
HTML
<!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>
|