feat: update workspace pages and global inbox
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { ProjectAi } from './project-ai'
|
||||
import { ProjectCron } from './project-cron'
|
||||
import { ProjectInbox } from './project-inbox'
|
||||
import { ProjectNotes } from './project-notes'
|
||||
import { ProjectOverview } from './project-overview'
|
||||
import { ProjectTasks } from './project-tasks'
|
||||
@@ -9,17 +8,21 @@ import type { ChannelKey, ProjectWorkspace } from './project-types'
|
||||
export function ProjectChannelPage({
|
||||
activeChannel,
|
||||
activeWorkspace,
|
||||
activeTaskID,
|
||||
onSelectItem,
|
||||
onOpenTask,
|
||||
onCloseTask,
|
||||
}: {
|
||||
activeChannel: ChannelKey
|
||||
activeWorkspace: ProjectWorkspace
|
||||
activeTaskID: string | null
|
||||
onSelectItem: (title: string) => void
|
||||
onOpenTask: (taskID: string) => void
|
||||
onCloseTask: () => void
|
||||
}) {
|
||||
switch (activeChannel) {
|
||||
case 'inbox':
|
||||
return <ProjectInbox activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
|
||||
case 'tasks':
|
||||
return <ProjectTasks activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
|
||||
return <ProjectTasks activeWorkspace={activeWorkspace} activeTaskID={activeTaskID} onOpenTask={onOpenTask} onCloseTask={onCloseTask} onSelectItem={onSelectItem} />
|
||||
case 'ai':
|
||||
return <ProjectAi activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
|
||||
case 'notes':
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Button, Card, Grid, Space, Tag, Typography } from '@arco-design/web-react'
|
||||
import { IconArchive, IconEmail, IconRefresh, IconSend, IconStar } from '@arco-design/web-react/icon'
|
||||
import type { ProjectWorkspace } from './project-types'
|
||||
|
||||
const { Row, Col } = Grid
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
|
||||
const folders = [
|
||||
{ label: '未处理', count: 18, color: 'red' },
|
||||
{ label: '待回复', count: 9, color: 'orange' },
|
||||
{ label: '已归档', count: 42, color: 'gray' },
|
||||
]
|
||||
|
||||
export function ProjectInbox({ activeWorkspace, onSelectItem }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void }) {
|
||||
const { inbox, project } = activeWorkspace
|
||||
const selected = inbox[0]
|
||||
|
||||
return (
|
||||
<div className="project-channel-page project-inbox-page overview-page">
|
||||
<div className="overview-head">
|
||||
<div>
|
||||
<Title heading={4}>Inbox 消息流</Title>
|
||||
<Text type="secondary">{project.name} 的邮件式收件箱,聚合外部反馈、系统通知和待处理线索。</Text>
|
||||
</div>
|
||||
<Space>
|
||||
<Button icon={<IconRefresh />}>刷新</Button>
|
||||
<Button type="primary" icon={<IconArchive />}>批量归档</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={12}>
|
||||
{folders.map((folder) => (
|
||||
<Col span={8} key={folder.label}>
|
||||
<Card className="compact-card" bordered>
|
||||
<Space>
|
||||
<Tag color={folder.color}>{folder.count}</Tag>
|
||||
<Text>{folder.label}</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
<div className="mail-layout">
|
||||
<Card className="mail-list queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>今日消息</Title>
|
||||
<Button type="text" size="mini">全部标记已读</Button>
|
||||
</div>
|
||||
{inbox.map((item, index) => (
|
||||
<button
|
||||
className={index === 0 ? 'mail-item active' : 'mail-item'}
|
||||
key={item.title}
|
||||
onClick={() => onSelectItem(item.title)}
|
||||
>
|
||||
<span><IconEmail /> {item.title}</span>
|
||||
<Text type="secondary">{item.meta}</Text>
|
||||
<div>
|
||||
<Tag>{item.tag}</Tag>
|
||||
<time>{item.time}</time>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
<Card className="mail-detail queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>{selected?.title ?? '暂无消息'}</Title>
|
||||
<Space>
|
||||
<Button type="text" icon={<IconStar />} />
|
||||
<Button type="text" icon={<IconSend />} />
|
||||
</Space>
|
||||
</div>
|
||||
<Text type="secondary">{selected ? `${selected.meta} · ${selected.owner}` : '真实接口暂无消息'}</Text>
|
||||
<Paragraph>
|
||||
已收到新的项目消息,需要判断是否转为任务、笔记或资料。当前建议先补充影响范围,再同步给相关负责人。
|
||||
</Paragraph>
|
||||
<div className="reply-box">
|
||||
<Text type="secondary">快速处理</Text>
|
||||
<Space>
|
||||
<Button>生成任务</Button>
|
||||
<Button>转为笔记</Button>
|
||||
<Button type="primary">回复</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -86,7 +86,7 @@ function TaskSection({ tasks, onSelectItem }: { tasks: ProjectWorkspace['tasks']
|
||||
<Tag color="arcoblue">{task.tag}</Tag>
|
||||
<span>进度 {task.progress}</span>
|
||||
<span>负责人 {task.owner}</span>
|
||||
<time>截止 {task.due}</time>
|
||||
<time>创建 {task.createdAt}</time>
|
||||
</button>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
import { Badge, Button, Layout, Space } from '@arco-design/web-react'
|
||||
import { IconDashboard, IconPlus } from '@arco-design/web-react/icon'
|
||||
import { IconDashboard, IconEmail, IconPlus } from '@arco-design/web-react/icon'
|
||||
import type { Project, WorkbenchView } from './project-types'
|
||||
|
||||
const { Sider } = Layout
|
||||
@@ -10,12 +10,14 @@ export function ProjectRail({
|
||||
projects,
|
||||
activeView,
|
||||
onSelectWorkspace,
|
||||
onSelectWorkspaceInbox,
|
||||
onSelectProject,
|
||||
}: {
|
||||
activeProject: Project
|
||||
projects: Project[]
|
||||
activeView: WorkbenchView
|
||||
onSelectWorkspace: () => void
|
||||
onSelectWorkspaceInbox: () => void
|
||||
onSelectProject: (project: Project) => void
|
||||
}) {
|
||||
return (
|
||||
@@ -27,10 +29,22 @@ export function ProjectRail({
|
||||
aria-label="工作台"
|
||||
title="工作台"
|
||||
/>
|
||||
<Button
|
||||
className={activeView === 'workspace-inbox' ? 'dashboard-button active' : 'dashboard-button'}
|
||||
icon={<IconEmail />}
|
||||
onClick={onSelectWorkspaceInbox}
|
||||
aria-label="Inbox"
|
||||
title="Inbox"
|
||||
/>
|
||||
<Space className="project-stack" direction="vertical" size={12}>
|
||||
{projects.map((project) => (
|
||||
<Badge key={project.id} count={project.badge} dot={false} className={project.urgent ? 'project-badge urgent' : 'project-badge'}>
|
||||
<button className={activeView === 'project' && activeProject.id === project.id ? 'project-button active' : 'project-button'} onClick={() => onSelectProject(project)} style={{ '--project-color': project.color } as CSSProperties} title={project.name}>
|
||||
<button
|
||||
className={activeView === 'project' && activeProject.id === project.id ? 'project-button active' : 'project-button'}
|
||||
onClick={() => onSelectProject(project)}
|
||||
style={{ '--project-color': project.color } as CSSProperties}
|
||||
title={project.name}
|
||||
>
|
||||
{project.short}
|
||||
</button>
|
||||
</Badge>
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
import { Button, Card, Progress, Space, Tag, Typography } from '@arco-design/web-react'
|
||||
import { IconCalendar, IconCheckCircle, IconPlus, IconUser } from '@arco-design/web-react/icon'
|
||||
import { Button, Card, Descriptions, Progress, Space, Tag, Typography } from '@arco-design/web-react'
|
||||
import { IconArrowLeft, IconCalendar, IconCheckCircle, IconPlus, IconUser } from '@arco-design/web-react/icon'
|
||||
import type { ProjectWorkspace, TaskItem } from './project-types'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
export function ProjectTasks({ activeWorkspace, onSelectItem }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void }) {
|
||||
export function ProjectTasks({
|
||||
activeWorkspace,
|
||||
activeTaskID,
|
||||
onOpenTask,
|
||||
onCloseTask,
|
||||
onSelectItem,
|
||||
}: {
|
||||
activeWorkspace: ProjectWorkspace
|
||||
activeTaskID: string | null
|
||||
onOpenTask: (taskID: string) => void
|
||||
onCloseTask: () => void
|
||||
onSelectItem: (title: string) => void
|
||||
}) {
|
||||
const { tasks, project } = activeWorkspace
|
||||
const activeTask = tasks.find((task) => task.id === activeTaskID)
|
||||
const lanes = makeLanes(tasks)
|
||||
|
||||
if (activeTask) {
|
||||
return <TaskDetail activeWorkspace={activeWorkspace} task={activeTask} onBack={onCloseTask} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="project-channel-page project-tasks-page overview-page">
|
||||
<div className="overview-head">
|
||||
<div>
|
||||
<Title heading={4}>工作计划</Title>
|
||||
<Text type="secondary">{project.name} 的 TODO 计划、负责人和截止时间。</Text>
|
||||
<Text type="secondary">{project.name} 的 TODO 计划、负责人和创建时间。</Text>
|
||||
</div>
|
||||
<Button type="primary" icon={<IconPlus />}>新建任务</Button>
|
||||
</div>
|
||||
@@ -20,14 +38,14 @@ export function ProjectTasks({ activeWorkspace, onSelectItem }: { activeWorkspac
|
||||
<Card className="queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>任务清单</Title>
|
||||
<Tag color="arcoblue">{tasks.length} 个进行中</Tag>
|
||||
<Tag color="arcoblue">{tasks.length} 个任务</Tag>
|
||||
</div>
|
||||
{tasks.map((task) => (
|
||||
<button className="data-row task-row" key={task.title} onClick={() => onSelectItem(task.title)}>
|
||||
<button className="data-row task-row" key={task.id} onClick={() => onOpenTask(task.id)}>
|
||||
<span className="row-title"><IconCheckCircle /> {task.title}</span>
|
||||
<Tag color="arcoblue">{task.tag}</Tag>
|
||||
<span><IconUser /> {task.owner}</span>
|
||||
<span><IconCalendar /> {task.due}</span>
|
||||
<span><IconCalendar /> {task.createdAt}</span>
|
||||
<Progress percent={Number.parseInt(task.progress, 10)} size="small" />
|
||||
</button>
|
||||
))}
|
||||
@@ -42,7 +60,7 @@ export function ProjectTasks({ activeWorkspace, onSelectItem }: { activeWorkspac
|
||||
</div>
|
||||
<Space direction="vertical" size={8}>
|
||||
{lane.items.map((title) => (
|
||||
<button className="task-card-button" key={title} onClick={() => onSelectItem(title)}>
|
||||
<button className="task-card-button" key={title} onClick={() => openTaskByTitle(tasks, title, onOpenTask, onSelectItem)}>
|
||||
<Text>{title}</Text>
|
||||
<Text type="secondary">优先级 · 本周</Text>
|
||||
</button>
|
||||
@@ -55,6 +73,53 @@ export function ProjectTasks({ activeWorkspace, onSelectItem }: { activeWorkspac
|
||||
)
|
||||
}
|
||||
|
||||
function TaskDetail({ activeWorkspace, task, onBack }: { activeWorkspace: ProjectWorkspace; task: TaskItem; onBack: () => void }) {
|
||||
const percent = Number.parseInt(task.progress, 10)
|
||||
|
||||
return (
|
||||
<div className="project-channel-page project-task-detail-page overview-page">
|
||||
<div className="overview-head">
|
||||
<div>
|
||||
<Button type="text" icon={<IconArrowLeft />} onClick={onBack}>返回任务列表</Button>
|
||||
<Title heading={4}>{task.title}</Title>
|
||||
<Text type="secondary">{activeWorkspace.project.name} 的任务详情</Text>
|
||||
</div>
|
||||
<Tag color={task.completed ? 'green' : 'arcoblue'}>{task.tag}</Tag>
|
||||
</div>
|
||||
|
||||
<Card className="queue-section task-detail-card" bordered>
|
||||
<Descriptions
|
||||
colon=":"
|
||||
column={2}
|
||||
data={[
|
||||
{ label: '所属项目', value: activeWorkspace.project.name },
|
||||
{ label: '负责人', value: task.owner },
|
||||
{ label: '创建时间', value: task.createdAt },
|
||||
{ label: '完成状态', value: task.completed ? '已完成' : '未完成' },
|
||||
]}
|
||||
/>
|
||||
<div className="task-detail-progress">
|
||||
<Text type="secondary">任务进度</Text>
|
||||
<Progress percent={Number.isNaN(percent) ? 0 : percent} />
|
||||
</div>
|
||||
<div className="task-detail-summary">
|
||||
<Title heading={6}>任务说明</Title>
|
||||
<Text>{task.summary || '暂无任务说明'}</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</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)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Badge, Button, Input, Layout, Space, Typography } from '@arco-design/web-react'
|
||||
import { IconApps, IconArrowLeft, IconArrowRight, IconMoon, IconNotification, IconQuestionCircle, IconSearch, IconSun } from '@arco-design/web-react/icon'
|
||||
import { Button, Input, Layout, Space, Typography } from '@arco-design/web-react'
|
||||
import { IconApps, IconMoon, IconSearch, IconSun } from '@arco-design/web-react/icon'
|
||||
import type { Theme } from './project-types'
|
||||
|
||||
const { Header } = Layout
|
||||
const { Text } = Typography
|
||||
|
||||
export function ProjectTopbar({ theme, onToggleTheme }: { theme: Theme; onToggleTheme: () => void }) {
|
||||
const desktop = isDesktopRuntime()
|
||||
|
||||
return (
|
||||
<Header className="topbar">
|
||||
<div className="brand-lockup">
|
||||
@@ -18,12 +20,26 @@ export function ProjectTopbar({ theme, onToggleTheme }: { theme: Theme; onToggle
|
||||
</div>
|
||||
<Space className="topbar-actions">
|
||||
<Button aria-label={theme === 'dark' ? '切换浅色模式' : '切换深色模式'} icon={theme === 'dark' ? <IconSun /> : <IconMoon />} onClick={onToggleTheme} />
|
||||
<Button icon={<IconArrowLeft />} disabled />
|
||||
<Button icon={<IconArrowRight />} disabled />
|
||||
<Badge count={8} dot><Button icon={<IconNotification />} /></Badge>
|
||||
<Button icon={<IconQuestionCircle />} />
|
||||
{desktop && (
|
||||
<>
|
||||
<Button aria-label="停靠左边" icon={<DockIcon side="left" />} />
|
||||
<Button aria-label="停靠右边" icon={<DockIcon side="right" />} />
|
||||
</>
|
||||
)}
|
||||
<Button icon={<IconApps />} />
|
||||
</Space>
|
||||
</Header>
|
||||
)
|
||||
}
|
||||
|
||||
function isDesktopRuntime() {
|
||||
return typeof window !== 'undefined' && '__TAURI__' in window
|
||||
}
|
||||
|
||||
function DockIcon({ side }: { side: 'left' | 'right' }) {
|
||||
return (
|
||||
<span className={`dock-icon dock-icon-${side}`} aria-hidden="true">
|
||||
<span />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ReactElement } from 'react'
|
||||
|
||||
export type Screen = 'login' | 'workbench'
|
||||
export type Theme = 'light' | 'dark'
|
||||
export type WorkbenchView = 'workspace' | 'project'
|
||||
export type WorkbenchView = 'workspace' | 'workspace-inbox' | 'project'
|
||||
export type ChannelKey = 'overview' | 'inbox' | 'tasks' | 'ai' | 'notes' | 'cron' | `custom:${string}`
|
||||
|
||||
export type Project = {
|
||||
@@ -41,6 +41,7 @@ export type TaskItem = {
|
||||
owner: string
|
||||
progress: string
|
||||
due: string
|
||||
createdAt: string
|
||||
tag: string
|
||||
summary: string
|
||||
completed: boolean
|
||||
|
||||
Reference in New Issue
Block a user