feat: add project task edit modal

This commit is contained in:
2026-07-21 01:18:06 +08:00
parent d6395d3c64
commit 1c58825a7b
7 changed files with 174 additions and 15 deletions

View File

@@ -147,7 +147,7 @@ function mapTask(task: BackendTask): TaskItem {
createdAt: formatCreatedAt(task.createdAt), createdAt: formatCreatedAt(task.createdAt),
completedAt: task.completedAt ? formatCreatedAt(task.completedAt) : '', completedAt: task.completedAt ? formatCreatedAt(task.completedAt) : '',
duration: task.duration || '', duration: task.duration || '',
tag: task.tag || (task.completed ? '已完成' : '进行中'), tag: task.tag || '',
summary: task.summary, summary: task.summary,
completed: task.completed, completed: task.completed,
} }

View File

@@ -3,6 +3,7 @@ import { ProjectCron } from './project-cron'
import { ProjectNewChannel } from './project-new-channel' import { ProjectNewChannel } from './project-new-channel'
import { ProjectNotes } from './project-notes' import { ProjectNotes } from './project-notes'
import { ProjectOverview } from './project-overview' import { ProjectOverview } from './project-overview'
import type { ProjectTaskUpdate } from './project-task-edit-modal'
import { ProjectTasks } from './project-tasks' import { ProjectTasks } from './project-tasks'
import type { ChannelKey, ProjectWorkspace } from './project-types' import type { ChannelKey, ProjectWorkspace } from './project-types'
@@ -17,6 +18,7 @@ export function ProjectChannelPage({
onUploadSource, onUploadSource,
onCreateCronPlan, onCreateCronPlan,
onCreateProjectTag, onCreateProjectTag,
onUpdateTask,
}: { }: {
activeChannel: ChannelKey activeChannel: ChannelKey
activeWorkspace: ProjectWorkspace activeWorkspace: ProjectWorkspace
@@ -28,10 +30,11 @@ export function ProjectChannelPage({
onUploadSource: () => void onUploadSource: () => void
onCreateCronPlan: () => void onCreateCronPlan: () => void
onCreateProjectTag: (name: string) => void onCreateProjectTag: (name: string) => void
onUpdateTask: (update: ProjectTaskUpdate) => void
}) { }) {
switch (activeChannel) { switch (activeChannel) {
case 'tasks': case 'tasks':
return <ProjectTasks activeWorkspace={activeWorkspace} activeTaskID={activeTaskID} onOpenTask={onOpenTask} onCloseTask={onCloseTask} onSelectItem={onSelectItem} onCreateTask={onCreateTask} onCreateProjectTag={onCreateProjectTag} /> return <ProjectTasks activeWorkspace={activeWorkspace} activeTaskID={activeTaskID} onOpenTask={onOpenTask} onCloseTask={onCloseTask} onSelectItem={onSelectItem} onCreateTask={onCreateTask} onCreateProjectTag={onCreateProjectTag} onUpdateTask={onUpdateTask} />
case 'ai': case 'ai':
return <ProjectAi activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} /> return <ProjectAi activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
case 'notes': case 'notes':
@@ -42,6 +45,6 @@ export function ProjectChannelPage({
return <ProjectNewChannel activeWorkspace={activeWorkspace} /> return <ProjectNewChannel activeWorkspace={activeWorkspace} />
case 'overview': case 'overview':
default: default:
return <ProjectOverview activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} /> return <ProjectOverview activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} onUpdateTask={onUpdateTask} />
} }
} }

View File

@@ -1,16 +1,26 @@
import { useMemo } from 'react' import { useMemo, useState } from 'react'
import { Button, Card, Grid, Space, Tag, Typography } from '@arco-design/web-react' import { Button, Card, Grid, Space, Tag, Typography } from '@arco-design/web-react'
import { IconCheckCircleFill, IconClockCircle, IconFile, IconLink } from '@arco-design/web-react/icon' import { IconCheckCircleFill, IconClockCircle, IconFile, IconLink } from '@arco-design/web-react/icon'
import { ProjectFileGrid } from './project-file-grid' import { ProjectFileGrid } from './project-file-grid'
import { ProjectTaskEditModal, type ProjectTaskUpdate } from './project-task-edit-modal'
import type { ProjectWorkspace, TaskItem } from './project-types' import type { ProjectWorkspace, TaskItem } from './project-types'
const { Row, Col } = Grid const { Row, Col } = Grid
const { Title, Text, Paragraph } = Typography const { Title, Text, Paragraph } = Typography
export function ProjectOverview({ activeWorkspace, onSelectItem }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void }) { export function ProjectOverview({
activeWorkspace,
onSelectItem,
onUpdateTask,
}: {
activeWorkspace: ProjectWorkspace
onSelectItem: (title: string) => void
onUpdateTask: (update: ProjectTaskUpdate) => void
}) {
const { project, tasks, notes, cronJobs } = activeWorkspace const { project, tasks, notes, cronJobs } = activeWorkspace
const runningTasks = useMemo(() => tasks.filter((task) => !task.completed), [tasks]) const runningTasks = useMemo(() => tasks.filter((task) => !task.completed), [tasks])
const completedTasks = useMemo(() => tasks.filter((task) => task.completed), [tasks]) const completedTasks = useMemo(() => tasks.filter((task) => task.completed), [tasks])
const [editingTask, setEditingTask] = useState<TaskItem | null>(null)
const shareURL = useMemo(() => projectShareURL(project.identifier || project.id), [project.id, project.identifier]) const shareURL = useMemo(() => projectShareURL(project.identifier || project.id), [project.id, project.identifier])
const metrics = useMemo(() => [ const metrics = useMemo(() => [
{ title: '进行中的任务', value: runningTasks.length, icon: <IconCheckCircleFill />, color: 'green' }, { title: '进行中的任务', value: runningTasks.length, icon: <IconCheckCircleFill />, color: 'green' },
@@ -52,14 +62,35 @@ export function ProjectOverview({ activeWorkspace, onSelectItem }: { activeWorks
))} ))}
</Row> </Row>
<TaskCardSection title={`进行中的任务(${runningTasks.length}`} tasks={runningTasks} emptyText="当前没有进行中的任务" onSelectItem={onSelectItem} /> <TaskCardSection title={`进行中的任务(${runningTasks.length}`} tasks={runningTasks} emptyText="当前没有进行中的任务" onEditTask={setEditingTask} />
<TaskCardSection title={`完成的任务(${completedTasks.length}`} tasks={completedTasks} emptyText="当前没有完成的任务" onSelectItem={onSelectItem} /> <TaskCardSection title={`完成的任务(${completedTasks.length}`} tasks={completedTasks} emptyText="当前没有完成的任务" onSelectItem={onSelectItem} />
<NoteSection notes={notes} onSelectItem={onSelectItem} /> <NoteSection notes={notes} onSelectItem={onSelectItem} />
<ProjectTaskEditModal
task={editingTask}
workspace={activeWorkspace}
onClose={() => setEditingTask(null)}
onSubmit={(update) => {
onUpdateTask(update)
setEditingTask(null)
}}
/>
</div> </div>
) )
} }
function TaskCardSection({ title, tasks, emptyText, onSelectItem }: { title: string; tasks: TaskItem[]; emptyText: string; onSelectItem: (title: string) => void }) { function TaskCardSection({
title,
tasks,
emptyText,
onSelectItem,
onEditTask,
}: {
title: string
tasks: TaskItem[]
emptyText: string
onSelectItem?: (title: string) => void
onEditTask?: (task: TaskItem) => void
}) {
return ( return (
<Card className="queue-section overview-task-section" bordered> <Card className="queue-section overview-task-section" bordered>
<div className="section-header"> <div className="section-header">
@@ -71,7 +102,7 @@ function TaskCardSection({ title, tasks, emptyText, onSelectItem }: { title: str
) : ( ) : (
<div className="overview-task-grid"> <div className="overview-task-grid">
{tasks.map((task) => ( {tasks.map((task) => (
<button key={task.id} className={task.completed ? 'overview-task-card completed' : 'overview-task-card'} onClick={() => onSelectItem(task.title)}> <button key={task.id} className={task.completed ? 'overview-task-card completed' : 'overview-task-card'} onClick={() => onEditTask ? onEditTask(task) : onSelectItem?.(task.title)}>
<span className="overview-task-title"> <span className="overview-task-title">
<IconCheckCircleFill /> <IconCheckCircleFill />
{task.title} {task.title}

View File

@@ -0,0 +1,93 @@
import { Input, Modal, Select, Space, Switch, Typography } from '@arco-design/web-react'
import { useEffect, useMemo, useState } from 'react'
import type { ProjectWorkspace, TaskItem } from './project-types'
const { Text } = Typography
const { TextArea } = Input
export type ProjectTaskUpdate = {
taskId: string
title: string
summary: string
tag: string
completed: boolean
}
type TaskDraft = {
title: string
summary: string
tag: string
completed: boolean
}
export function ProjectTaskEditModal({
task,
workspace,
onClose,
onSubmit,
}: {
task: TaskItem | null
workspace: ProjectWorkspace
onClose: () => void
onSubmit: (update: ProjectTaskUpdate) => void
}) {
const tagOptions = useMemo(() => workspace.tags.filter((tag) => tag !== 'all' && tag !== '全部'), [workspace.tags])
const [draft, setDraft] = useState<TaskDraft>({ title: '', summary: '', tag: '', completed: false })
useEffect(() => {
if (!task) return
setDraft({
title: task.title,
summary: task.summary,
tag: task.tag,
completed: task.completed,
})
}, [task])
return (
<Modal
className="action-modal"
title="编辑计划"
visible={Boolean(task)}
onCancel={onClose}
onOk={() => {
if (!task) return
onSubmit({
taskId: task.id,
title: draft.title.trim() || task.title,
summary: draft.summary.trim(),
tag: draft.tag.trim(),
completed: draft.completed,
})
}}
>
<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
allowClear
value={draft.tag || undefined}
placeholder="不设置标签"
onChange={(tag) => setDraft((value) => ({ ...value, tag: tag ?? '' }))}
>
{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>
)
}

View File

@@ -1,6 +1,7 @@
import { useState } from 'react' import { useState } from 'react'
import { Button, Card, Descriptions, Input, Modal, Progress, Space, Tag, Typography } from '@arco-design/web-react' import { Button, Card, Descriptions, Input, Modal, Progress, Space, Tag, Typography } from '@arco-design/web-react'
import { IconArrowLeft, IconCalendar, IconCheckCircle, IconCheckCircleFill, IconClockCircle, IconPlus } from '@arco-design/web-react/icon' import { IconArrowLeft, IconCalendar, IconCheckCircle, IconCheckCircleFill, IconClockCircle, IconPlus } from '@arco-design/web-react/icon'
import { ProjectTaskEditModal, type ProjectTaskUpdate } from './project-task-edit-modal'
import type { ProjectWorkspace, TaskItem } from './project-types' import type { ProjectWorkspace, TaskItem } from './project-types'
const { Title, Text, Paragraph } = Typography const { Title, Text, Paragraph } = Typography
@@ -12,6 +13,7 @@ export function ProjectTasks({
onCloseTask, onCloseTask,
onCreateTask, onCreateTask,
onCreateProjectTag, onCreateProjectTag,
onUpdateTask,
}: { }: {
activeWorkspace: ProjectWorkspace activeWorkspace: ProjectWorkspace
activeTaskID: string | null activeTaskID: string | null
@@ -20,6 +22,7 @@ export function ProjectTasks({
onSelectItem: (title: string) => void onSelectItem: (title: string) => void
onCreateTask: () => void onCreateTask: () => void
onCreateProjectTag: (name: string) => void onCreateProjectTag: (name: string) => void
onUpdateTask: (update: ProjectTaskUpdate) => void
}) { }) {
const { tasks, project, tags } = activeWorkspace const { tasks, project, tags } = activeWorkspace
const activeTask = tasks.find((task) => task.id === activeTaskID) const activeTask = tasks.find((task) => task.id === activeTaskID)
@@ -28,6 +31,7 @@ export function ProjectTasks({
const projectTags = tags.filter((tag) => tag !== 'all' && tag !== '全部') const projectTags = tags.filter((tag) => tag !== 'all' && tag !== '全部')
const [tagModalOpen, setTagModalOpen] = useState(false) const [tagModalOpen, setTagModalOpen] = useState(false)
const [tagName, setTagName] = useState('') const [tagName, setTagName] = useState('')
const [editingTask, setEditingTask] = useState<TaskItem | null>(null)
if (activeTask) { if (activeTask) {
return <TaskDetail activeWorkspace={activeWorkspace} task={activeTask} onBack={onCloseTask} /> return <TaskDetail activeWorkspace={activeWorkspace} task={activeTask} onBack={onCloseTask} />
@@ -71,7 +75,7 @@ export function ProjectTasks({
</div> </div>
<div className="task-plan-card-grid"> <div className="task-plan-card-grid">
{pendingTasks.map((task) => ( {pendingTasks.map((task) => (
<button className="task-plan-card" key={task.id} onClick={() => onOpenTask(task.id)}> <button className="task-plan-card" key={task.id} onClick={() => setEditingTask(task)}>
<span className="task-plan-title"><IconCheckCircleFill />{task.title}</span> <span className="task-plan-title"><IconCheckCircleFill />{task.title}</span>
<Paragraph ellipsis={{ rows: 2 }} type="secondary">{task.summary || '暂无正文'}</Paragraph> <Paragraph ellipsis={{ rows: 2 }} type="secondary">{task.summary || '暂无正文'}</Paragraph>
<div className="task-plan-meta"> <div className="task-plan-meta">
@@ -120,6 +124,15 @@ export function ProjectTasks({
</label> </label>
</Space> </Space>
</Modal> </Modal>
<ProjectTaskEditModal
task={editingTask}
workspace={activeWorkspace}
onClose={() => setEditingTask(null)}
onSubmit={(update) => {
onUpdateTask(update)
setEditingTask(null)
}}
/>
</div> </div>
) )
} }

View File

@@ -44,6 +44,7 @@ export function WorkspacePage({
onUpdateTask: (update: WorkspaceTaskUpdate) => void onUpdateTask: (update: WorkspaceTaskUpdate) => void
}) { }) {
const projects = workspaces.map((workspace) => workspace.project) const projects = workspaces.map((workspace) => workspace.project)
const projectOptions = useMemo(() => workspaces.map((workspace) => workspace.project), [workspaces])
const unfinishedTasks: WorkspaceTask[] = workspaces.flatMap((workspace) => const unfinishedTasks: WorkspaceTask[] = workspaces.flatMap((workspace) =>
workspace.tasks workspace.tasks
.filter((task) => !task.completed) .filter((task) => !task.completed)
@@ -78,7 +79,7 @@ export function WorkspacePage({
const urgentProjects = projects.filter((project) => project.urgent).length const urgentProjects = projects.filter((project) => project.urgent).length
const workspaceMetrics = [ const workspaceMetrics = [
{ title: '项目总数', value: projects.length, delta: `${urgentProjects} 个高优先级`, icon: <IconDashboard />, color: 'arcoblue' }, { title: '项目总数', value: projects.length, delta: `${urgentProjects} 个高优先级`, icon: <IconDashboard />, color: 'arcoblue' },
{ title: '未完成任务', value: unfinishedTasks.length, delta: '跨项目聚合', icon: <IconCheckCircleFill />, color: 'green' }, { title: '未完成计划', value: unfinishedTasks.length, delta: '跨项目聚合', icon: <IconCheckCircleFill />, color: 'green' },
{ title: 'AI 会话', value: aiSessions.length, delta: '项目内上下文', icon: <IconRobot />, color: 'purple' }, { title: 'AI 会话', value: aiSessions.length, delta: '项目内上下文', icon: <IconRobot />, color: 'purple' },
{ title: '知识资料', value: notes.length, delta: '笔记与资料合计', icon: <IconFile />, color: 'cyan' }, { title: '知识资料', value: notes.length, delta: '笔记与资料合计', icon: <IconFile />, color: 'cyan' },
] ]
@@ -111,7 +112,7 @@ export function WorkspacePage({
<Card className="queue-section" bordered> <Card className="queue-section" bordered>
<div className="section-header"> <div className="section-header">
<Title heading={6}></Title> <Title heading={6}></Title>
<Tag color="arcoblue">{unfinishedTasks.length}</Tag> <Tag color="arcoblue">{unfinishedTasks.length}</Tag>
</div> </div>
{unfinishedTasks.length ? ( {unfinishedTasks.length ? (
@@ -129,14 +130,14 @@ export function WorkspacePage({
</div> </div>
<div className="workspace-task-meta"> <div className="workspace-task-meta">
<Tag color={task.project.urgent ? 'red' : 'arcoblue'}>{task.project.name}</Tag> <Tag color={task.project.urgent ? 'red' : 'arcoblue'}>{task.project.name}</Tag>
<Tag color="green">{task.tag || '-'}</Tag>
<span><IconCalendar /> {task.createdAt}</span> <span><IconCalendar /> {task.createdAt}</span>
<Tag color="green">{task.tag}</Tag>
</div> </div>
</button> </button>
))} ))}
</div> </div>
) : ( ) : (
<Empty description="暂无未完成任务" /> <Empty description="暂无未完成计划" />
)} )}
</Card> </Card>
@@ -170,8 +171,13 @@ export function WorkspacePage({
</label> </label>
<label> <label>
<Text></Text> <Text></Text>
<Select value={draft.projectId} onChange={(projectId) => setDraft((value) => ({ ...value, projectId }))}> <Select
{projects.map((project) => ( showSearch
value={draft.projectId}
placeholder="选择项目"
onChange={(projectId) => setDraft((value) => ({ ...value, projectId }))}
>
{projectOptions.map((project) => (
<Select.Option key={project.id} value={project.id}>{project.name}</Select.Option> <Select.Option key={project.id} value={project.id}>{project.name}</Select.Option>
))} ))}
</Select> </Select>

View File

@@ -2,6 +2,7 @@ import { Layout } from '@arco-design/web-react'
import { WorkspacePage, type WorkspaceTaskUpdate } from './workspace-body' import { WorkspacePage, type WorkspaceTaskUpdate } from './workspace-body'
import { WorkspaceExplorePage } from './workspace-explore' import { WorkspaceExplorePage } from './workspace-explore'
import { ProjectChannelPage } from './projects/project-channel-page' import { ProjectChannelPage } from './projects/project-channel-page'
import type { ProjectTaskUpdate } from './projects/project-task-edit-modal'
import { ProjectRail } from './projects/project-rail' import { ProjectRail } from './projects/project-rail'
import { ProjectSidebar, type ProjectSettingsUpdate } from './projects/project-sidebar' import { ProjectSidebar, type ProjectSettingsUpdate } from './projects/project-sidebar'
import { ProjectStatusbar } from './projects/project-statusbar' import { ProjectStatusbar } from './projects/project-statusbar'
@@ -57,6 +58,17 @@ export function ProjectPage({
}) { }) {
const isProject = activeView === 'project' const isProject = activeView === 'project'
const projects = workspaces.map((workspace) => workspace.project) const projects = workspaces.map((workspace) => workspace.project)
const updateActiveProjectTask = (update: ProjectTaskUpdate) => {
onUpdateWorkspaceTask({
originalProjectId: activeWorkspace.project.id,
nextProjectId: activeWorkspace.project.id,
taskId: update.taskId,
title: update.title,
summary: update.summary,
tag: update.tag,
completed: update.completed,
})
}
return ( return (
<Layout className="workbench-shell"> <Layout className="workbench-shell">
@@ -101,6 +113,7 @@ export function ProjectPage({
onUploadSource={onUploadSource} onUploadSource={onUploadSource}
onCreateCronPlan={onCreateCronPlan} onCreateCronPlan={onCreateCronPlan}
onCreateProjectTag={onCreateProjectTag} onCreateProjectTag={onCreateProjectTag}
onUpdateTask={updateActiveProjectTask}
/> />
)} )}
</Content> </Content>