Files
Open-AutoGLM/dashboard/static/sessions.html
let5sne.win10 a356c481ca 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>
2026-01-10 02:22:42 +08:00

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>