2026-07-21 00:46:09 +08:00
|
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
|
|
|
|
import { Card, Empty, Grid, Input, Modal, Select, Space, Switch, Tag, Typography } from '@arco-design/web-react'
|
2026-07-20 18:25:04 +08:00
|
|
|
|
import {
|
|
|
|
|
|
IconCalendar,
|
|
|
|
|
|
IconCheckCircleFill,
|
|
|
|
|
|
IconDashboard,
|
|
|
|
|
|
IconFile,
|
|
|
|
|
|
IconRobot,
|
|
|
|
|
|
} from '@arco-design/web-react/icon'
|
2026-07-21 00:46:09 +08:00
|
|
|
|
import type { Project, ProjectWorkspace, TaskItem } from './projects/project-types'
|
2026-07-20 18:25:04 +08:00
|
|
|
|
|
|
|
|
|
|
const { Row, Col } = Grid
|
|
|
|
|
|
const { Title, Text, Paragraph } = Typography
|
2026-07-21 00:46:09 +08:00
|
|
|
|
const { TextArea } = Input
|
2026-07-20 18:25:04 +08:00
|
|
|
|
|
2026-07-21 00:46:09 +08:00
|
|
|
|
export type WorkspaceTaskUpdate = {
|
|
|
|
|
|
originalProjectId: string
|
|
|
|
|
|
nextProjectId: string
|
|
|
|
|
|
taskId: string
|
|
|
|
|
|
title: string
|
|
|
|
|
|
summary: string
|
|
|
|
|
|
tag: string
|
|
|
|
|
|
completed: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type WorkspaceTask = TaskItem & {
|
|
|
|
|
|
project: Project
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type TaskDraft = {
|
|
|
|
|
|
title: string
|
|
|
|
|
|
summary: string
|
|
|
|
|
|
projectId: string
|
|
|
|
|
|
tag: string
|
|
|
|
|
|
completed: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function WorkspacePage({
|
|
|
|
|
|
workspaces,
|
|
|
|
|
|
onUpdateTask,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
workspaces: ProjectWorkspace[]
|
|
|
|
|
|
onOpenTask: (project: Project, taskID: string) => void
|
|
|
|
|
|
onUpdateTask: (update: WorkspaceTaskUpdate) => void
|
|
|
|
|
|
}) {
|
2026-07-20 18:25:04 +08:00
|
|
|
|
const projects = workspaces.map((workspace) => workspace.project)
|
2026-07-21 00:46:09 +08:00
|
|
|
|
const unfinishedTasks: WorkspaceTask[] = workspaces.flatMap((workspace) =>
|
2026-07-20 18:25:04 +08:00
|
|
|
|
workspace.tasks
|
|
|
|
|
|
.filter((task) => !task.completed)
|
|
|
|
|
|
.map((task) => ({
|
|
|
|
|
|
...task,
|
|
|
|
|
|
project: workspace.project,
|
|
|
|
|
|
})),
|
|
|
|
|
|
)
|
2026-07-21 00:46:09 +08:00
|
|
|
|
const [editingTask, setEditingTask] = useState<WorkspaceTask | null>(null)
|
|
|
|
|
|
const [draft, setDraft] = useState<TaskDraft>({ title: '', summary: '', projectId: '', tag: '', completed: false })
|
|
|
|
|
|
const tagOptions = useMemo(() => {
|
|
|
|
|
|
const selectedWorkspace = workspaces.find((workspace) => workspace.project.id === draft.projectId)
|
2026-07-21 01:00:27 +08:00
|
|
|
|
return selectedWorkspace?.tags.filter((tag) => tag !== 'all' && tag !== '全部') ?? []
|
|
|
|
|
|
}, [draft.projectId, workspaces])
|
2026-07-21 00:46:09 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!editingTask) return
|
|
|
|
|
|
setDraft({
|
|
|
|
|
|
title: editingTask.title,
|
|
|
|
|
|
summary: editingTask.summary,
|
|
|
|
|
|
projectId: editingTask.project.id,
|
|
|
|
|
|
tag: editingTask.tag,
|
|
|
|
|
|
completed: editingTask.completed,
|
|
|
|
|
|
})
|
|
|
|
|
|
}, [editingTask])
|
2026-07-21 01:00:27 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!draft.tag || tagOptions.includes(draft.tag)) return
|
|
|
|
|
|
setDraft((value) => ({ ...value, tag: '' }))
|
|
|
|
|
|
}, [draft.tag, tagOptions])
|
2026-07-20 18:25:04 +08:00
|
|
|
|
const aiSessions = workspaces.flatMap((workspace) => workspace.aiSessions)
|
|
|
|
|
|
const notes = workspaces.flatMap((workspace) => workspace.notes)
|
|
|
|
|
|
const urgentProjects = projects.filter((project) => project.urgent).length
|
|
|
|
|
|
const workspaceMetrics = [
|
|
|
|
|
|
{ title: '项目总数', value: projects.length, delta: `${urgentProjects} 个高优先级`, icon: <IconDashboard />, color: 'arcoblue' },
|
|
|
|
|
|
{ title: '未完成任务', value: unfinishedTasks.length, delta: '跨项目聚合', icon: <IconCheckCircleFill />, color: 'green' },
|
|
|
|
|
|
{ title: 'AI 会话', value: aiSessions.length, delta: '项目内上下文', icon: <IconRobot />, color: 'purple' },
|
|
|
|
|
|
{ title: '知识资料', value: notes.length, delta: '笔记与资料合计', icon: <IconFile />, color: 'cyan' },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="workspace-page overview-page">
|
|
|
|
|
|
<div className="overview-head">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Title heading={4}>工作台</Title>
|
|
|
|
|
|
<Text type="secondary">所有项目的统一工作台,汇总项目任务、AI 会话和知识资产。</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Row gutter={12} className="metric-row">
|
|
|
|
|
|
{workspaceMetrics.map((metric) => (
|
|
|
|
|
|
<Col span={24 / workspaceMetrics.length} key={metric.title}>
|
|
|
|
|
|
<Card className="metric-card" bordered>
|
|
|
|
|
|
<Space align="start">
|
|
|
|
|
|
<Tag color={metric.color}>{metric.icon}</Tag>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Text type="secondary">{metric.title}</Text>
|
|
|
|
|
|
<Title heading={3}>{metric.value}</Title>
|
|
|
|
|
|
<Text className="metric-delta">{metric.delta}</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
<Card className="queue-section" bordered>
|
|
|
|
|
|
<div className="section-header">
|
|
|
|
|
|
<Title heading={6}>未完成的任务</Title>
|
|
|
|
|
|
<Tag color="arcoblue">{unfinishedTasks.length}</Tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{unfinishedTasks.length ? (
|
|
|
|
|
|
<div className="workspace-task-list">
|
|
|
|
|
|
{unfinishedTasks.map((task) => (
|
2026-07-21 00:46:09 +08:00
|
|
|
|
<button key={`${task.project.id}-${task.id}`} className="workspace-task-card" onClick={() => setEditingTask(task)}>
|
2026-07-20 18:25:04 +08:00
|
|
|
|
<div className="workspace-task-main">
|
|
|
|
|
|
<span className="workspace-task-title">
|
|
|
|
|
|
<IconCheckCircleFill />
|
|
|
|
|
|
{task.title}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<Paragraph ellipsis={{ rows: 2 }} type="secondary">
|
|
|
|
|
|
{task.summary || '暂无任务说明'}
|
|
|
|
|
|
</Paragraph>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="workspace-task-meta">
|
|
|
|
|
|
<Tag color={task.project.urgent ? 'red' : 'arcoblue'}>{task.project.name}</Tag>
|
|
|
|
|
|
<span><IconCalendar /> {task.createdAt}</span>
|
|
|
|
|
|
<Tag color="green">{task.tag}</Tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Empty description="暂无未完成任务" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
2026-07-21 00:46:09 +08:00
|
|
|
|
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
className="action-modal"
|
|
|
|
|
|
title="编辑任务"
|
|
|
|
|
|
visible={Boolean(editingTask)}
|
|
|
|
|
|
onCancel={() => setEditingTask(null)}
|
|
|
|
|
|
onOk={() => {
|
|
|
|
|
|
if (!editingTask) return
|
|
|
|
|
|
onUpdateTask({
|
|
|
|
|
|
originalProjectId: editingTask.project.id,
|
|
|
|
|
|
nextProjectId: draft.projectId,
|
|
|
|
|
|
taskId: editingTask.id,
|
|
|
|
|
|
title: draft.title.trim() || editingTask.title,
|
|
|
|
|
|
summary: draft.summary.trim(),
|
|
|
|
|
|
tag: draft.tag.trim(),
|
|
|
|
|
|
completed: draft.completed,
|
|
|
|
|
|
})
|
|
|
|
|
|
setEditingTask(null)
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Space direction="vertical" size={12} className="action-form">
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<Text>任务标题</Text>
|
|
|
|
|
|
<Input value={draft.title} onChange={(title) => setDraft((value) => ({ ...value, title }))} />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<Text>任务内容</Text>
|
|
|
|
|
|
<TextArea rows={4} value={draft.summary} onChange={(summary) => setDraft((value) => ({ ...value, summary }))} />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<Text>所属项目</Text>
|
|
|
|
|
|
<Select value={draft.projectId} onChange={(projectId) => setDraft((value) => ({ ...value, projectId }))}>
|
|
|
|
|
|
{projects.map((project) => (
|
|
|
|
|
|
<Select.Option key={project.id} value={project.id}>{project.name}</Select.Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<Text>所属标签</Text>
|
|
|
|
|
|
<Select
|
2026-07-21 01:00:27 +08:00
|
|
|
|
allowClear
|
|
|
|
|
|
value={draft.tag || undefined}
|
|
|
|
|
|
placeholder="不设置标签"
|
|
|
|
|
|
onChange={(tag) => setDraft((value) => ({ ...value, tag: tag ?? '' }))}
|
2026-07-21 00:46:09 +08:00
|
|
|
|
>
|
|
|
|
|
|
{tagOptions.map((tag) => (
|
|
|
|
|
|
<Select.Option key={tag} value={tag}>{tag}</Select.Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label className="action-switch">
|
|
|
|
|
|
<Text>完成状态</Text>
|
|
|
|
|
|
<Switch checked={draft.completed} checkedText="完成" uncheckedText="未完成" onChange={(completed) => setDraft((value) => ({ ...value, completed }))} />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Modal>
|
2026-07-20 18:25:04 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|