feat: update workspace pages and global inbox

This commit is contained in:
2026-07-20 18:25:04 +08:00
parent c64daa8657
commit df12201caa
20 changed files with 597 additions and 229 deletions

View File

@@ -1,14 +1,37 @@
import type { ReactElement } from 'react'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { Button, Card, Form, Input, Space, Typography } from '@arco-design/web-react'
import { IconBook, IconCheckCircleFill, IconLaunch, IconSafe, IconUserGroup } from '@arco-design/web-react/icon'
import {
IconBook,
IconCheckCircleFill,
IconCloseCircleFill,
IconLaunch,
IconLoading,
IconSafe,
IconUserGroup,
} from '@arco-design/web-react/icon'
const { Title, Text, Paragraph } = Typography
type ConnectionStatus = 'checking' | 'online' | 'offline'
export function LoginPage({ onLogin }: { onLogin: (input: { server: string; email: string; password: string }) => void }) {
const [server, setServer] = useState('http://127.0.0.1:8080')
const [server, setServer] = useState('http://localhost:9150')
const [email, setEmail] = useState('demo@senlin.ai')
const [password, setPassword] = useState('password123')
const [status, setStatus] = useState<ConnectionStatus>('checking')
useEffect(() => {
const controller = new AbortController()
const timer = window.setTimeout(() => {
void checkServerStatus(server, controller.signal, setStatus)
}, 300)
return () => {
controller.abort()
window.clearTimeout(timer)
}
}, [server])
return (
<section className="login-screen">
@@ -16,15 +39,14 @@ export function LoginPage({ onLogin }: { onLogin: (input: { server: string; emai
<div className="login-brand-panel">
<div className="brand-lockup large">
<span className="brand-symbol">S</span>
<Title heading={2}>SenlinAI</Title>
<Title heading={2}>Agent</Title>
</div>
<Text className="login-slogan"> · </Text>
<Text type="secondary"> · · AI </Text>
<Text type="secondary"> · · AI Agent</Text>
<Space className="login-footer-links" size={24}>
<Text type="secondary">v1.0.0</Text>
<Text type="secondary"></Text>
<Text type="secondary"></Text>
<Text type="secondary"> SenlinAI</Text>
</Space>
</div>
@@ -34,7 +56,9 @@ export function LoginPage({ onLogin }: { onLogin: (input: { server: string; emai
<Form layout="vertical" className="login-form">
<Form.Item label="服务器地址IP 或域名优先)">
<Input value={server} onChange={setServer} placeholder="例如https://10.0.0.15:8080" suffix={<IconLaunch />} />
<ConnectionCard status={status} onCheck={() => checkServerStatus(server, undefined, setStatus)} />
</Form.Item>
<Form.Item label="账号">
<Input value={email} onChange={setEmail} placeholder="请输入用户名或邮箱" />
</Form.Item>
@@ -51,14 +75,6 @@ export function LoginPage({ onLogin }: { onLogin: (input: { server: string; emai
<Button type="primary" long size="large" onClick={() => onLogin({ server, email, password })}>
</Button>
<div className="connection-card">
<IconCheckCircleFill />
<div>
<Text className="connection-title"></Text>
<Text type="secondary"> {server}</Text>
</div>
<Button type="text" size="mini"></Button>
</div>
</Form>
</div>
@@ -76,6 +92,48 @@ export function LoginPage({ onLogin }: { onLogin: (input: { server: string; emai
)
}
function ConnectionCard({ status, onCheck }: { status: ConnectionStatus; onCheck: () => void }) {
const icon = status === 'checking' ? <IconLoading /> : status === 'online' ? <IconCheckCircleFill /> : <IconCloseCircleFill />
const title = status === 'checking' ? '正在检查' : status === 'online' ? '连接正常' : '连接异常'
return (
<div className={`connection-card ${status}`}>
{icon}
<div>
<Text className="connection-title">{title}</Text>
</div>
<Button type="text" size="mini" loading={status === 'checking'} onClick={onCheck}>
</Button>
</div>
)
}
async function checkServerStatus(
server: string,
signal: AbortSignal | undefined,
setStatus: (status: ConnectionStatus) => void,
) {
const baseUrl = normalizeBaseUrl(server)
setStatus('checking')
try {
const response = await fetch(`${baseUrl}/api/status`, { signal })
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const payload = await response.json() as { timestamp_ms?: number }
if (typeof payload.timestamp_ms !== 'number') throw new Error('invalid status payload')
setStatus('online')
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') return
setStatus('offline')
}
}
function normalizeBaseUrl(value: string) {
const trimmed = value.trim()
return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed
}
function ValuePoint({ icon, title, desc }: { icon: ReactElement; title: string; desc: string }) {
return (
<div className="value-point">

View File

@@ -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':

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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>
)
}

View File

@@ -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

View File

@@ -0,0 +1,92 @@
import { Card, Empty, Grid, Space, Tag, Typography } from '@arco-design/web-react'
import {
IconCalendar,
IconCheckCircleFill,
IconDashboard,
IconFile,
IconRobot,
} from '@arco-design/web-react/icon'
import type { Project, ProjectWorkspace } from './projects/project-types'
const { Row, Col } = Grid
const { Title, Text, Paragraph } = Typography
export function WorkspacePage({ workspaces, onOpenTask }: { workspaces: ProjectWorkspace[]; onOpenTask: (project: Project, taskID: string) => void }) {
const projects = workspaces.map((workspace) => workspace.project)
const unfinishedTasks = workspaces.flatMap((workspace) =>
workspace.tasks
.filter((task) => !task.completed)
.map((task) => ({
...task,
project: workspace.project,
})),
)
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) => (
<button key={`${task.project.id}-${task.id}`} className="workspace-task-card" onClick={() => onOpenTask(task.project, task.id)}>
<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>
</div>
)
}

View File

@@ -1,5 +1,6 @@
import { Layout } from '@arco-design/web-react'
import { WorkspacePage } from './workspace'
import { WorkspacePage } from './workspace-body'
import { WorkspaceInboxPage } from './workspace-inbox'
import { ProjectChannelPage } from './projects/project-channel-page'
import { ProjectRail } from './projects/project-rail'
import { ProjectSidebar } from './projects/project-sidebar'
@@ -14,25 +15,33 @@ export function ProjectPage({
activeWorkspace,
workspaces,
activeChannel,
activeTaskID,
theme,
onSelectProject,
onSelectWorkspace,
onSelectWorkspaceInbox,
onSelectChannel,
onSelectItem,
onOpenTask,
onCloseTask,
onToggleTheme,
}: {
activeView: WorkbenchView
activeWorkspace: ProjectWorkspace
workspaces: ProjectWorkspace[]
activeChannel: ChannelKey
activeTaskID: string | null
theme: Theme
onSelectProject: (project: Project) => void
onSelectWorkspace: () => void
onSelectWorkspaceInbox: () => void
onSelectChannel: (key: ChannelKey) => void
onSelectItem: (title: string) => void
onOpenTask: (project: Project, taskID: string) => void
onCloseTask: () => void
onToggleTheme: () => void
}) {
const isWorkspace = activeView === 'workspace'
const isProject = activeView === 'project'
const projects = workspaces.map((workspace) => workspace.project)
return (
@@ -45,10 +54,11 @@ export function ProjectPage({
projects={projects}
activeView={activeView}
onSelectWorkspace={onSelectWorkspace}
onSelectWorkspaceInbox={onSelectWorkspaceInbox}
onSelectProject={onSelectProject}
/>
{!isWorkspace && (
{isProject && (
<ProjectSidebar
activeView={activeView}
activeWorkspace={activeWorkspace}
@@ -59,13 +69,18 @@ export function ProjectPage({
)}
<Content className="stage">
{isWorkspace ? (
<WorkspacePage workspaces={workspaces} onSelectProject={onSelectProject} />
{activeView === 'workspace' ? (
<WorkspacePage workspaces={workspaces} onOpenTask={onOpenTask} />
) : activeView === 'workspace-inbox' ? (
<WorkspaceInboxPage workspaces={workspaces} onSelectItem={onSelectItem} />
) : (
<ProjectChannelPage
activeChannel={activeChannel}
activeWorkspace={activeWorkspace}
activeTaskID={activeTaskID}
onSelectItem={onSelectItem}
onOpenTask={(taskID) => onOpenTask(activeWorkspace.project, taskID)}
onCloseTask={onCloseTask}
/>
)}
</Content>

View File

@@ -0,0 +1,95 @@
import { Button, Card, Empty, 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 './projects/project-types'
const { Row, Col } = Grid
const { Title, Text, Paragraph } = Typography
export function WorkspaceInboxPage({ workspaces, onSelectItem }: { workspaces: ProjectWorkspace[]; onSelectItem: (title: string) => void }) {
const messages = workspaces.flatMap((workspace) => workspace.inbox.map((item) => ({ ...item, project: workspace.project })))
const selected = messages[0]
const openCount = messages.filter((item) => item.status === 'open').length
const handledCount = messages.length - openCount
return (
<div className="workspace-inbox-page overview-page">
<div className="overview-head">
<div>
<Title heading={4}>Inbox</Title>
<Text type="secondary">线</Text>
</div>
<Space>
<Button icon={<IconRefresh />}></Button>
<Button type="primary" icon={<IconArchive />}></Button>
</Space>
</div>
<Row gutter={12}>
<Col span={8}>
<Card className="compact-card" bordered>
<Space><Tag color="red">{openCount}</Tag><Text></Text></Space>
</Card>
</Col>
<Col span={8}>
<Card className="compact-card" bordered>
<Space><Tag color="arcoblue">{messages.length}</Tag><Text></Text></Space>
</Card>
</Col>
<Col span={8}>
<Card className="compact-card" bordered>
<Space><Tag color="green">{handledCount}</Tag><Text></Text></Space>
</Card>
</Col>
</Row>
{messages.length ? (
<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>
{messages.map((item, index) => (
<button
className={index === 0 ? 'mail-item active' : 'mail-item'}
key={`${item.project.id}-${item.id}`}
onClick={() => onSelectItem(item.title)}
>
<span><IconEmail /> {item.title}</span>
<Text type="secondary">{item.meta}</Text>
<div>
<Tag color={item.project.urgent ? 'red' : 'arcoblue'}>{item.project.name}</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.meta} · {selected.project.name}</Text>
<Paragraph>{selected.summary || '暂无消息正文'}</Paragraph>
<div className="reply-box">
<Text type="secondary"></Text>
<Space>
<Button></Button>
<Button></Button>
<Button type="primary"></Button>
</Space>
</div>
</Card>
</div>
) : (
<Card className="queue-section" bordered>
<Empty description="暂无外部消息" />
</Card>
)}
</div>
)
}

View File

@@ -1,89 +0,0 @@
import { Button, Card, Grid, Space, Tag, Typography } from '@arco-design/web-react'
import {
IconCheckCircleFill,
IconDashboard,
IconEmail,
IconFile,
IconRobot,
} from '@arco-design/web-react/icon'
import type { Project, ProjectWorkspace } from './projects/project-types'
const { Row, Col } = Grid
const { Title, Text } = Typography
export function WorkspacePage({ workspaces, onSelectProject }: { workspaces: ProjectWorkspace[]; onSelectProject: (project: Project) => void }) {
const projects = workspaces.map((workspace) => workspace.project)
const inbox = workspaces.flatMap((workspace) => workspace.inbox)
const tasks = workspaces.flatMap((workspace) => workspace.tasks).filter((task) => !task.completed)
const aiSessions = workspaces.flatMap((workspace) => workspace.aiSessions)
const notes = workspaces.flatMap((workspace) => workspace.notes)
const totalProjectBadges = projects.reduce((sum, project) => sum + project.badge, 0)
const urgentProjects = projects.filter((project) => project.urgent).length
const workspaceMetrics = [
{ title: '项目总数', value: projects.length, delta: `${urgentProjects} 个高优先级`, icon: <IconDashboard />, color: 'arcoblue' },
{ title: '全局待处理', value: totalProjectBadges, delta: '跨项目聚合', icon: <IconEmail />, color: 'red' },
{ title: '进行中任务', value: tasks.length, delta: '本周持续推进', icon: <IconCheckCircleFill />, color: 'green' },
{ title: 'AI 会话', value: aiSessions.length, delta: '队列空闲', icon: <IconRobot />, color: 'purple' },
{ title: '知识资料', value: notes.length, delta: '最近 7 天更新', icon: <IconFile />, color: 'cyan' },
]
return (
<div className="workspace-page overview-page">
<div className="overview-head">
<div>
<Title heading={4}></Title>
<Text type="secondary"></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>
<Button type="text" size="mini"></Button>
</div>
{projects.map((project) => (
<button key={project.id} className="data-row" onClick={() => onSelectProject(project)}>
<span className="row-title"><IconDashboard /> {project.name}</span>
<Tag color={project.urgent ? 'red' : 'green'}>{project.urgent ? '高优先级' : '正常'}</Tag>
<span> {project.badge}</span>
<span> {project.short}</span>
<time></time>
</button>
))}
</Card>
<Card className="queue-section" bordered>
<div className="section-header">
<Title heading={6}></Title>
<Button type="text" size="mini"> Inbox</Button>
</div>
{inbox.slice(0, 4).map((item) => (
<div key={item.title} className="data-row">
<span className="row-title"><IconEmail /> {item.title}</span>
<span>{item.meta}</span>
<Tag>{item.tag}</Tag>
<span>{item.owner}</span>
<time>{item.time}</time>
</div>
))}
</Card>
</div>
)
}