update: 重构后台任务展示,采用悬浮窗样式

This commit is contained in:
xiamuceer-j
2026-04-29 17:31:06 +08:00
parent 9a9ae0608e
commit 5f5fd99005
10 changed files with 613 additions and 610 deletions
+22 -346
View File
@@ -1,9 +1,10 @@
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Progress, Card, InputNumber, Alert, Radio, Descriptions, Collapse, Popconfirm, Pagination, theme } from 'antd';
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined, InfoCircleOutlined, CaretRightOutlined, DeleteOutlined, BookOutlined, FormOutlined, PlusOutlined, ReadOutlined, ClockCircleOutlined, LoadingOutlined } from '@ant-design/icons';
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, InputNumber, Alert, Radio, Descriptions, Collapse, Popconfirm, Pagination, theme } from 'antd';
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined, InfoCircleOutlined, CaretRightOutlined, DeleteOutlined, BookOutlined, FormOutlined, PlusOutlined, ReadOutlined } from '@ant-design/icons';
import { useStore } from '../store';
import { eventBus } from '../store/eventBus';
import { useChapterSync } from '../store/hooks';
import { generateChapterBackground, getProjectTasks, cancelTask, deleteTask, type TaskStatus as BgTaskStatus } from '../services/backgroundTaskService';
import { generateChapterBackground } from '../services/backgroundTaskService';
import { projectApi, writingStyleApi, chapterApi } from '../services/api';
import type { Chapter, ChapterUpdate, ApiError, WritingStyle, AnalysisTask, ExpansionPlanData } from '../types';
import type { TextAreaRef } from 'antd/es/input/TextArea';
@@ -97,111 +98,6 @@ export default function Chapters() {
const [singleChapterProgress, setSingleChapterProgress] = useState(0);
const [singleChapterProgressMessage, setSingleChapterProgressMessage] = useState('');
// 后台生成任务状态
const [bgTaskVisible, setBgTaskVisible] = useState(false);
const [bgTaskProgress, setBgTaskProgress] = useState(0);
const [bgTaskMessage, setBgTaskMessage] = useState('');
const [bgTaskRunning, setBgTaskRunning] = useState(false);
const bgTaskCancelRef = useRef<(() => void) | null>(null);
const [projectBgTasks, setProjectBgTasks] = useState<BgTaskStatus[]>([]);
const bgPollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// 后台任务列表 Modal 状态
const [taskListVisible, setTaskListVisible] = useState(false);
const [taskList, setTaskList] = useState<BgTaskStatus[]>([]);
const [taskListLoading, setTaskListLoading] = useState(false);
// 轮询项目后台任务
useEffect(() => {
if (!currentProject) return;
const pollBgTasks = async () => {
try {
const resp = await getProjectTasks(currentProject.id, 'chapter_generate', 10);
const active = resp.items.filter(t => t.status === 'pending' || t.status === 'running');
setProjectBgTasks(active);
// 如果有活跃任务,继续轮询
if (active.length > 0) {
bgPollTimerRef.current = setTimeout(pollBgTasks, 3000);
}
} catch {}
};
pollBgTasks();
return () => { if (bgPollTimerRef.current) clearTimeout(bgPollTimerRef.current); };
}, [currentProject]);
// 加载并显示后台任务列表
const showTaskListModal = async () => {
if (!currentProject?.id) return;
setTaskListVisible(true);
setTaskListLoading(true);
try {
const result = await getProjectTasks(currentProject.id);
setTaskList(result.items || []);
} catch (error) {
message.error('加载任务列表失败');
} finally {
setTaskListLoading(false);
}
};
// 刷新任务列表
const refreshTaskList = async () => {
if (!currentProject?.id) return;
setTaskListLoading(true);
try {
const result = await getProjectTasks(currentProject.id);
setTaskList(result.items || []);
const active = (result.items || []).filter(t => t.status === 'pending' || t.status === 'running');
setProjectBgTasks(active);
} catch (error) {
console.error('刷新任务列表失败:', error);
} finally {
setTaskListLoading(false);
}
};
// 获取任务状态标签
const getTaskStatusTag = (status: BgTaskStatus['status']) => {
switch (status) {
case 'pending': return <Tag icon={<ClockCircleOutlined />} color="default"></Tag>;
case 'running': return <Tag icon={<LoadingOutlined />} color="processing"></Tag>;
case 'completed': return <Tag icon={<CheckCircleOutlined />} color="success"></Tag>;
case 'failed': return <Tag icon={<CloseCircleOutlined />} color="error"></Tag>;
case 'cancelled': return <Tag icon={<CloseCircleOutlined />} color="default"></Tag>;
default: return <Tag>{status}</Tag>;
}
};
// 获取任务类型标签
const getTaskTypeLabel = (taskType: string) => {
switch (taskType) {
case 'chapter_generate': return '章节生成';
case 'outline_new': return '大纲生成';
case 'outline_continue': return '大纲续写';
default: return taskType;
}
};
// 处理取消后台任务
const handleCancelBgTask = async (taskId: string) => {
try {
await cancelTask(taskId);
message.success('任务已取消');
refreshTaskList();
} catch (error) {
message.error('取消任务失败');
}
};
// 处理删除任务记录
const handleDeleteBgTask = async (taskId: string) => {
try {
await deleteTask(taskId);
message.success('任务记录已删除');
refreshTaskList();
} catch (error) {
message.error('删除任务记录失败');
}
};
// 批量生成相关状态
const [batchGenerateVisible, setBatchGenerateVisible] = useState(false);
@@ -643,7 +539,7 @@ export default function Chapters() {
// 启动轮询
startBatchPolling(task.batch_id);
message.info('检测到未完成的批量生成任务,已在顶部显示进度');
message.info('检测到未完成的批量生成任务,请查看任务列表');
}
} catch (error) {
console.error('检查批量生成任务失败:', error);
@@ -1079,6 +975,7 @@ export default function Chapters() {
// 后台生成章节(关闭浏览器也不影响)
// 不再强制显示进度弹窗,任务进度在右下角悬浮任务框中显示
const handleBackgroundGenerate = async () => {
if (!editingId) return;
if (!selectedStyleId) {
@@ -1087,12 +984,7 @@ export default function Chapters() {
}
try {
setBgTaskVisible(true);
setBgTaskRunning(true);
setBgTaskProgress(0);
setBgTaskMessage("正在创建后台任务...");
const cancelFn = await generateChapterBackground(
await generateChapterBackground(
editingId,
{
style_id: selectedStyleId,
@@ -1100,14 +992,10 @@ export default function Chapters() {
model: selectedModel,
narrative_perspective: temporaryNarrativePerspective,
},
(status) => {
setBgTaskProgress(status.progress || 0);
setBgTaskMessage(status.status_message || "处理中...");
() => {
// 进度更新由悬浮任务框处理,无需额外操作
},
(_) => {
setBgTaskProgress(100);
setBgTaskMessage("生成完成!");
setBgTaskRunning(false);
message.success("后台章节生成完成!");
refreshChapters();
if (currentProject) {
@@ -1116,17 +1004,15 @@ export default function Chapters() {
loadAnalysisTasks();
},
(error) => {
setBgTaskRunning(false);
setBgTaskMessage("失败: " + error);
message.error("后台生成失败: " + error);
}
);
bgTaskCancelRef.current = cancelFn;
message.info("已提交后台生成任务,可以关闭此页面");
message.info("章节生成任务已提交,可在右下角任务面板查看进度");
// 通知悬浮任务框刷新
eventBus.emit('background-task-created');
} catch (error) {
message.error("创建后台任务失败");
setBgTaskRunning(false);
}
};
const getStatusColor = (status: string) => {
@@ -1243,7 +1129,7 @@ export default function Chapters() {
try {
setBatchGenerating(true);
setBatchGenerateVisible(false); // 关闭配置对话框,避免遮挡进度弹窗
setBatchGenerateVisible(false); // 关闭配置对话框,任务进度在悬浮任务框中显示
const requestBody: {
start_chapter_number: number;
@@ -1293,7 +1179,9 @@ export default function Chapters() {
estimated_time_minutes: result.estimated_time_minutes,
});
message.success(`批量生成任务已创建,预计需要 ${result.estimated_time_minutes} 分钟`);
message.success(`批量生成任务已创建,预计需要 ${result.estimated_time_minutes} 分钟,可在右下角任务面板查看进度`);
// 通知悬浮任务框刷新
eventBus.emit('background-task-created');
// 🔔 触发浏览器通知(任务开始)
showBrowserNotification(
@@ -2092,23 +1980,17 @@ export default function Chapters() {
>
{batchAnalyzableChapterCount > 0 ? ` (${batchAnalyzableChapterCount})` : ''}
</Button>
<Button
icon={<ClockCircleOutlined />}
onClick={showTaskListModal}
>
{projectBgTasks.length > 0 && <Badge count={projectBgTasks.length} size="small" style={{ marginLeft: 4 }} />}
</Button>
<Button
type="primary"
icon={<RocketOutlined />}
onClick={handleOpenBatchGenerate}
disabled={chapters.length === 0}
disabled={chapters.length === 0 || batchGenerating}
loading={batchGenerating}
block={isMobile}
size={isMobile ? 'middle' : 'middle'}
style={{ background: token.colorInfo, borderColor: token.colorInfo }}
style={batchGenerating ? {} : { background: token.colorInfo, borderColor: token.colorInfo }}
>
{batchGenerating ? '生成中...' : '批量生成'}
</Button>
<Button
type="default"
@@ -2123,102 +2005,6 @@ export default function Chapters() {
</Space>
</div>
{/* 后台生成任务进度 */}
{(projectBgTasks.length > 0 || (batchGenerating && batchProgress)) && (
<div style={{
marginBottom: 16,
padding: '12px 16px',
background: token.colorInfoBg,
borderRadius: token.borderRadius,
border: `1px solid ${token.colorInfoBorder}`
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<RocketOutlined style={{ color: token.colorInfo }} spin />
<span style={{ fontWeight: 600, color: token.colorInfo }}>
</span>
<span style={{ fontSize: 12, color: token.colorTextSecondary }}>
</span>
</div>
{/* 批量生成进度 */}
{batchGenerating && batchProgress && (
<div style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '8px 0',
borderBottom: `1px solid ${token.colorBorderSecondary}`
}}>
<Tag color="processing" style={{ minWidth: 60, textAlign: 'center' }}>
</Tag>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 12, marginBottom: 4, color: token.colorText }}>
{batchProgress.current_chapter_number
? `正在生成第 ${batchProgress.current_chapter_number}`
: '批量生成中...'} ({batchProgress.completed}/{batchProgress.total})
</div>
<div style={{
background: token.colorBgLayout, borderRadius: 4,
height: 8, overflow: 'hidden'
}}>
<div style={{
background: token.colorInfo, height: '100%',
width: (batchProgress.total > 0 ? Math.round((batchProgress.completed / batchProgress.total) * 100) : 0) + '%',
transition: 'width 0.3s'
}} />
</div>
</div>
<span style={{ fontSize: 13, fontWeight: 600, color: token.colorInfo, minWidth: 40, textAlign: 'right' }}>
{batchProgress.total > 0 ? Math.round((batchProgress.completed / batchProgress.total) * 100) : 0}%
</span>
<Button size="small" danger onClick={() => {
modal.confirm({
title: '确认取消',
content: '确定要取消批量生成吗?已生成的章节将保留。',
okText: '确定取消',
cancelText: '继续生成',
okButtonProps: { danger: true },
centered: true,
onOk: handleCancelBatchGenerate,
});
}}>
</Button>
</div>
)}
{/* 单章节后台生成进度 */}
{projectBgTasks.map(task => (
<div key={task.id} style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '6px 0',
borderBottom: `1px solid ${token.colorBorderSecondary}`
}}>
<Tag color={task.status === 'running' ? 'processing' : 'default'}
style={{ minWidth: 60, textAlign: 'center' }}>
{task.status === 'running' ? '生成中' : '排队中'}
</Tag>
<div style={{ flex: 1 }}>
<div style={{
background: token.colorBgLayout, borderRadius: 4,
height: 6, overflow: 'hidden'
}}>
<div style={{
background: token.colorInfo, height: '100%',
width: (task.progress || 0) + '%',
transition: 'width 0.3s'
}} />
</div>
</div>
<span style={{ fontSize: 12, color: token.colorTextSecondary, minWidth: 40, textAlign: 'right' }}>
{task.progress || 0}%
</span>
<span style={{ fontSize: 12, color: token.colorTextSecondary }}>
{task.status_message || ''}
</span>
</div>
))}
</div>
)}
<div style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
{chapters.length === 0 ? (
@@ -2769,7 +2555,7 @@ export default function Chapters() {
icon={canGenerate ? <ThunderboltOutlined /> : <LockOutlined />}
onClick={() => currentChapter && showGenerateModal(currentChapter)}
loading={isContinuing}
disabled={!canGenerate || bgTaskRunning}
disabled={!canGenerate}
danger={!canGenerate}
style={{ fontWeight: 'bold' }}
title={!canGenerate ? disabledReason : '根据大纲和前置章节内容创作(流式)'}
@@ -2779,8 +2565,7 @@ export default function Chapters() {
<Button
icon={<RocketOutlined />}
onClick={handleBackgroundGenerate}
disabled={!canGenerate || bgTaskRunning || isContinuing}
loading={bgTaskRunning}
disabled={!canGenerate || isContinuing}
style={{ fontWeight: 'bold' }}
title={!canGenerate ? disabledReason : '后台生成:关闭浏览器也不影响,完成后自动保存'}
>
@@ -2792,26 +2577,6 @@ export default function Chapters() {
</Space.Compact>
</Form.Item>
{/* 后台生成进度 */}
{bgTaskVisible && (
<Alert
message={bgTaskRunning ? '后台生成进行中...' : '后台生成完成'}
description={
<div>
<div style={{ marginBottom: 8 }}>{bgTaskMessage}</div>
<div style={{ background: '#f0f0f0', borderRadius: 4, height: 8, overflow: 'hidden' }}>
<div style={{ background: '#1890ff', height: '100%', width: bgTaskProgress + '%', transition: 'width 0.3s' }} />
</div>
<div style={{ fontSize: 12, color: '#888', marginTop: 4 }}>{bgTaskProgress}%</div>
</div>
}
type={bgTaskRunning ? 'info' : (bgTaskProgress >= 100 ? 'success' : 'error')}
showIcon
style={{ marginBottom: 12 }}
closable={!bgTaskRunning}
onClose={() => setBgTaskVisible(false)}
/>
)}
{/* 第一行:写作风格 + 叙事角度 */}
<div style={{
@@ -3233,95 +2998,6 @@ export default function Chapters() {
message={singleChapterProgressMessage}
/>
{/* 后台任务列表 Modal */}
<Modal
title={
<Space>
<ClockCircleOutlined />
<span></span>
{taskList.filter(t => t.status === 'running' || t.status === 'pending').length > 0 && (
<Badge count={taskList.filter(t => t.status === 'running' || t.status === 'pending').length} />
)}
</Space>
}
open={taskListVisible}
onCancel={() => setTaskListVisible(false)}
width={isMobile ? '95%' : 700}
centered
footer={
<Space>
<Button icon={<SyncOutlined />} onClick={refreshTaskList} loading={taskListLoading}>
</Button>
<Button onClick={() => setTaskListVisible(false)}>
</Button>
</Space>
}
>
{taskListLoading && taskList.length === 0 ? (
<div style={{ textAlign: 'center', padding: 40 }}>
<LoadingOutlined style={{ fontSize: 24 }} />
<div style={{ marginTop: 12, color: token.colorTextSecondary }}>...</div>
</div>
) : taskList.length === 0 ? (
<Empty description="暂无后台任务" />
) : (
<List
dataSource={taskList}
renderItem={(task) => (
<List.Item
key={task.id}
actions={[
...(task.status === 'running' || task.status === 'pending'
? [<Button key="cancel" size="small" danger onClick={() => handleCancelBgTask(task.id)}></Button>]
: []
),
...(task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled'
? [<Button key="delete" size="small" type="link" danger onClick={() => handleDeleteBgTask(task.id)}></Button>]
: []
),
].filter(Boolean)}
>
<List.Item.Meta
title={
<Space>
{getTaskStatusTag(task.status)}
<span>{getTaskTypeLabel(task.task_type)}</span>
{task.status === 'running' || task.status === 'pending' ? (
<Progress percent={task.progress} size="small" style={{ width: 120 }} />
) : null}
</Space>
}
description={
<div>
<div style={{ fontSize: 12, color: token.colorTextSecondary }}>
{task.status_message || '无状态信息'}
</div>
<div style={{ fontSize: 11, color: token.colorTextTertiary, marginTop: 4 }}>
: {task.created_at ? new Date(task.created_at).toLocaleString() : '-'}
{task.completed_at && ' | 完成: ' + new Date(task.completed_at).toLocaleString()}
</div>
{task.error_message && (
<div style={{ fontSize: 12, color: token.colorError, marginTop: 4 }}>
{'❌ ' + task.error_message}
</div>
)}
{task.task_result && task.status === 'completed' && (
<div style={{ fontSize: 12, color: token.colorSuccess, marginTop: 4 }}>
{'✅ ' + ((task.task_result as Record<string, unknown>).message as string || '任务完成')}
</div>
)}
</div>
}
/>
</List.Item>
)}
/>
)}
</Modal>
{/* 章节阅读器 */}
{readingChapter && (
<ChapterReader
+31 -202
View File
@@ -1,11 +1,13 @@
import { useState, useEffect, useMemo, useRef } from 'react';
import { Button, List, Modal, Form, Input, message, Empty, Space, Popconfirm, Card, Select, Radio, Tag, InputNumber, Tabs, Pagination, theme, Progress, Badge, Tooltip } from 'antd';
import { EditOutlined, DeleteOutlined, ThunderboltOutlined, BranchesOutlined, AppstoreAddOutlined, CheckCircleOutlined, ExclamationCircleOutlined, PlusOutlined, FileTextOutlined, ClockCircleOutlined, ReloadOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons';
import { useState, useEffect, useMemo } from 'react';
import { Button, List, Modal, Form, Input, message, Empty, Space, Popconfirm, Card, Select, Radio, Tag, InputNumber, Tabs, Pagination, theme } from 'antd';
import { EditOutlined, DeleteOutlined, ThunderboltOutlined, BranchesOutlined, AppstoreAddOutlined, CheckCircleOutlined, ExclamationCircleOutlined, PlusOutlined, FileTextOutlined } from '@ant-design/icons';
import { useStore } from '../store';
import { eventBus } from '../store/eventBus';
import { getProjectTasks, type TaskStatus } from '../services/backgroundTaskService';
import { useOutlineSync } from '../store/hooks';
import { SSEPostClient } from '../utils/sseClient';
import { SSEProgressModal } from '../components/SSEProgressModal';
import { generateOutlineBackground, getProjectTasks, cancelTask, deleteTask, type TaskStatus } from '../services/backgroundTaskService';
import { generateOutlineBackground } from '../services/backgroundTaskService';
import { outlineApi, chapterApi, projectApi, characterApi } from '../services/api';
import type { OutlineExpansionResponse, BatchOutlineExpansionResponse, ChapterPlanItem, ApiError, Character } from '../types';
@@ -155,13 +157,7 @@ export default function Outline() {
const [sseMessage, setSSEMessage] = useState('');
const [sseModalVisible, setSSEModalVisible] = useState(false);
// 后台任务取消函数引用
const cancelGenerateRef = useRef<(() => void) | null>(null);
// 后台任务列表状态
const [taskListVisible, setTaskListVisible] = useState(false);
const [taskList, setTaskList] = useState<TaskStatus[]>([]);
const [taskListLoading, setTaskListLoading] = useState(false);
useEffect(() => {
const handleResize = () => {
@@ -190,10 +186,26 @@ export default function Outline() {
refreshOutlines();
// 加载项目角色列表
loadProjectCharacters();
// 检查是否有活跃的大纲生成任务,恢复按钮禁用状态
checkActiveOutlineTasks();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentProject?.id]); // 只依赖 ID,不依赖函数
// 检查是否有活跃的大纲生成任务(页面切换后恢复状态)
const checkActiveOutlineTasks = async () => {
if (!currentProject?.id) return;
try {
const result = await getProjectTasks(currentProject.id, 'outline_new', 5);
const result2 = await getProjectTasks(currentProject.id, 'outline_continue', 5);
const allTasks = [...(result.items || []), ...(result2.items || [])];
const hasActive = allTasks.some((t: TaskStatus) => t.status === 'running' || t.status === 'pending');
setIsGenerating(hasActive);
} catch (error) {
console.error('检查活跃大纲任务失败:', error);
}
};
// 加载项目角色列表
const loadProjectCharacters = async () => {
if (!currentProject?.id) return;
@@ -546,11 +558,6 @@ export default function Outline() {
// 关闭生成表单Modal
Modal.destroyAll();
// 显示进度Modal
setSSEProgress(0);
setSSEMessage('正在连接AI服务...');
setSSEModalVisible(true);
// 准备请求数据
const requestData: OutlineGenerateRequestData = {
project_id: currentProject.id,
@@ -583,35 +590,30 @@ export default function Outline() {
console.log('=========================');
// 使用后台任务生成(不怕断连,关闭浏览器也继续运行)
setSSEMessage('正在创建后台任务...');
const cancelFn = await generateOutlineBackground(
// 不再强制显示进度弹窗,任务进度在右下角悬浮任务框中显示
await generateOutlineBackground(
requestData,
(status) => {
setSSEProgress(status.progress);
setSSEMessage(status.status_message || '处理中...');
() => {
// 进度更新由悬浮任务框处理,无需额外操作
},
(result) => {
message.success(result.task_result?.message as string || '大纲生成完成!');
setSSEModalVisible(false);
setIsGenerating(false);
cancelGenerateRef.current = null;
refreshOutlines();
},
(error) => {
message.error(`生成失败: ${error}`);
setSSEModalVisible(false);
setIsGenerating(false);
cancelGenerateRef.current = null;
}
);
cancelGenerateRef.current = cancelFn;
message.info('大纲生成任务已提交,可在右下角任务面板查看进度');
// 通知悬浮任务框刷新
eventBus.emit('background-task-created');
} catch (error) {
console.error('AI生成失败:', error);
message.error('AI生成失败');
setSSEModalVisible(false);
setIsGenerating(false);
}
};
@@ -1902,168 +1904,8 @@ export default function Outline() {
};
// 加载并显示后台任务列表
const showTaskListModal = async () => {
if (!currentProject?.id) return;
setTaskListVisible(true);
setTaskListLoading(true);
try {
const result = await getProjectTasks(currentProject.id);
setTaskList(result.items || []);
} catch (error) {
message.error('加载任务列表失败');
} finally {
setTaskListLoading(false);
}
};
// 刷新任务列表
const refreshTaskList = async () => {
if (!currentProject?.id) return;
setTaskListLoading(true);
try {
const result = await getProjectTasks(currentProject.id);
setTaskList(result.items || []);
} catch (error) {
console.error('刷新任务列表失败:', error);
} finally {
setTaskListLoading(false);
}
};
// 获取任务状态标签
const getTaskStatusTag = (status: TaskStatus['status']) => {
switch (status) {
case 'pending': return <Tag icon={<ClockCircleOutlined />} color="default"></Tag>;
case 'running': return <Tag icon={<LoadingOutlined />} color="processing"></Tag>;
case 'completed': return <Tag icon={<CheckCircleOutlined />} color="success"></Tag>;
case 'failed': return <Tag icon={<CloseCircleOutlined />} color="error"></Tag>;
case 'cancelled': return <Tag icon={<CloseCircleOutlined />} color="default"></Tag>;
default: return <Tag>{status}</Tag>;
}
};
// 获取任务类型标签
const getTaskTypeLabel = (taskType: string) => {
switch (taskType) {
case 'outline_new': return '大纲生成';
case 'outline_continue': return '大纲续写';
default: return taskType;
}
};
// 处理取消后台任务
const handleCancelTask = async (taskId: string) => {
try {
await cancelTask(taskId);
message.success('任务已取消');
refreshTaskList();
} catch (error) {
message.error('取消任务失败');
}
};
// 处理删除任务记录
const handleDeleteTask = async (taskId: string) => {
try {
await deleteTask(taskId);
message.success('任务记录已删除');
refreshTaskList();
} catch (error) {
message.error('删除任务记录失败');
}
};
return (
<>
{/* 后台任务列表 Modal */}
<Modal
title={
<Space>
<ClockCircleOutlined />
<span></span>
{taskList.filter(t => t.status === 'running' || t.status === 'pending').length > 0 && (
<Badge count={taskList.filter(t => t.status === 'running' || t.status === 'pending').length} />
)}
</Space>
}
open={taskListVisible}
onCancel={() => setTaskListVisible(false)}
width={isMobile ? '95%' : 700}
centered
footer={
<Space>
<Button icon={<ReloadOutlined />} onClick={refreshTaskList} loading={taskListLoading}>
</Button>
<Button onClick={() => setTaskListVisible(false)}>
</Button>
</Space>
}
>
{taskListLoading && taskList.length === 0 ? (
<div style={{ textAlign: 'center', padding: 40 }}>
<LoadingOutlined style={{ fontSize: 24 }} />
<div style={{ marginTop: 12, color: token.colorTextSecondary }}>...</div>
</div>
) : taskList.length === 0 ? (
<Empty description="暂无后台任务" />
) : (
<List
dataSource={taskList}
renderItem={(task) => (
<List.Item
key={task.id}
actions={[
...(task.status === 'running' || task.status === 'pending'
? [<Button key="cancel" size="small" danger onClick={() => handleCancelTask(task.id)}></Button>]
: []
),
...(task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled'
? [<Button key="delete" size="small" type="link" danger onClick={() => handleDeleteTask(task.id)}></Button>]
: []
),
].filter(Boolean)}
>
<List.Item.Meta
title={
<Space>
{getTaskStatusTag(task.status)}
<span>{getTaskTypeLabel(task.task_type)}</span>
{task.status === 'running' || task.status === 'pending' ? (
<Progress percent={task.progress} size="small" style={{ width: 120 }} />
) : null}
</Space>
}
description={
<div>
<div style={{ fontSize: 12, color: token.colorTextSecondary }}>
{task.status_message || '无状态信息'}
</div>
<div style={{ fontSize: 11, color: token.colorTextTertiary, marginTop: 4 }}>
: {task.created_at ? new Date(task.created_at).toLocaleString() : '-'}
{task.completed_at && ` | 完成: ${new Date(task.completed_at).toLocaleString()}`}
</div>
{task.error_message && (
<div style={{ fontSize: 12, color: token.colorError, marginTop: 4 }}>
{task.error_message}
</div>
)}
{task.task_result && task.status === 'completed' && (
<div style={{ fontSize: 12, color: token.colorSuccess, marginTop: 4 }}>
{(task.task_result as Record<string, unknown>).message as string || '任务完成'}
</div>
)}
</div>
}
/>
</List.Item>
)}
/>
)}
</Modal>
{/* 批量展开预览 Modal */}
<Modal
title={
@@ -2092,13 +1934,9 @@ export default function Outline() {
message={sseMessage}
title="AI生成中(后台运行,可关闭页面)..."
onCancel={() => {
if (cancelGenerateRef.current) {
cancelGenerateRef.current();
cancelGenerateRef.current = null;
}
setSSEModalVisible(false);
setIsGenerating(false);
message.info('已取消生成任务');
setIsExpanding(false);
message.info('已取消操作');
}}
/>
@@ -2153,15 +1991,6 @@ export default function Outline() {
>
{isMobile ? 'AI生成/续写' : 'AI生成/续写大纲'}
</Button>
<Tooltip title="查看后台任务进度">
<Button
icon={<ClockCircleOutlined />}
onClick={showTaskListModal}
block={isMobile}
>
{isMobile ? '任务' : '后台任务'}
</Button>
</Tooltip>
{outlines.length > 0 && currentProject?.outline_mode === 'one-to-many' && (
<Button
icon={<AppstoreAddOutlined />}
+4
View File
@@ -26,6 +26,7 @@ import { projectApi } from '../services/api';
import ThemeSwitch from '../components/ThemeSwitch';
import { useThemeMode } from '../theme/useThemeMode';
import { getStoredSidebarCollapsed, setStoredSidebarCollapsed } from '../utils/sidebarState';
import FloatingTaskPanel from '../components/FloatingTaskPanel';
const { Header, Sider, Content } = Layout;
@@ -656,6 +657,9 @@ export default function ProjectDetail() {
</Content>
</Layout>
</Layout>
{/* 悬浮任务框 */}
{projectId && <FloatingTaskPanel projectId={projectId} />}
</Layout>
);
}