Files
deerflow-code/frontend-web/src/strategy-components/components/task-components/TaskAnalyzes.tsx
T
2026-06-09 19:41:48 +08:00

474 lines
20 KiB
TypeScript

import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ArrowRight, Bot, Layers, Plus, Search, Users, X } from 'lucide-react';
import { DateRangePicker, DateRangePickerProps, DateRangeValue, Loading } from 'tdesign-react';
import dayjs from 'dayjs';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@/contexts/ThemeContext';
import { requestCrudService } from '@/service/request-axios-service';
import { toast } from 'sonner';
import { KanbanColumn } from './KanbanColumn';
import { TaskListItem } from './TaskManagement';
import { listBusinessMappings } from '@/strategy-components/api/business-mapping';
interface TaskListPageProps {
title: string;
category: string;
subCategory: string;
}
interface StateItem {
startTime: string;
endTime: string;
task: TaskListItem | null;
}
type ColumnType = 'unanalyzed' | 'running' | 'completed';
const service = requestCrudService;
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
/** 本页对应的业务码(任务FX = 6BF);用于查业务映射拿到要自动选用的业务链 / 单智能体。 */
const BUSINESS_CODE = '6BF';
const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategory }) => {
const { theme } = useTheme();
const navigate = useNavigate();
const isDark = theme === 'dark';
const [searchQuery, setSearchQuery] = useState('');
const [showAnalysisModal, setShowAnalysisModal] = useState(false);
const [selectedTask, setSelectedTask] = useState<TaskListItem | null>(null);
const [unanalyzedTotal, setUnanalyzedTotal] = useState(0);
const [runningTotal, setRunningTotal] = useState(0);
const [completedTotal, setCompletedTotal] = useState(0);
const [unanalyzedList, setUnanalyzedList] = useState<TaskListItem[]>([]);
const [runningList, setRunningList] = useState<TaskListItem[]>([]);
const [completedList, setCompletedList] = useState<TaskListItem[]>([]);
const [unanalyzedLoading, setUnanalyzedLoading] = useState(false);
const [runningLoading, setRunningLoading] = useState(false);
const [completedLoading, setCompletedLoading] = useState(false);
const [dateRange, setDateRange] = useState<DateRangeValue>([]);
const stateRef = useRef<StateItem>({
startTime: '',
endTime: '',
task: null,
});
const presets = useMemo<DateRangePickerProps['presets']>(
() => ({
最近7天: [dayjs().subtract(6, 'day').toDate(), dayjs().toDate()],
最近3天: [dayjs().subtract(2, 'day').toDate(), dayjs().toDate()],
今天: [dayjs().toDate(), dayjs().toDate()],
}),
[],
);
const taskStatusMap: Record<ColumnType, number[]> = {
unanalyzed: [0, 10],
running: [20],
completed: [25, 30, 40, 60, 70, 80, 90, 100, 110],
};
const setRangeChange = async (value: DateRangeValue) => {
setDateRange(value);
stateRef.current.startTime = '';
stateRef.current.endTime = '';
if (Array.isArray(value) && value.length > 1) {
stateRef.current.startTime = String(value[0] || '');
stateRef.current.endTime = String(value[1] || '');
}
await taskDataChange();
};
const taskListSearch = async (
taskType: ColumnType,
pageNum = 1,
pageSize = 10,
) => {
if (taskType === 'unanalyzed') setUnanalyzedLoading(true);
if (taskType === 'running') setRunningLoading(true);
if (taskType === 'completed') setCompletedLoading(true);
try {
const response = await service.post(`${zncmUrl}/task/cop-tasks`, {
content: searchQuery,
pageNum,
pageSize,
taskStatus: taskStatusMap[taskType],
startTime: stateRef.current.startTime,
endTime: stateRef.current.endTime,
taskDirection: '',
});
const result = response?.data ?? {};
const records = Array.isArray(result.records) ? result.records : [];
const normalized = records.map((item: TaskListItem) => ({
...item,
column: taskType,
taskLevel: item.taskLevel || 1,
}));
const total = Number(result.total ?? normalized.length);
if (taskType === 'unanalyzed') {
setUnanalyzedTotal(total);
setUnanalyzedList(normalized);
} else if (taskType === 'running') {
setRunningTotal(total);
setRunningList(normalized);
} else {
setCompletedTotal(total);
setCompletedList(normalized);
}
} finally {
if (taskType === 'unanalyzed') setUnanalyzedLoading(false);
if (taskType === 'running') setRunningLoading(false);
if (taskType === 'completed') setCompletedLoading(false);
}
};
const pageIndexChange = async (type: string, pageIndex: number) => {
await taskListSearch(type as ColumnType, pageIndex);
};
const taskDataChange = async (pageNum = 1, pageSize = 10) => {
await Promise.all([
taskListSearch('unanalyzed', pageNum, pageSize),
taskListSearch('running', pageNum, pageSize),
taskListSearch('completed', pageNum, pageSize),
]);
};
const handleStartAnalysis = (task: TaskListItem) => {
setSelectedTask(task);
stateRef.current.task = task;
setShowAnalysisModal(true);
};
const handleOpenTaskDetail = () => {
if (!stateRef.current.task) return;
window.open(`https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-info-search?id=${stateRef.current.task.id}`);
setShowAnalysisModal(false);
};
const handleOpenConsultation = () => {
const task = stateRef.current.task;
if (!task) return;
// 会商自动化串联(模块 C):跳转圆桌规划页,带上任务名称/描述 + 本页业务码(6BF=任务FX)。
// 圆桌据此自动开跑 Step 1,并在「进入研讨」时自动选用 6BF 配置的业务链(跳过选链弹窗)。
navigate('/page/strategy/qa/multi-agent-roundtable', {
state: {
roundtablePrefill: {
taskTitle: task.taskName,
taskGoal: task.overview,
businessCode: BUSINESS_CODE,
autoStart: true,
},
},
});
setShowAnalysisModal(false);
};
// 单智能体分析:用「业务链条管理」里为本业务配置的智能体,跳转到其问答页(新会话),
// 把任务名称/简介作为开场消息暂存,由 AgentChatPage 读取 sessionStorage.pendingAgentMessage
// 自动发出并开始问答。未配置智能体则提示去配置。
const handleSingleAgentAnalysis = async () => {
const task = stateRef.current.task;
if (!task) return;
let agentId: string | null = null;
try {
const mappings = await listBusinessMappings();
agentId = mappings.find((m) => m.businessCode === BUSINESS_CODE)?.agentId ?? null;
} catch {
/* ignore → 下面按未配置处理 */
}
if (!agentId) {
toast.error('该业务尚未在「业务链条管理」中配置单智能体');
return;
}
const text = [
task.taskName ? `任务名称:${task.taskName}` : '',
task.overview ? `任务简介:${task.overview}` : '',
]
.filter(Boolean)
.join('\n');
try {
sessionStorage.setItem(
'pendingAgentMessage',
JSON.stringify({ agentId, text }),
);
} catch {
/* noop */
}
navigate(`/page/strategy/agents/${agentId}/chats/new`);
setShowAnalysisModal(false);
};
const handleKeyDown = async (event: React.KeyboardEvent<HTMLInputElement>) => {
if ((event.metaKey || event.ctrlKey || event.shiftKey) && event.key === 'Enter') {
event.preventDefault();
} else if (event.key === 'Enter') {
event.preventDefault();
await taskDataChange();
} else if (event.key === '/') {
event.preventDefault();
}
};
useEffect(() => {
taskDataChange();
}, []);
const shellClass = isDark
? 'strategy-data h-full overflow-y-auto bg-[radial-gradient(circle_at_top_left,rgba(27,94,168,0.28),transparent_32%),linear-gradient(180deg,#081322_0%,#0d1d33_42%,#0a1628_100%)] text-slate-100'
: 'strategy-data h-full overflow-y-auto bg-[radial-gradient(circle_at_top_left,rgba(245,34,45,0.08),transparent_26%),linear-gradient(180deg,#fffdfd_0%,#fff7f7_42%,#fafafa_100%)] text-[#222222]';
const metricCardClass = isDark
? 'border border-[rgba(110,136,176,0.24)] bg-[linear-gradient(180deg,rgba(13,31,55,0.98),rgba(8,19,34,0.98))] shadow-[0_14px_42px_rgba(2,8,20,0.30)]'
: 'border border-[#E8E8E8] bg-white shadow-[0_12px_30px_rgba(245,34,45,0.06)]';
const inputClass = isDark
? 'border-[rgba(110,136,176,0.3)] bg-[rgba(8,23,42,0.92)] text-slate-100 placeholder:text-slate-500'
: 'border-[#E8E8E8] bg-white text-[#222222] placeholder:text-[#A6A6A6] shadow-[0_4px_14px_rgba(245,34,45,0.04)]';
const panelGlowClass = isDark
? 'before:pointer-events-none before:absolute before:inset-x-8 before:top-0 before:h-px before:bg-[linear-gradient(90deg,transparent,rgba(96,165,250,0.45),transparent)]'
: '';
const metrics = [
{
label: '全部任务',
value: unanalyzedTotal + runningTotal + completedTotal,
icon: Layers,
accent: 'from-[#4D8DFF] to-[#7CC9FF]',
},
{
label: '未分析',
value: unanalyzedTotal,
icon: Bot,
accent: 'from-[#7C9BFF] to-[#5EEAD4]',
},
{
label: '分析中',
value: runningTotal,
icon: Layers,
accent: 'from-[#22C55E] to-[#67E8F9]',
},
{
label: '已完成',
value: completedTotal,
icon: Bot,
accent: 'from-[#38BDF8] to-[#A78BFA]',
},
];
return (
<div className={shellClass}>
<div className={`relative mx-auto flex min-h-full max-w-[1680px] flex-col gap-5 px-6 py-6 lg:px-8 ${panelGlowClass}`}>
<div className="flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
<div className="space-y-3">
<div className="flex items-center gap-3">
<div>
<h1 className={`text-[26px] font-semibold tracking-[0.02em] ${isDark ? 'text-slate-50' : 'text-[#222222]'}`}>
{title}
</h1>
<p className={`mt-1 text-sm ${isDark ? 'text-slate-300/80' : 'text-slate-500'}`}>
面向规划任务的任务分析看板,聚焦目标拆解、受众判断和策略阻力识别,支持快速检索、启动分析和查看会商结果。
</p>
</div>
</div>
<div className={`inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs ${isDark ? 'border-[rgba(110,136,176,0.28)] bg-[rgba(9,25,46,0.72)] text-slate-300' : 'border-slate-200 bg-white text-slate-600'}`}>
<span>{category}</span>
<ArrowRight className="h-3.5 w-3.5 opacity-70" />
<span>{subCategory}</span>
</div>
</div>
<div className="flex flex-col gap-3 xl:items-end">
<div className="flex flex-wrap items-center gap-3">
<label className={`flex h-11 min-w-[320px] items-center gap-3 rounded-2xl border px-4 ${inputClass}`}>
<Search className="h-4 w-4 text-[#7CC9FF]" />
<input
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
onKeyDown={handleKeyDown}
className="w-full bg-transparent text-sm outline-none"
placeholder="搜索任务名称、摘要或负责人"
/>
</label>
<DateRangePicker
className="sentiment-date-range"
value={dateRange}
presets={presets}
enableTimePicker
clearable
popupProps={{ overlayClassName: 'sentiment-date-range-popup' }}
onChange={setRangeChange}
/>
</div>
<button
type="button"
className={`inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm ${isDark ? 'bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]' : 'bg-[#FFF1F0] text-[#CF1322]'}`}
>
<Plus className="h-4 w-4" />
新建任务
</button>
</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
{metrics.map(({ label, value, icon: Icon, accent }) => (
<div key={label} className={`rounded-3xl p-5 ${metricCardClass}`}>
<div className="flex items-center justify-between">
<div>
<p className={`text-sm ${isDark ? 'text-slate-300/78' : 'text-slate-500'}`}>{label}</p>
<p className={`mt-3 text-3xl font-semibold tracking-[0.03em] ${isDark ? 'text-white' : 'text-[#222222]'}`}>
{value}
</p>
</div>
<div className={`flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br ${accent} shadow-[inset_0_1px_0_rgba(255,255,255,0.18)]`}>
<Icon className="h-5 w-5 text-white" />
</div>
</div>
</div>
))}
</div>
<div className="grid auto-rows-auto grid-cols-1 items-stretch gap-4 xl:grid-cols-3">
<div className="h-full self-stretch">
<Loading className="block h-full" loading={unanalyzedLoading} showOverlay={false}>
<div className="h-full">
<KanbanColumn
title="未分析"
count={unanalyzedTotal}
color="bg-[#7C9BFF]"
tasks={unanalyzedList}
taskType="unanalyzed"
pageIndexSearch={pageIndexChange}
handleStartAnalysis={handleStartAnalysis}
/>
</div>
</Loading>
</div>
<div className="h-full self-stretch">
<Loading className="block h-full" loading={runningLoading} showOverlay={false}>
<div className="h-full">
<KanbanColumn
title="分析中"
count={runningTotal}
color="bg-[#22C55E]"
tasks={runningList}
taskType="running"
pageIndexSearch={pageIndexChange}
handleStartAnalysis={handleStartAnalysis}
/>
</div>
</Loading>
</div>
<div className="h-full self-stretch">
<Loading className="block h-full" loading={completedLoading} showOverlay={false}>
<div className="h-full">
<KanbanColumn
title="已完成"
count={completedTotal}
color="bg-[#38BDF8]"
tasks={completedList}
taskType="completed"
pageIndexSearch={pageIndexChange}
handleStartAnalysis={handleStartAnalysis}
/>
</div>
</Loading>
</div>
</div>
</div>
{showAnalysisModal && selectedTask ? (
<div className="fixed inset-0 z-[2000] flex items-center justify-center bg-[rgba(3,10,22,0.62)] px-4 backdrop-blur-sm">
<div className={`w-full max-w-[560px] rounded-[28px] border p-6 ${isDark ? 'border-[rgba(110,136,176,0.26)] bg-[linear-gradient(180deg,rgba(10,29,54,0.98),rgba(8,20,37,0.98))] text-slate-100 shadow-[0_28px_80px_rgba(2,8,20,0.42)]' : 'border-slate-200 bg-white text-slate-900 shadow-xl'}`}>
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-xl font-semibold">选择分析模式</h3>
<p className={`mt-2 text-sm ${isDark ? 'text-slate-300/78' : 'text-slate-500'}`}>
当前任务:{selectedTask.taskName}
</p>
</div>
<button
type="button"
onClick={() => setShowAnalysisModal(false)}
className={`rounded-xl p-2 transition ${isDark ? 'hover:bg-[rgba(15,41,74,0.88)]' : 'hover:bg-slate-100'}`}
>
<X className="h-4 w-4" />
</button>
</div>
<div className="mt-6 grid gap-3">
<button
type="button"
onClick={handleOpenTaskDetail}
className={`group flex items-center justify-between rounded-2xl border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(110,136,176,0.24)] bg-[rgba(10,30,54,0.9)] hover:border-[rgba(96,165,250,0.48)] hover:bg-[rgba(12,38,69,0.96)]' : 'border-slate-200 bg-slate-50 hover:border-sky-200 hover:bg-sky-50'}`}
>
<div className="flex items-center gap-4">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[linear-gradient(180deg,#1677FF,#0EA5E9)]">
<Layers className="h-5 w-5 text-white" />
</div>
<div>
<div className="font-medium">查看任务详情</div>
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300/72' : 'text-slate-500'}`}>
进入 B1 系统查看任务详情。
</div>
</div>
</div>
<ArrowRight className="h-4 w-4 opacity-70 transition group-hover:translate-x-0.5" />
</button>
<button
type="button"
onClick={handleOpenConsultation}
className={`group flex items-center justify-between rounded-2xl border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(110,136,176,0.24)] bg-[rgba(10,30,54,0.9)] hover:border-[rgba(45,212,191,0.48)] hover:bg-[rgba(10,42,58,0.96)]' : 'border-slate-200 bg-slate-50 hover:border-emerald-200 hover:bg-emerald-50'}`}
>
<div className="flex items-center gap-4">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[linear-gradient(180deg,#14B8A6,#22C55E)]">
<Users className="h-5 w-5 text-white" />
</div>
<div>
<div className="font-medium">多智能体会商</div>
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300/72' : 'text-slate-500'}`}>
多个 AI 智能体从不同视角开展讨论并形成共识。
</div>
</div>
</div>
<ArrowRight className="h-4 w-4 opacity-70 transition group-hover:translate-x-0.5" />
</button>
<button
type="button"
onClick={handleSingleAgentAnalysis}
className={`group flex items-center justify-between rounded-2xl border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(110,136,176,0.24)] bg-[rgba(10,30,54,0.9)] hover:border-[rgba(167,139,250,0.48)] hover:bg-[rgba(28,24,58,0.96)]' : 'border-slate-200 bg-slate-50 hover:border-violet-200 hover:bg-violet-50'}`}
>
<div className="flex items-center gap-4">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[linear-gradient(180deg,#7C3AED,#A78BFA)]">
<Bot className="h-5 w-5 text-white" />
</div>
<div>
<div className="font-medium">单智能体分析</div>
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300/72' : 'text-slate-500'}`}>
由单个 AI 智能体直接对该任务进行分析问答。
</div>
</div>
</div>
<ArrowRight className="h-4 w-4 opacity-70 transition group-hover:translate-x-0.5" />
</button>
</div>
</div>
</div>
) : null}
</div>
);
};
export default TaskAnalyzes;