feat: add project action modals and APIs

This commit is contained in:
2026-07-20 21:45:36 +08:00
parent df12201caa
commit c1a144cb44
16 changed files with 877 additions and 23 deletions

View File

@@ -0,0 +1,162 @@
import { useEffect, useState } from 'react'
import { Input, Modal, Space, Switch, Typography } from '@arco-design/web-react'
const { Text } = Typography
const { TextArea } = Input
export type ProjectActionModal = 'project' | 'task' | 'source' | 'cron' | null
export type ProjectDraft = {
name: string
description: string
}
export type TaskDraft = {
title: string
description: string
dueAt: string
}
export type SourceDraft = {
title: string
file: File | null
}
export type CronDraft = {
title: string
schedule: string
enabled: boolean
nextRunAt: string
}
export function ProjectActionModals({
activeModal,
loading,
onClose,
onCreateProject,
onCreateTask,
onUploadSource,
onCreateCronPlan,
}: {
activeModal: ProjectActionModal
loading: boolean
onClose: () => void
onCreateProject: (draft: ProjectDraft) => void
onCreateTask: (draft: TaskDraft) => void
onUploadSource: (draft: SourceDraft) => void
onCreateCronPlan: (draft: CronDraft) => void
}) {
const [project, setProject] = useState<ProjectDraft>({ name: '', description: '' })
const [task, setTask] = useState<TaskDraft>({ title: '', description: '', dueAt: '' })
const [source, setSource] = useState<SourceDraft>({ title: '', file: null })
const [cron, setCron] = useState<CronDraft>({ title: '', schedule: '0 9 * * *', enabled: true, nextRunAt: '' })
useEffect(() => {
if (activeModal === null) {
setProject({ name: '', description: '' })
setTask({ title: '', description: '', dueAt: '' })
setSource({ title: '', file: null })
setCron({ title: '', schedule: '0 9 * * *', enabled: true, nextRunAt: '' })
}
}, [activeModal])
return (
<>
<Modal
className="action-modal"
title="新建项目"
visible={activeModal === 'project'}
confirmLoading={loading}
onCancel={onClose}
onOk={() => onCreateProject(project)}
>
<Space direction="vertical" size={12} className="action-form">
<label>
<Text></Text>
<Input placeholder="例如:项目 A3" value={project.name} onChange={(name) => setProject((draft) => ({ ...draft, name }))} />
</label>
<label>
<Text></Text>
<TextArea rows={4} placeholder="项目目标、背景或协作说明" value={project.description} onChange={(description) => setProject((draft) => ({ ...draft, description }))} />
</label>
</Space>
</Modal>
<Modal
className="action-modal"
title="新建任务"
visible={activeModal === 'task'}
confirmLoading={loading}
onCancel={onClose}
onOk={() => onCreateTask(task)}
>
<Space direction="vertical" size={12} className="action-form">
<label>
<Text></Text>
<Input placeholder="要完成什么?" value={task.title} onChange={(title) => setTask((draft) => ({ ...draft, title }))} />
</label>
<label>
<Text></Text>
<TextArea rows={4} placeholder="补充背景、验收口径或下一步" value={task.description} onChange={(description) => setTask((draft) => ({ ...draft, description }))} />
</label>
<label>
<Text></Text>
<Input placeholder="2026-07-21T09:30:00Z可留空" value={task.dueAt} onChange={(dueAt) => setTask((draft) => ({ ...draft, dueAt }))} />
</label>
</Space>
</Modal>
<Modal
className="action-modal"
title="上传文件"
visible={activeModal === 'source'}
confirmLoading={loading}
onCancel={onClose}
onOk={() => onUploadSource(source)}
>
<Space direction="vertical" size={12} className="action-form">
<label>
<Text></Text>
<Input placeholder="默认使用文件名" value={source.title} onChange={(title) => setSource((draft) => ({ ...draft, title }))} />
</label>
<label>
<Text></Text>
<input
className="native-file-input"
type="file"
onChange={(event) => setSource((draft) => ({ ...draft, file: event.target.files?.[0] ?? null }))}
/>
</label>
</Space>
</Modal>
<Modal
className="action-modal"
title="新建计划任务"
visible={activeModal === 'cron'}
confirmLoading={loading}
onCancel={onClose}
onOk={() => onCreateCronPlan(cron)}
>
<Space direction="vertical" size={12} className="action-form">
<label>
<Text></Text>
<Input placeholder="例如:每日 Inbox 整理" value={cron.title} onChange={(title) => setCron((draft) => ({ ...draft, title }))} />
</label>
<label>
<Text>Cron </Text>
<Input value={cron.schedule} onChange={(schedule) => setCron((draft) => ({ ...draft, schedule }))} />
</label>
<label>
<Text></Text>
<Input placeholder="2026-07-22T08:00:00Z可留空" value={cron.nextRunAt} onChange={(nextRunAt) => setCron((draft) => ({ ...draft, nextRunAt }))} />
</label>
<label className="action-switch">
<Text></Text>
<Switch checked={cron.enabled} onChange={(enabled) => setCron((draft) => ({ ...draft, enabled }))} />
</label>
</Space>
</Modal>
</>
)
}

View File

@@ -12,6 +12,9 @@ export function ProjectChannelPage({
onSelectItem,
onOpenTask,
onCloseTask,
onCreateTask,
onUploadSource,
onCreateCronPlan,
}: {
activeChannel: ChannelKey
activeWorkspace: ProjectWorkspace
@@ -19,16 +22,19 @@ export function ProjectChannelPage({
onSelectItem: (title: string) => void
onOpenTask: (taskID: string) => void
onCloseTask: () => void
onCreateTask: () => void
onUploadSource: () => void
onCreateCronPlan: () => void
}) {
switch (activeChannel) {
case 'tasks':
return <ProjectTasks activeWorkspace={activeWorkspace} activeTaskID={activeTaskID} onOpenTask={onOpenTask} onCloseTask={onCloseTask} onSelectItem={onSelectItem} />
return <ProjectTasks activeWorkspace={activeWorkspace} activeTaskID={activeTaskID} onOpenTask={onOpenTask} onCloseTask={onCloseTask} onSelectItem={onSelectItem} onCreateTask={onCreateTask} />
case 'ai':
return <ProjectAi activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
case 'notes':
return <ProjectNotes activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
return <ProjectNotes activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} onUploadSource={onUploadSource} />
case 'cron':
return <ProjectCron activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
return <ProjectCron activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} onCreateCronPlan={onCreateCronPlan} />
case 'overview':
default:
return <ProjectOverview activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />

View File

@@ -4,7 +4,7 @@ import type { ProjectWorkspace } from './project-types'
const { Title, Text } = Typography
export function ProjectCron({ activeWorkspace, onSelectItem }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void }) {
export function ProjectCron({ activeWorkspace, onSelectItem, onCreateCronPlan }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void; onCreateCronPlan: () => void }) {
const { cronJobs, project } = activeWorkspace
const enabledCount = cronJobs.filter((job) => job.enabled).length
const pausedCount = cronJobs.length - enabledCount
@@ -16,7 +16,7 @@ export function ProjectCron({ activeWorkspace, onSelectItem }: { activeWorkspace
<Title heading={4}>Cron </Title>
<Text type="secondary">{project.name} AI </Text>
</div>
<Button type="primary" icon={<IconPlus />}></Button>
<Button type="primary" icon={<IconPlus />} onClick={onCreateCronPlan}></Button>
</div>
<Card className="queue-section" bordered>

View File

@@ -6,7 +6,7 @@ const { Title, Text } = Typography
const folders = ['需求文档', '技术方案', '会议纪要', '竞品资料']
export function ProjectNotes({ activeWorkspace, onSelectItem }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void }) {
export function ProjectNotes({ activeWorkspace, onSelectItem, onUploadSource }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void; onUploadSource: () => void }) {
const { notes, project } = activeWorkspace
return (
@@ -18,7 +18,7 @@ export function ProjectNotes({ activeWorkspace, onSelectItem }: { activeWorkspac
</div>
<Space>
<Button icon={<IconSearch />}></Button>
<Button type="primary" icon={<IconPlus />}>/</Button>
<Button type="primary" icon={<IconPlus />} onClick={onUploadSource}>/</Button>
</Space>
</div>

View File

@@ -12,6 +12,7 @@ export function ProjectRail({
onSelectWorkspace,
onSelectWorkspaceInbox,
onSelectProject,
onCreateProject,
}: {
activeProject: Project
projects: Project[]
@@ -19,6 +20,7 @@ export function ProjectRail({
onSelectWorkspace: () => void
onSelectWorkspaceInbox: () => void
onSelectProject: (project: Project) => void
onCreateProject: () => void
}) {
return (
<Sider className="project-rail" width={96}>
@@ -50,7 +52,7 @@ export function ProjectRail({
</Badge>
))}
</Space>
<Button className="create-project" aria-label="新建项目" icon={<IconPlus />} title="新建项目" />
<Button className="create-project" aria-label="新建项目" icon={<IconPlus />} title="新建项目" onClick={onCreateProject} />
</Sider>
)
}

View File

@@ -10,12 +10,14 @@ export function ProjectTasks({
onOpenTask,
onCloseTask,
onSelectItem,
onCreateTask,
}: {
activeWorkspace: ProjectWorkspace
activeTaskID: string | null
onOpenTask: (taskID: string) => void
onCloseTask: () => void
onSelectItem: (title: string) => void
onCreateTask: () => void
}) {
const { tasks, project } = activeWorkspace
const activeTask = tasks.find((task) => task.id === activeTaskID)
@@ -32,7 +34,7 @@ export function ProjectTasks({
<Title heading={4}></Title>
<Text type="secondary">{project.name} TODO </Text>
</div>
<Button type="primary" icon={<IconPlus />}></Button>
<Button type="primary" icon={<IconPlus />} onClick={onCreateTask}></Button>
</div>
<Card className="queue-section" bordered>

View File

@@ -25,6 +25,10 @@ export function ProjectPage({
onOpenTask,
onCloseTask,
onToggleTheme,
onCreateProject,
onCreateTask,
onUploadSource,
onCreateCronPlan,
}: {
activeView: WorkbenchView
activeWorkspace: ProjectWorkspace
@@ -40,6 +44,10 @@ export function ProjectPage({
onOpenTask: (project: Project, taskID: string) => void
onCloseTask: () => void
onToggleTheme: () => void
onCreateProject: () => void
onCreateTask: () => void
onUploadSource: () => void
onCreateCronPlan: () => void
}) {
const isProject = activeView === 'project'
const projects = workspaces.map((workspace) => workspace.project)
@@ -56,6 +64,7 @@ export function ProjectPage({
onSelectWorkspace={onSelectWorkspace}
onSelectWorkspaceInbox={onSelectWorkspaceInbox}
onSelectProject={onSelectProject}
onCreateProject={onCreateProject}
/>
{isProject && (
@@ -81,6 +90,9 @@ export function ProjectPage({
onSelectItem={onSelectItem}
onOpenTask={(taskID) => onOpenTask(activeWorkspace.project, taskID)}
onCloseTask={onCloseTask}
onCreateTask={onCreateTask}
onUploadSource={onUploadSource}
onCreateCronPlan={onCreateCronPlan}
/>
)}
</Content>