jytest/web/tasks.html

231 lines
9.7 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>任务管理</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="header">
<h1>任务管理</h1>
</div>
<div class="container">
<div id="task-list">
<div class="loading">
<div class="spinner"></div>
<p>加载任务列表...</p>
</div>
</div>
</div>
<!-- 任务详情弹窗 -->
<div id="modal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:200;display:none">
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:white;border-radius:12px;padding:20px;width:90%;max-width:500px;max-height:80vh;overflow-y:auto">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h3 style="font-size:16px">任务详情</h3>
<button onclick="closeModal()" style="background:none;border:none;font-size:20px;cursor:pointer">&times;</button>
</div>
<div id="modal-content"></div>
</div>
</div>
<nav class="nav">
<a href="index.html" class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
首页
</a>
<a href="novel.html" class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
短剧
</a>
<a href="video.html" class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
视频
</a>
<a href="tasks.html" class="nav-item active">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
任务
</a>
</nav>
<script src="js/app.js"></script>
<script>
let pollTimers = {};
async function loadTasks() {
try {
const tasks = await API.get('/api/tasks');
renderTasks(tasks);
} catch(err) {
document.getElementById('task-list').innerHTML =
`<div class="empty"><p>加载失败: ${err.message}</p></div>`;
}
}
function renderTasks(tasks) {
const container = document.getElementById('task-list');
if (!tasks || tasks.length === 0) {
container.innerHTML = `<div class="empty">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<p>暂无任务</p>
<p style="font-size:13px;margin-top:8px">去首页创建你的第一个任务吧</p>
</div>`;
return;
}
container.innerHTML = tasks.map(t => `
<div class="task-item" id="task-${t.id}">
<div class="task-header">
<span class="task-title">${getCapabilityName(t.capability)} - ${getActionName(t.action)}</span>
${getStatusBadge(t.status)}
</div>
<div class="task-meta">
任务ID: ${t.id}<br>
创建时间: ${formatTime(t.created_at)}<br>
${t.thread_id ? 'Thread ID: ' + t.thread_id + '<br>' : ''}
${t.elapsed_sec ? '耗时: ' + t.elapsed_sec + '秒<br>' : ''}
${t.error_code ? '错误: ' + t.error_code + ' ' + t.error_msg + '<br>' : ''}
</div>
<div class="task-actions">
<button class="btn btn-primary" onclick="refreshTask('${t.id}')">刷新状态</button>
<button class="btn btn-success" onclick="viewTask('${t.id}')">查看详情</button>
<button class="btn btn-danger" onclick="deleteTask('${t.id}')">删除</button>
</div>
<div id="result-${t.id}"></div>
</div>
`).join('');
// 对运行中的任务自动轮询
tasks.forEach(t => {
if (['CREATED', 'QUEUED', 'RUNNING'].includes(t.status)) {
startPolling(t);
}
});
}
function startPolling(task) {
if (pollTimers[task.id]) return;
const queryUrl = task.type === 'novel' ? '/api/novel/query' : '/api/video/query';
pollTimers[task.id] = pollTask(
task.id,
(id) => API.post(queryUrl, {task_id: id}),
(res) => {
const status = res.data?.status;
if (status) {
const el = document.querySelector(`#task-${task.id} .task-header`);
if (el) {
el.innerHTML = `<span class="task-title">${getCapabilityName(task.capability)} - ${getActionName(task.action)}</span>${getStatusBadge(status)}`;
}
if (status === 'SUCCESS') {
showTaskResult(task, res.data);
}
}
},
5000
);
}
async function refreshTask(id) {
try {
const task = await API.get('/api/task?id=' + id);
const queryUrl = task.type === 'novel' ? '/api/novel/query' : '/api/video/query';
const result = await API.post(queryUrl, {task_id: id});
if (result.data) {
const el = document.querySelector(`#task-${id} .task-header`);
if (el) {
el.innerHTML = `<span class="task-title">${getCapabilityName(task.capability)} - ${getActionName(task.action)}</span>${getStatusBadge(result.data.status)}`;
}
if (result.data.status === 'SUCCESS') {
showTaskResult(task, result.data);
}
showAlert('状态已更新', 'success');
}
} catch(err) {
showAlert('刷新失败: ' + err.message, 'error');
}
}
function showTaskResult(task, data) {
const container = document.getElementById('result-' + task.id);
if (!container) return;
if (task.type === 'video' && data.video_artifact) {
const a = data.video_artifact;
if (a.url) {
container.innerHTML = `<div class="result">
<div class="result-title">视频成品</div>
<video class="result-video" controls src="${a.url}"></video>
<p style="margin-top:8px;font-size:12px;color:var(--text-light)">
时长: ${a.duration}s | 分辨率: ${a.width}x${a.height}
</p>
<a href="${a.url}" download class="btn btn-success" style="margin-top:8px;display:inline-block">下载视频</a>
</div>`;
}
} else if (task.type === 'novel' && data.novel_data) {
container.innerHTML = `<div class="result">
<div class="result-title">短剧数据</div>
<pre style="font-size:12px;overflow-x:auto;white-space:pre-wrap">${JSON.stringify(data.novel_data, null, 2)}</pre>
</div>`;
}
}
async function viewTask(id) {
try {
const task = await API.get('/api/task?id=' + id);
const modal = document.getElementById('modal');
const content = document.getElementById('modal-content');
content.innerHTML = `
<div style="font-size:13px">
<p><strong>任务ID:</strong> ${task.id}</p>
<p><strong>类型:</strong> ${task.type}</p>
<p><strong>能力:</strong> ${getCapabilityName(task.capability)}</p>
<p><strong>动作:</strong> ${getActionName(task.action)}</p>
<p><strong>状态:</strong> ${getStatusBadge(task.status)}</p>
<p><strong>Run ID:</strong> ${task.run_id || '-'}</p>
<p><strong>Thread ID:</strong> ${task.thread_id || '-'}</p>
<p><strong>Asset ID:</strong> ${task.asset_id || '-'}</p>
<p><strong>创建时间:</strong> ${formatTime(task.created_at)}</p>
<p><strong>更新时间:</strong> ${formatTime(task.updated_at)}</p>
${task.error_code ? `<p><strong>错误码:</strong> ${task.error_code}</p><p><strong>错误信息:</strong> ${task.error_msg}</p>` : ''}
${task.usage ? `<p><strong>计费:</strong> <pre style="font-size:11px">${JSON.stringify(task.usage, null, 2)}</pre></p>` : ''}
<p><strong>输入参数:</strong></p>
<pre style="font-size:11px;background:#f8fafc;padding:8px;border-radius:4px;overflow-x:auto">${JSON.stringify(task.input, null, 2)}</pre>
${task.output ? `<p style="margin-top:8px"><strong>输出结果:</strong></p>
<pre style="font-size:11px;background:#f0fdf4;padding:8px;border-radius:4px;overflow-x:auto">${JSON.stringify(task.output, null, 2)}</pre>` : ''}
</div>
`;
modal.style.display = 'block';
} catch(err) {
showAlert('获取详情失败: ' + err.message, 'error');
}
}
async function deleteTask(id) {
if (!confirm('确定要删除这个任务吗?')) return;
try {
await API.post('/api/task/delete', {id: id});
document.getElementById('task-' + id)?.remove();
showAlert('任务已删除', 'success');
} catch(err) {
showAlert('删除失败: ' + err.message, 'error');
}
}
function closeModal() {
document.getElementById('modal').style.display = 'none';
}
// 点击弹窗外部关闭
document.getElementById('modal')?.addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
// 初始加载
loadTasks();
// 每30秒自动刷新
setInterval(loadTasks, 30000);
</script>
</body>
</html>