feat: reorganize project task planning UI
This commit is contained in:
@@ -1044,6 +1044,98 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-tag-section .arco-card-body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-plan-card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-plan-card {
|
||||||
|
min-height: 138px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
border: 1px solid var(--senlin-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--senlin-surface);
|
||||||
|
color: var(--senlin-text);
|
||||||
|
padding: 14px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-plan-card:hover {
|
||||||
|
border-color: color-mix(in srgb, var(--senlin-primary) 42%, var(--senlin-border));
|
||||||
|
box-shadow: 0 8px 22px color-mix(in srgb, var(--senlin-primary) 12%, transparent);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-plan-title,
|
||||||
|
.completed-plan-title {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--senlin-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-plan-title svg,
|
||||||
|
.completed-plan-title svg {
|
||||||
|
color: var(--senlin-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-plan-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px 10px;
|
||||||
|
color: var(--senlin-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-plan-row {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(260px, 1fr) 120px 120px 86px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--senlin-border);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--senlin-muted);
|
||||||
|
padding: 10px 0;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-plan-row:first-of-type {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-plan-row:hover {
|
||||||
|
background: color-mix(in srgb, var(--senlin-primary) 5%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-plan-main {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-plan-row > span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-thread {
|
.chat-thread {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -145,6 +145,8 @@ function mapTask(task: BackendTask): TaskItem {
|
|||||||
progress: task.completed ? '100%' : '60%',
|
progress: task.completed ? '100%' : '60%',
|
||||||
due: task.due || '未设置',
|
due: task.due || '未设置',
|
||||||
createdAt: formatCreatedAt(task.createdAt),
|
createdAt: formatCreatedAt(task.createdAt),
|
||||||
|
completedAt: task.completedAt ? formatCreatedAt(task.completedAt) : '',
|
||||||
|
duration: task.duration || '',
|
||||||
tag: task.tag || (task.completed ? '已完成' : '进行中'),
|
tag: task.tag || (task.completed ? '已完成' : '进行中'),
|
||||||
summary: task.summary,
|
summary: task.summary,
|
||||||
completed: task.completed,
|
completed: task.completed,
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export type BackendTask = {
|
|||||||
owner: string
|
owner: string
|
||||||
due: string
|
due: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
completedAt: string
|
||||||
|
duration: string
|
||||||
tag: string
|
tag: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ function App() {
|
|||||||
title: draft.title.trim(),
|
title: draft.title.trim(),
|
||||||
description: draft.description.trim(),
|
description: draft.description.trim(),
|
||||||
status: 'open',
|
status: 'open',
|
||||||
dueAt: normalizeOptionalTime(draft.dueAt),
|
|
||||||
tag: draft.tag.trim(),
|
tag: draft.tag.trim(),
|
||||||
})
|
})
|
||||||
await refreshAfterAction()
|
await refreshAfterAction()
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export type ProjectDraft = {
|
|||||||
export type TaskDraft = {
|
export type TaskDraft = {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
dueAt: string
|
|
||||||
tag: string
|
tag: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,14 +52,14 @@ export function ProjectActionModals({
|
|||||||
tagOptions: string[]
|
tagOptions: string[]
|
||||||
}) {
|
}) {
|
||||||
const [project, setProject] = useState<ProjectDraft>({ name: '', identifier: '', icon: '', background: '', description: '' })
|
const [project, setProject] = useState<ProjectDraft>({ name: '', identifier: '', icon: '', background: '', description: '' })
|
||||||
const [task, setTask] = useState<TaskDraft>({ title: '', description: '', dueAt: '', tag: '' })
|
const [task, setTask] = useState<TaskDraft>({ title: '', description: '', tag: '' })
|
||||||
const [source, setSource] = useState<SourceDraft>({ title: '', file: null })
|
const [source, setSource] = useState<SourceDraft>({ title: '', file: null })
|
||||||
const [cron, setCron] = useState<CronDraft>({ title: '', schedule: '0 9 * * *', enabled: true, nextRunAt: '' })
|
const [cron, setCron] = useState<CronDraft>({ title: '', schedule: '0 9 * * *', enabled: true, nextRunAt: '' })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeModal === null) {
|
if (activeModal === null) {
|
||||||
setProject({ name: '', identifier: '', icon: '', background: '', description: '' })
|
setProject({ name: '', identifier: '', icon: '', background: '', description: '' })
|
||||||
setTask({ title: '', description: '', dueAt: '', tag: '' })
|
setTask({ title: '', description: '', tag: '' })
|
||||||
setSource({ title: '', file: null })
|
setSource({ title: '', file: null })
|
||||||
setCron({ title: '', schedule: '0 9 * * *', enabled: true, nextRunAt: '' })
|
setCron({ title: '', schedule: '0 9 * * *', enabled: true, nextRunAt: '' })
|
||||||
}
|
}
|
||||||
@@ -117,10 +116,6 @@ export function ProjectActionModals({
|
|||||||
<Text>任务说明</Text>
|
<Text>任务说明</Text>
|
||||||
<TextArea rows={4} placeholder="补充背景、验收口径或下一步" value={task.description} onChange={(description) => setTask((draft) => ({ ...draft, description }))} />
|
<TextArea rows={4} placeholder="补充背景、验收口径或下一步" value={task.description} onChange={(description) => setTask((draft) => ({ ...draft, description }))} />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
|
||||||
<Text>截止时间</Text>
|
|
||||||
<Input placeholder="2026-07-21T09:30:00Z,可留空" value={task.dueAt} onChange={(dueAt) => setTask((draft) => ({ ...draft, dueAt }))} />
|
|
||||||
</label>
|
|
||||||
<label>
|
<label>
|
||||||
<Text>所属标签</Text>
|
<Text>所属标签</Text>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function ProjectChannelPage({
|
|||||||
onCreateTask,
|
onCreateTask,
|
||||||
onUploadSource,
|
onUploadSource,
|
||||||
onCreateCronPlan,
|
onCreateCronPlan,
|
||||||
|
onCreateProjectTag,
|
||||||
}: {
|
}: {
|
||||||
activeChannel: ChannelKey
|
activeChannel: ChannelKey
|
||||||
activeWorkspace: ProjectWorkspace
|
activeWorkspace: ProjectWorkspace
|
||||||
@@ -26,10 +27,11 @@ export function ProjectChannelPage({
|
|||||||
onCreateTask: () => void
|
onCreateTask: () => void
|
||||||
onUploadSource: () => void
|
onUploadSource: () => void
|
||||||
onCreateCronPlan: () => void
|
onCreateCronPlan: () => void
|
||||||
|
onCreateProjectTag: (name: string) => void
|
||||||
}) {
|
}) {
|
||||||
switch (activeChannel) {
|
switch (activeChannel) {
|
||||||
case 'tasks':
|
case 'tasks':
|
||||||
return <ProjectTasks activeWorkspace={activeWorkspace} activeTaskID={activeTaskID} onOpenTask={onOpenTask} onCloseTask={onCloseTask} onSelectItem={onSelectItem} onCreateTask={onCreateTask} />
|
return <ProjectTasks activeWorkspace={activeWorkspace} activeTaskID={activeTaskID} onOpenTask={onOpenTask} onCloseTask={onCloseTask} onSelectItem={onSelectItem} onCreateTask={onCreateTask} onCreateProjectTag={onCreateProjectTag} />
|
||||||
case 'ai':
|
case 'ai':
|
||||||
return <ProjectAi activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
|
return <ProjectAi activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
|
||||||
case 'notes':
|
case 'notes':
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import type { ProjectWorkspace } from './project-types'
|
|||||||
|
|
||||||
const { Title, Text } = Typography
|
const { Title, Text } = Typography
|
||||||
|
|
||||||
const folders = ['需求文档', '技术方案', '会议纪要', '竞品资料']
|
|
||||||
|
|
||||||
export function ProjectNotes({ activeWorkspace, onSelectItem, onUploadSource }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void; onUploadSource: () => void }) {
|
export function ProjectNotes({ activeWorkspace, onSelectItem, onUploadSource }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void; onUploadSource: () => void }) {
|
||||||
const { notes, project } = activeWorkspace
|
const { notes, project } = activeWorkspace
|
||||||
|
|
||||||
@@ -23,8 +21,6 @@ export function ProjectNotes({ activeWorkspace, onSelectItem, onUploadSource }:
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProjectFileGrid items={folderItems(folders)} onSelectItem={onSelectItem} />
|
|
||||||
|
|
||||||
<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>
|
||||||
@@ -35,14 +31,3 @@ export function ProjectNotes({ activeWorkspace, onSelectItem, onUploadSource }:
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function folderItems(names: string[]) {
|
|
||||||
return names.map((name) => ({
|
|
||||||
id: `folder-${name}`,
|
|
||||||
title: name,
|
|
||||||
type: 'folder',
|
|
||||||
updated: '文件夹',
|
|
||||||
owner: '',
|
|
||||||
source: 'folder',
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Fragment, useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Badge, Button, Divider, Input, Layout, Modal, Space, Tag, Typography } from '@arco-design/web-react'
|
import { Badge, Button, Divider, Input, Layout, Modal, Space, Typography } from '@arco-design/web-react'
|
||||||
import { IconApps, IconDashboard, IconMore, IconPlus, IconSettings } from '@arco-design/web-react/icon'
|
import { IconApps, IconDashboard, IconMore, IconPlus, IconSettings } from '@arco-design/web-react/icon'
|
||||||
import type { ChannelKey, ProjectChannel, ProjectWorkspace, WorkbenchView } from './project-types'
|
import type { ChannelKey, ProjectChannel, ProjectWorkspace, WorkbenchView } from './project-types'
|
||||||
|
|
||||||
@@ -23,7 +23,6 @@ export function ProjectSidebar({
|
|||||||
onSelectChannel,
|
onSelectChannel,
|
||||||
onSelectItem,
|
onSelectItem,
|
||||||
onUpdateProject,
|
onUpdateProject,
|
||||||
onCreateProjectTag,
|
|
||||||
}: {
|
}: {
|
||||||
activeView: WorkbenchView
|
activeView: WorkbenchView
|
||||||
activeWorkspace: ProjectWorkspace
|
activeWorkspace: ProjectWorkspace
|
||||||
@@ -31,7 +30,6 @@ export function ProjectSidebar({
|
|||||||
onSelectChannel: (key: ChannelKey) => void
|
onSelectChannel: (key: ChannelKey) => void
|
||||||
onSelectItem: (title: string) => void
|
onSelectItem: (title: string) => void
|
||||||
onUpdateProject: (update: ProjectSettingsUpdate) => void
|
onUpdateProject: (update: ProjectSettingsUpdate) => void
|
||||||
onCreateProjectTag: (name: string) => void
|
|
||||||
}) {
|
}) {
|
||||||
const workspaceMode = activeView === 'workspace'
|
const workspaceMode = activeView === 'workspace'
|
||||||
const channels = activeWorkspace.channels
|
const channels = activeWorkspace.channels
|
||||||
@@ -43,8 +41,6 @@ export function ProjectSidebar({
|
|||||||
background: activeWorkspace.project.background,
|
background: activeWorkspace.project.background,
|
||||||
description: activeWorkspace.project.description,
|
description: activeWorkspace.project.description,
|
||||||
})
|
})
|
||||||
const [tagModalOpen, setTagModalOpen] = useState(false)
|
|
||||||
const [tagName, setTagName] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!settingsOpen) return
|
if (!settingsOpen) return
|
||||||
@@ -58,13 +54,6 @@ export function ProjectSidebar({
|
|||||||
}, [activeWorkspace.project, settingsOpen])
|
}, [activeWorkspace.project, settingsOpen])
|
||||||
|
|
||||||
const projectIcon = activeWorkspace.project.short || activeWorkspace.project.identifier || activeWorkspace.project.name.slice(0, 2)
|
const projectIcon = activeWorkspace.project.short || activeWorkspace.project.identifier || activeWorkspace.project.name.slice(0, 2)
|
||||||
const submitTag = () => {
|
|
||||||
if (!tagName.trim()) return
|
|
||||||
onCreateProjectTag(tagName.trim())
|
|
||||||
setTagName('')
|
|
||||||
setTagModalOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sider className="channel-sidebar" width={248}>
|
<Sider className="channel-sidebar" width={248}>
|
||||||
<div className="project-title">
|
<div className="project-title">
|
||||||
@@ -109,29 +98,6 @@ export function ProjectSidebar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<section className="tag-cloud">
|
|
||||||
<Text className="section-title">标签</Text>
|
|
||||||
<Space wrap>
|
|
||||||
{activeWorkspace.tags.map((tag) => (
|
|
||||||
<Fragment key={tag}>
|
|
||||||
<Tag color={isAllTag(tag) ? 'arcoblue' : 'gray'}>{tag}</Tag>
|
|
||||||
{isAllTag(tag) && (
|
|
||||||
<Button
|
|
||||||
className="tag-create-button"
|
|
||||||
size="mini"
|
|
||||||
type="text"
|
|
||||||
icon={<IconPlus />}
|
|
||||||
onClick={() => setTagModalOpen(true)}
|
|
||||||
>
|
|
||||||
新建
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<section className="recent-sessions">
|
<section className="recent-sessions">
|
||||||
<Text className="section-title">最近会话</Text>
|
<Text className="section-title">最近会话</Text>
|
||||||
@@ -183,37 +149,10 @@ export function ProjectSidebar({
|
|||||||
</label>
|
</label>
|
||||||
</Space>
|
</Space>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
|
||||||
className="action-modal"
|
|
||||||
title="新建标签"
|
|
||||||
visible={tagModalOpen}
|
|
||||||
onCancel={() => {
|
|
||||||
setTagModalOpen(false)
|
|
||||||
setTagName('')
|
|
||||||
}}
|
|
||||||
onOk={submitTag}
|
|
||||||
>
|
|
||||||
<Space direction="vertical" size={12} className="action-form">
|
|
||||||
<label>
|
|
||||||
<Text>标签名称</Text>
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
placeholder="例如:设计、客户、重要"
|
|
||||||
value={tagName}
|
|
||||||
onChange={setTagName}
|
|
||||||
onPressEnter={submitTag}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</Space>
|
|
||||||
</Modal>
|
|
||||||
</Sider>
|
</Sider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAllTag(tag: string) {
|
|
||||||
return tag === '全部' || tag === 'all'
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderChannelButton(channel: ProjectChannel, activeChannel: ChannelKey, onSelectChannel: (key: ChannelKey) => void) {
|
function renderChannelButton(channel: ProjectChannel, activeChannel: ChannelKey, onSelectChannel: (key: ChannelKey) => void) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { Button, Card, Descriptions, Progress, Space, Tag, Typography } from '@arco-design/web-react'
|
import { useState } from 'react'
|
||||||
import { IconArrowLeft, IconCalendar, IconCheckCircle, IconPlus, IconUser } from '@arco-design/web-react/icon'
|
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 type { ProjectWorkspace, TaskItem } from './project-types'
|
import type { ProjectWorkspace, TaskItem } from './project-types'
|
||||||
|
|
||||||
const { Title, Text } = Typography
|
const { Title, Text, Paragraph } = Typography
|
||||||
|
|
||||||
export function ProjectTasks({
|
export function ProjectTasks({
|
||||||
activeWorkspace,
|
activeWorkspace,
|
||||||
activeTaskID,
|
activeTaskID,
|
||||||
onOpenTask,
|
onOpenTask,
|
||||||
onCloseTask,
|
onCloseTask,
|
||||||
onSelectItem,
|
|
||||||
onCreateTask,
|
onCreateTask,
|
||||||
|
onCreateProjectTag,
|
||||||
}: {
|
}: {
|
||||||
activeWorkspace: ProjectWorkspace
|
activeWorkspace: ProjectWorkspace
|
||||||
activeTaskID: string | null
|
activeTaskID: string | null
|
||||||
@@ -18,59 +19,107 @@ export function ProjectTasks({
|
|||||||
onCloseTask: () => void
|
onCloseTask: () => void
|
||||||
onSelectItem: (title: string) => void
|
onSelectItem: (title: string) => void
|
||||||
onCreateTask: () => void
|
onCreateTask: () => void
|
||||||
|
onCreateProjectTag: (name: string) => void
|
||||||
}) {
|
}) {
|
||||||
const { tasks, project } = activeWorkspace
|
const { tasks, project, tags } = activeWorkspace
|
||||||
const activeTask = tasks.find((task) => task.id === activeTaskID)
|
const activeTask = tasks.find((task) => task.id === activeTaskID)
|
||||||
const lanes = makeLanes(tasks)
|
const pendingTasks = tasks.filter((task) => !task.completed)
|
||||||
|
const completedTasks = tasks.filter((task) => task.completed)
|
||||||
|
const projectTags = tags.filter((tag) => tag !== 'all' && tag !== '全部')
|
||||||
|
const [tagModalOpen, setTagModalOpen] = useState(false)
|
||||||
|
const [tagName, setTagName] = useState('')
|
||||||
|
|
||||||
if (activeTask) {
|
if (activeTask) {
|
||||||
return <TaskDetail activeWorkspace={activeWorkspace} task={activeTask} onBack={onCloseTask} />
|
return <TaskDetail activeWorkspace={activeWorkspace} task={activeTask} onBack={onCloseTask} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function submitTag() {
|
||||||
|
const nextTag = tagName.trim()
|
||||||
|
if (!nextTag) return
|
||||||
|
onCreateProjectTag(nextTag)
|
||||||
|
setTagName('')
|
||||||
|
setTagModalOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="project-channel-page project-tasks-page overview-page">
|
<div className="project-channel-page project-tasks-page overview-page">
|
||||||
<div className="overview-head">
|
<div className="overview-head">
|
||||||
<div>
|
<div>
|
||||||
<Title heading={4}>工作计划</Title>
|
<Title heading={4}>工作计划</Title>
|
||||||
<Text type="secondary">{project.name} 的 TODO 计划、负责人和创建时间。</Text>
|
<Text type="secondary">{project.name} 的标签、未完成计划和完成记录。</Text>
|
||||||
</div>
|
</div>
|
||||||
<Button type="primary" icon={<IconPlus />} onClick={onCreateTask}>新建任务</Button>
|
<Button type="primary" icon={<IconPlus />} onClick={onCreateTask}>新建任务</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="queue-section" bordered>
|
<Card className="queue-section task-tag-section" bordered>
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<Title heading={6}>任务清单</Title>
|
<Title heading={6}>标签</Title>
|
||||||
<Tag color="arcoblue">{tasks.length} 个任务</Tag>
|
<Button className="tag-create-button" size="mini" type="text" icon={<IconPlus />} onClick={() => setTagModalOpen(true)}>新建</Button>
|
||||||
</div>
|
</div>
|
||||||
{tasks.map((task) => (
|
<Space wrap>
|
||||||
<button className="data-row task-row" key={task.id} onClick={() => onOpenTask(task.id)}>
|
<Tag color="arcoblue">全部</Tag>
|
||||||
<span className="row-title"><IconCheckCircle /> {task.title}</span>
|
{projectTags.map((tag) => (
|
||||||
<Tag color="arcoblue">{task.tag}</Tag>
|
<Tag key={tag} color="gray">{tag}</Tag>
|
||||||
<span><IconUser /> {task.owner}</span>
|
))}
|
||||||
<span><IconCalendar /> {task.createdAt}</span>
|
</Space>
|
||||||
<Progress percent={Number.parseInt(task.progress, 10)} size="small" />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="task-board">
|
<Card className="queue-section" bordered>
|
||||||
{lanes.map((lane) => (
|
<div className="section-header">
|
||||||
<Card className="task-lane" bordered key={lane.title}>
|
<Title heading={6}>未完成的计划</Title>
|
||||||
<div className="section-header">
|
<Tag color="arcoblue">{pendingTasks.length}</Tag>
|
||||||
<Title heading={6}>{lane.title}</Title>
|
</div>
|
||||||
<Tag color={lane.color}>{lane.items.length}</Tag>
|
<div className="task-plan-card-grid">
|
||||||
</div>
|
{pendingTasks.map((task) => (
|
||||||
<Space direction="vertical" size={8}>
|
<button className="task-plan-card" key={task.id} onClick={() => onOpenTask(task.id)}>
|
||||||
{lane.items.map((title) => (
|
<span className="task-plan-title"><IconCheckCircleFill />{task.title}</span>
|
||||||
<button className="task-card-button" key={title} onClick={() => openTaskByTitle(tasks, title, onOpenTask, onSelectItem)}>
|
<Paragraph ellipsis={{ rows: 2 }} type="secondary">{task.summary || '暂无正文'}</Paragraph>
|
||||||
<Text>{title}</Text>
|
<div className="task-plan-meta">
|
||||||
<Text type="secondary">优先级 · 本周</Text>
|
<Tag color="arcoblue">{task.tag || '未设置标签'}</Tag>
|
||||||
</button>
|
<span><IconCalendar /> 创建 {task.createdAt}</span>
|
||||||
))}
|
</div>
|
||||||
</Space>
|
</button>
|
||||||
</Card>
|
))}
|
||||||
|
{pendingTasks.length === 0 && <Text type="secondary">当前没有未完成计划</Text>}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="queue-section" bordered>
|
||||||
|
<div className="section-header">
|
||||||
|
<Title heading={6}>完成的计划</Title>
|
||||||
|
<Tag color="green">{completedTasks.length}</Tag>
|
||||||
|
</div>
|
||||||
|
{completedTasks.map((task) => (
|
||||||
|
<button className="completed-plan-row" key={task.id} onClick={() => onOpenTask(task.id)}>
|
||||||
|
<span className="completed-plan-main">
|
||||||
|
<Text className="completed-plan-title"><IconCheckCircle />{task.title}</Text>
|
||||||
|
<Text type="secondary" ellipsis={{ showTooltip: true }}>{task.summary || '暂无正文'}</Text>
|
||||||
|
</span>
|
||||||
|
<span>{task.createdAt}</span>
|
||||||
|
<span>{task.completedAt || '未知'}</span>
|
||||||
|
<span><IconClockCircle /> {task.duration || '未知'}</span>
|
||||||
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
{completedTasks.length === 0 && <Text type="secondary">当前没有完成计划</Text>}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
className="action-modal"
|
||||||
|
title="新建标签"
|
||||||
|
visible={tagModalOpen}
|
||||||
|
onCancel={() => {
|
||||||
|
setTagModalOpen(false)
|
||||||
|
setTagName('')
|
||||||
|
}}
|
||||||
|
onOk={submitTag}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size={12} className="action-form">
|
||||||
|
<label>
|
||||||
|
<Text>标签名称</Text>
|
||||||
|
<Input autoFocus placeholder="例如:设计、客户、重要" value={tagName} onChange={setTagName} onPressEnter={submitTag} />
|
||||||
|
</label>
|
||||||
|
</Space>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -86,7 +135,7 @@ function TaskDetail({ activeWorkspace, task, onBack }: { activeWorkspace: Projec
|
|||||||
<Title heading={4}>{task.title}</Title>
|
<Title heading={4}>{task.title}</Title>
|
||||||
<Text type="secondary">{activeWorkspace.project.name} 的任务详情</Text>
|
<Text type="secondary">{activeWorkspace.project.name} 的任务详情</Text>
|
||||||
</div>
|
</div>
|
||||||
<Tag color={task.completed ? 'green' : 'arcoblue'}>{task.tag}</Tag>
|
<Tag color={task.completed ? 'green' : 'arcoblue'}>{task.tag || '未设置标签'}</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="queue-section task-detail-card" bordered>
|
<Card className="queue-section task-detail-card" bordered>
|
||||||
@@ -98,6 +147,8 @@ function TaskDetail({ activeWorkspace, task, onBack }: { activeWorkspace: Projec
|
|||||||
{ label: '负责人', value: task.owner },
|
{ label: '负责人', value: task.owner },
|
||||||
{ label: '创建时间', value: task.createdAt },
|
{ label: '创建时间', value: task.createdAt },
|
||||||
{ label: '完成状态', value: task.completed ? '已完成' : '未完成' },
|
{ label: '完成状态', value: task.completed ? '已完成' : '未完成' },
|
||||||
|
{ label: '完成时间', value: task.completedAt || '未完成' },
|
||||||
|
{ label: '耗时', value: task.duration || '未完成' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div className="task-detail-progress">
|
<div className="task-detail-progress">
|
||||||
@@ -112,22 +163,3 @@ function TaskDetail({ activeWorkspace, task, onBack }: { activeWorkspace: Projec
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTaskByTitle(tasks: TaskItem[], title: string, onOpenTask: (taskID: string) => void, onSelectItem: (title: string) => void) {
|
|
||||||
const task = tasks.find((item) => item.title === title)
|
|
||||||
if (task) {
|
|
||||||
onOpenTask(task.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
onSelectItem(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeLanes(tasks: TaskItem[]) {
|
|
||||||
const done = tasks.filter((task) => task.completed)
|
|
||||||
const active = tasks.filter((task) => !task.completed)
|
|
||||||
return [
|
|
||||||
{ title: '待开始', color: 'gray', items: active.slice(0, 1).map((task) => task.title) },
|
|
||||||
{ title: '进行中', color: 'arcoblue', items: active.map((task) => task.title) },
|
|
||||||
{ title: '已完成', color: 'green', items: done.length ? done.map((task) => task.title) : ['暂无已完成任务'] },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export type TaskItem = {
|
|||||||
progress: string
|
progress: string
|
||||||
due: string
|
due: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
completedAt: string
|
||||||
|
duration: string
|
||||||
tag: string
|
tag: string
|
||||||
summary: string
|
summary: string
|
||||||
completed: boolean
|
completed: boolean
|
||||||
|
|||||||
@@ -55,14 +55,9 @@ export function WorkspacePage({
|
|||||||
const [editingTask, setEditingTask] = useState<WorkspaceTask | null>(null)
|
const [editingTask, setEditingTask] = useState<WorkspaceTask | null>(null)
|
||||||
const [draft, setDraft] = useState<TaskDraft>({ title: '', summary: '', projectId: '', tag: '', completed: false })
|
const [draft, setDraft] = useState<TaskDraft>({ title: '', summary: '', projectId: '', tag: '', completed: false })
|
||||||
const tagOptions = useMemo(() => {
|
const tagOptions = useMemo(() => {
|
||||||
const tags = new Set<string>()
|
|
||||||
const selectedWorkspace = workspaces.find((workspace) => workspace.project.id === draft.projectId)
|
const selectedWorkspace = workspaces.find((workspace) => workspace.project.id === draft.projectId)
|
||||||
selectedWorkspace?.tags
|
return selectedWorkspace?.tags.filter((tag) => tag !== 'all' && tag !== '全部') ?? []
|
||||||
.filter((tag) => tag !== 'all' && tag !== '全部')
|
}, [draft.projectId, workspaces])
|
||||||
.forEach((tag) => tags.add(tag))
|
|
||||||
if (draft.tag) tags.add(draft.tag)
|
|
||||||
return Array.from(tags)
|
|
||||||
}, [draft.projectId, draft.tag, workspaces])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editingTask) return
|
if (!editingTask) return
|
||||||
@@ -74,6 +69,10 @@ export function WorkspacePage({
|
|||||||
completed: editingTask.completed,
|
completed: editingTask.completed,
|
||||||
})
|
})
|
||||||
}, [editingTask])
|
}, [editingTask])
|
||||||
|
useEffect(() => {
|
||||||
|
if (!draft.tag || tagOptions.includes(draft.tag)) return
|
||||||
|
setDraft((value) => ({ ...value, tag: '' }))
|
||||||
|
}, [draft.tag, tagOptions])
|
||||||
const aiSessions = workspaces.flatMap((workspace) => workspace.aiSessions)
|
const aiSessions = workspaces.flatMap((workspace) => workspace.aiSessions)
|
||||||
const notes = workspaces.flatMap((workspace) => workspace.notes)
|
const notes = workspaces.flatMap((workspace) => workspace.notes)
|
||||||
const urgentProjects = projects.filter((project) => project.urgent).length
|
const urgentProjects = projects.filter((project) => project.urgent).length
|
||||||
@@ -180,10 +179,10 @@ export function WorkspacePage({
|
|||||||
<label>
|
<label>
|
||||||
<Text>所属标签</Text>
|
<Text>所属标签</Text>
|
||||||
<Select
|
<Select
|
||||||
allowCreate
|
allowClear
|
||||||
value={draft.tag}
|
value={draft.tag || undefined}
|
||||||
placeholder="选择或输入标签"
|
placeholder="不设置标签"
|
||||||
onChange={(tag) => setDraft((value) => ({ ...value, tag }))}
|
onChange={(tag) => setDraft((value) => ({ ...value, tag: tag ?? '' }))}
|
||||||
>
|
>
|
||||||
{tagOptions.map((tag) => (
|
{tagOptions.map((tag) => (
|
||||||
<Select.Option key={tag} value={tag}>{tag}</Select.Option>
|
<Select.Option key={tag} value={tag}>{tag}</Select.Option>
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ export function ProjectPage({
|
|||||||
onSelectChannel={onSelectChannel}
|
onSelectChannel={onSelectChannel}
|
||||||
onSelectItem={onSelectItem}
|
onSelectItem={onSelectItem}
|
||||||
onUpdateProject={onUpdateProject}
|
onUpdateProject={onUpdateProject}
|
||||||
onCreateProjectTag={onCreateProjectTag}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -101,6 +100,7 @@ export function ProjectPage({
|
|||||||
onCreateTask={onCreateTask}
|
onCreateTask={onCreateTask}
|
||||||
onUploadSource={onUploadSource}
|
onUploadSource={onUploadSource}
|
||||||
onCreateCronPlan={onCreateCronPlan}
|
onCreateCronPlan={onCreateCronPlan}
|
||||||
|
onCreateProjectTag={onCreateProjectTag}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Content>
|
</Content>
|
||||||
|
|||||||
@@ -54,14 +54,16 @@ type InboxMessage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WorkTask struct {
|
type WorkTask struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
Completed bool `json:"completed"`
|
Completed bool `json:"completed"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
Due string `json:"due"`
|
Due string `json:"due"`
|
||||||
CreatedAt string `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
Tag string `json:"tag"`
|
CompletedAt string `json:"completedAt"`
|
||||||
|
Duration string `json:"duration"`
|
||||||
|
Tag string `json:"tag"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AISessionItem struct {
|
type AISessionItem struct {
|
||||||
@@ -550,15 +552,24 @@ func (s *Service) workspaceTasks(projectID uint) ([]WorkTask, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
completed := task.Status == "done"
|
||||||
|
completedAt := ""
|
||||||
|
duration := ""
|
||||||
|
if completed {
|
||||||
|
completedAt = displayTime(task.UpdatedAt)
|
||||||
|
duration = displayDuration(task.CreatedAt, task.UpdatedAt)
|
||||||
|
}
|
||||||
items = append(items, WorkTask{
|
items = append(items, WorkTask{
|
||||||
ID: fmt.Sprint(task.ID),
|
ID: fmt.Sprint(task.ID),
|
||||||
Title: task.Title,
|
Title: task.Title,
|
||||||
Summary: task.Description,
|
Summary: task.Description,
|
||||||
Completed: task.Status == "done",
|
Completed: completed,
|
||||||
Owner: owner,
|
Owner: owner,
|
||||||
Due: displayOptionalTime(task.DueAt),
|
Due: displayOptionalTime(task.DueAt),
|
||||||
CreatedAt: displayTime(task.CreatedAt),
|
CreatedAt: displayTime(task.CreatedAt),
|
||||||
Tag: tagNames[task.ID],
|
CompletedAt: completedAt,
|
||||||
|
Duration: duration,
|
||||||
|
Tag: tagNames[task.ID],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
@@ -740,6 +751,26 @@ func displayOptionalTime(value *time.Time) string {
|
|||||||
return displayTime(*value)
|
return displayTime(*value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func displayDuration(start time.Time, end time.Time) string {
|
||||||
|
if start.IsZero() || end.IsZero() || end.Before(start) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
duration := end.Sub(start)
|
||||||
|
days := int(duration.Hours()) / 24
|
||||||
|
if days > 0 {
|
||||||
|
return fmt.Sprintf("%d 天", days)
|
||||||
|
}
|
||||||
|
hours := int(duration.Hours())
|
||||||
|
if hours > 0 {
|
||||||
|
return fmt.Sprintf("%d 小时", hours)
|
||||||
|
}
|
||||||
|
minutes := int(duration.Minutes())
|
||||||
|
if minutes > 0 {
|
||||||
|
return fmt.Sprintf("%d 分钟", minutes)
|
||||||
|
}
|
||||||
|
return "1 分钟内"
|
||||||
|
}
|
||||||
|
|
||||||
func sourceDisplayName(source models.SenlinAgentSource) string {
|
func sourceDisplayName(source models.SenlinAgentSource) string {
|
||||||
switch source.Kind {
|
switch source.Kind {
|
||||||
case "file":
|
case "file":
|
||||||
|
|||||||
Reference in New Issue
Block a user