From df12201caac2bef9a8257000eabff4bc542f5ecf Mon Sep 17 00:00:00 2001 From: yanweidong Date: Mon, 20 Jul 2026 18:25:04 +0800 Subject: [PATCH] feat: update workspace pages and global inbox --- .../scripts/structure-check.mjs | 8 +- .../scripts/visual-check.mjs | 29 ++++- apps/senlinai-acro-react/src/App.css | 109 ++++++++++++++++++ apps/senlinai-acro-react/src/api/mappers.tsx | 34 +++++- apps/senlinai-acro-react/src/api/projects.ts | 1 + apps/senlinai-acro-react/src/app/App.tsx | 22 +++- apps/senlinai-acro-react/src/pages/login.tsx | 86 +++++++++++--- .../pages/projects/project-channel-page.tsx | 11 +- .../src/pages/projects/project-inbox.tsx | 90 --------------- .../src/pages/projects/project-overview.tsx | 2 +- .../src/pages/projects/project-rail.tsx | 18 ++- .../src/pages/projects/project-tasks.tsx | 81 +++++++++++-- .../src/pages/projects/project-topbar.tsx | 28 ++++- .../src/pages/projects/project-types.ts | 3 +- .../src/pages/workspace-body.tsx | 92 +++++++++++++++ .../pages/{project.tsx => workspace-home.tsx} | 25 +++- .../src/pages/workspace-inbox.tsx | 95 +++++++++++++++ .../src/pages/workspace.tsx | 89 -------------- backend/internal/logic/projects/service.go | 2 + .../internal/logic/projects/service_test.go | 1 + 20 files changed, 597 insertions(+), 229 deletions(-) delete mode 100644 apps/senlinai-acro-react/src/pages/projects/project-inbox.tsx create mode 100644 apps/senlinai-acro-react/src/pages/workspace-body.tsx rename apps/senlinai-acro-react/src/pages/{project.tsx => workspace-home.tsx} (70%) create mode 100644 apps/senlinai-acro-react/src/pages/workspace-inbox.tsx delete mode 100644 apps/senlinai-acro-react/src/pages/workspace.tsx diff --git a/apps/senlinai-acro-react/scripts/structure-check.mjs b/apps/senlinai-acro-react/scripts/structure-check.mjs index abff6bf..0a80fac 100644 --- a/apps/senlinai-acro-react/scripts/structure-check.mjs +++ b/apps/senlinai-acro-react/scripts/structure-check.mjs @@ -3,11 +3,11 @@ import { existsSync, readFileSync } from 'node:fs' const requiredFiles = [ 'src/app/App.tsx', 'src/pages/login.tsx', - 'src/pages/workspace.tsx', - 'src/pages/project.tsx', + 'src/pages/workspace-body.tsx', + 'src/pages/workspace-home.tsx', + 'src/pages/workspace-inbox.tsx', 'src/pages/projects/project-overview.tsx', 'src/pages/projects/project-channel-page.tsx', - 'src/pages/projects/project-inbox.tsx', 'src/pages/projects/project-tasks.tsx', 'src/pages/projects/project-ai.tsx', 'src/pages/projects/project-notes.tsx', @@ -28,7 +28,7 @@ if (existsSync('src/App.tsx')) { failures.push('legacy src/App.tsx should be removed after page split') } -for (const removed of ['src/pages/projects/project-data.tsx', 'src/pages/projects/project-inspector.tsx']) { +for (const removed of ['src/pages/projects/project-data.tsx', 'src/pages/projects/project-inspector.tsx', 'src/pages/projects/project-inbox.tsx']) { if (existsSync(removed)) { failures.push(`${removed} should be removed; frontend data must come from backend APIs`) } diff --git a/apps/senlinai-acro-react/scripts/visual-check.mjs b/apps/senlinai-acro-react/scripts/visual-check.mjs index e33d6db..c8156db 100644 --- a/apps/senlinai-acro-react/scripts/visual-check.mjs +++ b/apps/senlinai-acro-react/scripts/visual-check.mjs @@ -38,6 +38,7 @@ const collectMetrics = async () => page.evaluate(() => { const workspaceActions = [...document.querySelectorAll('.workspace-page .overview-head .arco-btn')] .map((button) => button.textContent?.trim()) const dashboard = document.querySelector('.dashboard-button') + const workspaceInbox = document.querySelector('.dashboard-button[title="Inbox"]') const project = document.querySelector('.project-button') const create = document.querySelector('.create-project') const firstBadge = document.querySelector('.project-badge .arco-badge-number') @@ -83,6 +84,8 @@ const collectMetrics = async () => page.evaluate(() => { workspaceActions, dashboardButton: rect(dashboard), dashboardButtonActive: dashboard?.classList.contains('active') ?? false, + workspaceInboxButton: rect(workspaceInbox), + workspaceInboxButtonActive: workspaceInbox?.classList.contains('active') ?? false, projectButton: rect(project), projectButtonActive: project?.classList.contains('active') ?? false, createProjectButton: rect(create), @@ -108,6 +111,11 @@ await page.waitForTimeout(700) await page.screenshot({ path: 'test-results/workbench-react-acro-light.png', fullPage: true }) const workspaceMetrics = await collectMetrics() +await page.locator('.dashboard-button[title="Inbox"]').click() +await page.waitForTimeout(500) +await page.screenshot({ path: 'test-results/workspace-inbox-react-acro-light.png', fullPage: true }) +const workspaceInboxMetrics = await collectMetrics() + await page.locator('.project-button').first().click() await page.waitForTimeout(500) await page.screenshot({ path: 'test-results/project-react-acro-light.png', fullPage: true }) @@ -122,7 +130,6 @@ const channelListHoverMetrics = await collectMetrics() const channelPageChecks = [] for (const channel of [ - { label: 'Inbox 消息流', pageClass: 'project-inbox-page', expectedHeading: 'Inbox 消息流' }, { label: '工作计划', pageClass: 'project-tasks-page', expectedHeading: '工作计划' }, { label: 'AI 会话', pageClass: 'project-ai-page', expectedHeading: 'AI 会话' }, { label: '笔记资料', pageClass: 'project-notes-page', expectedHeading: '笔记资料' }, @@ -151,6 +158,7 @@ await server.close() console.log(JSON.stringify({ workspaceMetrics, + workspaceInboxMetrics, projectMetrics, channelSidebarHoverMetrics, stageHoverMetrics, @@ -172,6 +180,8 @@ if (metrics.overflowX) failures.push('expected no horizontal overflow') if (!metrics.workbenchMain) failures.push('missing workbench main') if (!metrics.hasWorkspacePage) failures.push('expected login to land on workspace page') if (!metrics.dashboardButtonActive) failures.push('expected dashboard button to be active after login') +if (!metrics.workspaceInboxButton) failures.push('expected fixed workspace inbox button to render in project rail') +if (metrics.workspaceInboxButtonActive) failures.push('expected fixed workspace inbox button not to be active after login') if (metrics.projectButtonActive) failures.push('expected first project button not to be active on workspace landing page') if (metrics.channelSidebar) failures.push(`expected workspace to hide channel sidebar, got ${JSON.stringify(metrics.channelSidebar)}`) if (metrics.inspector) failures.push(`expected workspace to hide inspector, got ${JSON.stringify(metrics.inspector)}`) @@ -220,6 +230,13 @@ if ( ) { failures.push(`dashboard button size ${JSON.stringify(metrics.dashboardButton)} does not match project button ${JSON.stringify(metrics.projectButton)}`) } +if ( + !metrics.workspaceInboxButton || + metrics.workspaceInboxButton.width !== metrics.projectButton?.width || + metrics.workspaceInboxButton.height !== metrics.projectButton?.height +) { + failures.push(`workspace inbox button size ${JSON.stringify(metrics.workspaceInboxButton)} does not match project button ${JSON.stringify(metrics.projectButton)}`) +} if ( !metrics.createProjectButton || metrics.createProjectButton.width !== metrics.projectButton?.width || @@ -228,6 +245,16 @@ if ( failures.push(`create project button size ${JSON.stringify(metrics.createProjectButton)} does not match project button ${JSON.stringify(metrics.projectButton)}`) } +if (!workspaceInboxMetrics.workspaceInboxButtonActive) failures.push('expected fixed workspace inbox button to become active when selected') +if (workspaceInboxMetrics.dashboardButtonActive) failures.push('expected dashboard button not to be active on workspace inbox page') +if (workspaceInboxMetrics.channelSidebar) failures.push(`expected workspace inbox to hide channel sidebar, got ${JSON.stringify(workspaceInboxMetrics.channelSidebar)}`) +if (workspaceInboxMetrics.inspector) failures.push(`expected workspace inbox to hide inspector, got ${JSON.stringify(workspaceInboxMetrics.inspector)}`) +if (!workspaceInboxMetrics.stage || !workspaceInboxMetrics.projectRail) { + failures.push(`missing workspace inbox layout regions: rail=${JSON.stringify(workspaceInboxMetrics.projectRail)}, stage=${JSON.stringify(workspaceInboxMetrics.stage)}`) +} else if (Math.abs(workspaceInboxMetrics.stage.left - workspaceInboxMetrics.projectRail.right) > 1) { + failures.push(`expected workspace inbox stage to start after project rail, got stage.left=${workspaceInboxMetrics.stage.left}, rail.right=${workspaceInboxMetrics.projectRail.right}`) +} + if (!projectMetrics.hasProjectOverview) failures.push('expected first project click to show project overview page') if (projectMetrics.hasWorkspacePage) failures.push('expected project overview mode to leave workspace page') if (!projectMetrics.projectButtonActive) failures.push('expected first project button to be active in project mode') diff --git a/apps/senlinai-acro-react/src/App.css b/apps/senlinai-acro-react/src/App.css index 16626f1..ff6eaa3 100644 --- a/apps/senlinai-acro-react/src/App.css +++ b/apps/senlinai-acro-react/src/App.css @@ -271,6 +271,35 @@ justify-content: flex-end; } +.dock-icon { + position: relative; + display: inline-block; + width: 16px; + height: 14px; + border: 1.5px solid currentcolor; + border-radius: 2px; + box-sizing: border-box; +} + +.dock-icon span { + position: absolute; + top: 0; + bottom: 0; + width: 5px; + background: currentcolor; + opacity: 0.2; +} + +.dock-icon-left span { + left: 0; + border-right: 1.5px solid currentcolor; +} + +.dock-icon-right span { + right: 0; + border-left: 1.5px solid currentcolor; +} + .workbench-main { min-height: 0; min-width: 0; @@ -550,6 +579,65 @@ grid-template-columns: minmax(260px, 1fr) 82px 100px 100px 130px; } +.workspace-task-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 12px; + padding-top: 8px; +} + +.workspace-task-card { + display: flex; + min-height: 148px; + flex-direction: column; + justify-content: space-between; + gap: 14px; + 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; +} + +.workspace-task-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); +} + +.workspace-task-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; +} + +.workspace-task-title svg { + color: var(--senlin-primary); +} + +.workspace-task-main .arco-typography { + margin: 8px 0 0; +} + +.workspace-task-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 10px; + color: var(--senlin-muted); + font-size: 12px; +} + +.workspace-task-meta span { + display: inline-flex; + align-items: center; + gap: 4px; +} + .ai-row { grid-template-columns: minmax(260px, 1fr) minmax(260px, 1.2fr) 80px 90px; } @@ -631,6 +719,27 @@ font-size: 12px; } +.task-detail-card .arco-card-body { + display: grid; + gap: 18px; +} + +.task-detail-progress { + display: grid; + gap: 8px; +} + +.task-detail-summary { + display: grid; + gap: 8px; + border-top: 1px solid var(--senlin-border); + padding-top: 14px; +} + +.task-detail-summary h6 { + margin: 0; +} + .mail-detail .arco-typography, .mail-item .arco-typography, .ai-session .arco-typography { diff --git a/apps/senlinai-acro-react/src/api/mappers.tsx b/apps/senlinai-acro-react/src/api/mappers.tsx index 93bd421..1b8ee08 100644 --- a/apps/senlinai-acro-react/src/api/mappers.tsx +++ b/apps/senlinai-acro-react/src/api/mappers.tsx @@ -24,7 +24,7 @@ export function mapWorkspace(payload: BackendProjectWorkspace, index = 0): Proje return { project, - channels: payload.channels.map(mapChannel), + channels: payload.channels.filter((channel) => channel.type !== 'inbox').map(mapChannel), tags: payload.tags.map((tag) => (tag === 'all' ? '全部' : tag)), recentSessions: payload.recentSessions.map(mapAISession), inbox: payload.inbox.map(mapInbox), @@ -124,12 +124,44 @@ function mapTask(task: BackendTask): TaskItem { owner: task.owner, progress: task.completed ? '100%' : '60%', due: task.due || '未设置', + createdAt: formatCreatedAt(task.createdAt), tag: task.tag || (task.completed ? '已完成' : '进行中'), summary: task.summary, completed: task.completed, } } +function formatCreatedAt(value: string) { + const date = parseBackendDate(value) + if (!date) return '未知时间' + + const now = new Date() + const today = startOfDay(now) + const yesterday = new Date(today) + yesterday.setDate(today.getDate() - 1) + const dateDay = startOfDay(date) + const time = `${pad(date.getHours())}:${pad(date.getMinutes())}` + + if (dateDay.getTime() === today.getTime()) return `今天 ${time}` + if (dateDay.getTime() === yesterday.getTime()) return `昨天 ${time}` + return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${time}` +} + +function parseBackendDate(value: string) { + if (!value) return null + const normalized = value.includes('T') ? value : `${value.replace(' ', 'T')}Z` + const date = new Date(normalized) + return Number.isNaN(date.getTime()) ? null : date +} + +function startOfDay(value: Date) { + return new Date(value.getFullYear(), value.getMonth(), value.getDate()) +} + +function pad(value: number) { + return String(value).padStart(2, '0') +} + function mapAISession(session: BackendAISession): AISession { return { id: session.id, diff --git a/apps/senlinai-acro-react/src/api/projects.ts b/apps/senlinai-acro-react/src/api/projects.ts index a8114f7..a111419 100644 --- a/apps/senlinai-acro-react/src/api/projects.ts +++ b/apps/senlinai-acro-react/src/api/projects.ts @@ -45,6 +45,7 @@ export type BackendTask = { completed: boolean owner: string due: string + createdAt: string tag: string } diff --git a/apps/senlinai-acro-react/src/app/App.tsx b/apps/senlinai-acro-react/src/app/App.tsx index d2aeb01..1a93351 100644 --- a/apps/senlinai-acro-react/src/app/App.tsx +++ b/apps/senlinai-acro-react/src/app/App.tsx @@ -6,8 +6,8 @@ import { login, setApiBaseUrl, type ApiSession } from '../api/client' import { mapWorkspace } from '../api/mappers' import { fetchProjectWorkspace, fetchProjects } from '../api/projects' import { LoginPage } from '../pages/login' -import { ProjectPage } from '../pages/project' -import type { ChannelKey, ProjectWorkspace, Screen, Theme, WorkbenchView } from '../pages/projects/project-types' +import { ProjectPage } from '../pages/workspace-home' +import type { ChannelKey, Project, ProjectWorkspace, Screen, Theme, WorkbenchView } from '../pages/projects/project-types' function App() { const [screen, setScreen] = useState('login') @@ -17,6 +17,7 @@ function App() { const [workspaces, setWorkspaces] = useState([]) const [activeProjectID, setActiveProjectID] = useState('') const [activeChannel, setActiveChannel] = useState('overview') + const [activeTaskID, setActiveTaskID] = useState(null) const [, setSelectedItem] = useState('Inbox 待处理(36)') const [loading, setLoading] = useState(false) @@ -45,6 +46,13 @@ function App() { } } + function openTask(project: Project, taskID: string) { + setActiveProjectID(project.id) + setActiveView('project') + setActiveChannel('tasks') + setActiveTaskID(taskID) + } + return (
@@ -58,15 +66,23 @@ function App() { activeWorkspace={activeWorkspace} workspaces={workspaces} activeChannel={activeChannel} + activeTaskID={activeTaskID} theme={theme} onSelectWorkspace={() => setActiveView('workspace')} + onSelectWorkspaceInbox={() => setActiveView('workspace-inbox')} onSelectProject={(project) => { setActiveProjectID(project.id) setActiveView('project') setActiveChannel('overview') + setActiveTaskID(null) + }} + onSelectChannel={(channel) => { + setActiveChannel(channel) + setActiveTaskID(null) }} - onSelectChannel={setActiveChannel} onSelectItem={setSelectedItem} + onOpenTask={openTask} + onCloseTask={() => setActiveTaskID(null)} onToggleTheme={() => setTheme(dark ? 'light' : 'dark')} /> ) : ( diff --git a/apps/senlinai-acro-react/src/pages/login.tsx b/apps/senlinai-acro-react/src/pages/login.tsx index af6d8af..f477e8f 100644 --- a/apps/senlinai-acro-react/src/pages/login.tsx +++ b/apps/senlinai-acro-react/src/pages/login.tsx @@ -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('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 (
@@ -16,15 +39,14 @@ export function LoginPage({ onLogin }: { onLogin: (input: { server: string; emai
S - SenlinAI + 森林Agent
私有部署 · 安全可控的智能协作平台 - 知识沉淀 · 团队协作 · AI 助理 + 知识沉淀 · 团队协作 · AI Agent v1.0.0 隐私政策 服务协议 - 关于 SenlinAI
@@ -34,7 +56,9 @@ export function LoginPage({ onLogin }: { onLogin: (input: { server: string; emai
} /> + checkServerStatus(server, undefined, setStatus)} /> + @@ -51,14 +75,6 @@ export function LoginPage({ onLogin }: { onLogin: (input: { server: string; emai -
- -
- 连接正常 - 已连接到 {server} -
- -
@@ -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' ? : status === 'online' ? : + const title = status === 'checking' ? '正在检查' : status === 'online' ? '连接正常' : '连接异常' + + return ( +
+ {icon} +
+ {title} +
+ +
+ ) +} + +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 (
diff --git a/apps/senlinai-acro-react/src/pages/projects/project-channel-page.tsx b/apps/senlinai-acro-react/src/pages/projects/project-channel-page.tsx index 18ea4c4..34cd8f0 100644 --- a/apps/senlinai-acro-react/src/pages/projects/project-channel-page.tsx +++ b/apps/senlinai-acro-react/src/pages/projects/project-channel-page.tsx @@ -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 case 'tasks': - return + return case 'ai': return case 'notes': diff --git a/apps/senlinai-acro-react/src/pages/projects/project-inbox.tsx b/apps/senlinai-acro-react/src/pages/projects/project-inbox.tsx deleted file mode 100644 index 0de3312..0000000 --- a/apps/senlinai-acro-react/src/pages/projects/project-inbox.tsx +++ /dev/null @@ -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 ( -
-
-
- Inbox 消息流 - {project.name} 的邮件式收件箱,聚合外部反馈、系统通知和待处理线索。 -
- - - - -
- - - {folders.map((folder) => ( - - - - {folder.count} - {folder.label} - - - - ))} - - -
- -
- 今日消息 - -
- {inbox.map((item, index) => ( - - ))} -
- - -
- {selected?.title ?? '暂无消息'} - -
- {selected ? `${selected.meta} · ${selected.owner}` : '真实接口暂无消息'} - - 已收到新的项目消息,需要判断是否转为任务、笔记或资料。当前建议先补充影响范围,再同步给相关负责人。 - -
- 快速处理 - - - - - -
-
-
-
- ) -} diff --git a/apps/senlinai-acro-react/src/pages/projects/project-overview.tsx b/apps/senlinai-acro-react/src/pages/projects/project-overview.tsx index 4244401..820135d 100644 --- a/apps/senlinai-acro-react/src/pages/projects/project-overview.tsx +++ b/apps/senlinai-acro-react/src/pages/projects/project-overview.tsx @@ -86,7 +86,7 @@ function TaskSection({ tasks, onSelectItem }: { tasks: ProjectWorkspace['tasks'] {task.tag} 进度 {task.progress} 负责人 {task.owner} - + ))} diff --git a/apps/senlinai-acro-react/src/pages/projects/project-rail.tsx b/apps/senlinai-acro-react/src/pages/projects/project-rail.tsx index af0a6dc..d4636f8 100644 --- a/apps/senlinai-acro-react/src/pages/projects/project-rail.tsx +++ b/apps/senlinai-acro-react/src/pages/projects/project-rail.tsx @@ -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="工作台" /> + diff --git a/apps/senlinai-acro-react/src/pages/projects/project-tasks.tsx b/apps/senlinai-acro-react/src/pages/projects/project-tasks.tsx index 2e74e2c..6ce6ccb 100644 --- a/apps/senlinai-acro-react/src/pages/projects/project-tasks.tsx +++ b/apps/senlinai-acro-react/src/pages/projects/project-tasks.tsx @@ -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 + } + return (
工作计划 - {project.name} 的 TODO 计划、负责人和截止时间。 + {project.name} 的 TODO 计划、负责人和创建时间。
@@ -20,14 +38,14 @@ export function ProjectTasks({ activeWorkspace, onSelectItem }: { activeWorkspac
任务清单 - {tasks.length} 个进行中 + {tasks.length} 个任务
{tasks.map((task) => ( - ))} @@ -42,7 +60,7 @@ export function ProjectTasks({ activeWorkspace, onSelectItem }: { activeWorkspac
{lane.items.map((title) => ( - @@ -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 ( +
+
+
+ + {task.title} + {activeWorkspace.project.name} 的任务详情 +
+ {task.tag} +
+ + + +
+ 任务进度 + +
+
+ 任务说明 + {task.summary || '暂无任务说明'} +
+
+
+ ) +} + +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) diff --git a/apps/senlinai-acro-react/src/pages/projects/project-topbar.tsx b/apps/senlinai-acro-react/src/pages/projects/project-topbar.tsx index 4f01e84..b901d32 100644 --- a/apps/senlinai-acro-react/src/pages/projects/project-topbar.tsx +++ b/apps/senlinai-acro-react/src/pages/projects/project-topbar.tsx @@ -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 (
@@ -18,12 +20,26 @@ export function ProjectTopbar({ theme, onToggleTheme }: { theme: Theme; onToggle
) } + +function isDesktopRuntime() { + return typeof window !== 'undefined' && '__TAURI__' in window +} + +function DockIcon({ side }: { side: 'left' | 'right' }) { + return ( +