From da76abf2d0801829370a6f20c083d629fd9d1670 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Mon, 20 Jul 2026 11:40:45 +0800 Subject: [PATCH] Add workspace landing page --- .../scripts/structure-check.mjs | 1 + .../scripts/visual-check.mjs | 17 ++++ apps/senlinai-acro-react/src/App.css | 6 ++ apps/senlinai-acro-react/src/app/App.tsx | 10 +- .../senlinai-acro-react/src/pages/project.tsx | 28 +++++- .../src/pages/projects/project-inspector.tsx | 37 +++++++- .../src/pages/projects/project-rail.tsx | 24 ++++- .../src/pages/projects/project-sidebar.tsx | 73 ++++++++++++--- .../src/pages/projects/project-types.ts | 1 + .../src/pages/workspace.tsx | 91 +++++++++++++++++++ 10 files changed, 260 insertions(+), 28 deletions(-) create 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 9b0c0b9..9cca642 100644 --- a/apps/senlinai-acro-react/scripts/structure-check.mjs +++ b/apps/senlinai-acro-react/scripts/structure-check.mjs @@ -3,6 +3,7 @@ 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/projects/project-overview.tsx', 'src/pages/projects/project-data.tsx', diff --git a/apps/senlinai-acro-react/scripts/visual-check.mjs b/apps/senlinai-acro-react/scripts/visual-check.mjs index 0ba035d..63d85b3 100644 --- a/apps/senlinai-acro-react/scripts/visual-check.mjs +++ b/apps/senlinai-acro-react/scripts/visual-check.mjs @@ -34,10 +34,13 @@ const metrics = await page.evaluate(() => { const statusbar = document.querySelector('.statusbar') const search = document.querySelector('.global-search') const main = document.querySelector('.workbench-main') + const workspacePage = document.querySelector('.workspace-page') const rail = document.querySelector('.project-rail') const railChildren = rail?.querySelector('.arco-layout-sider-children') const sidebar = document.querySelector('.channel-sidebar') + const sidebarTitle = document.querySelector('.project-title h5') const stage = document.querySelector('.stage') + const inspectorTitle = document.querySelector('.inspector-title h5') const inspector = document.querySelector('.inspector') const dashboard = document.querySelector('.dashboard-button') const project = document.querySelector('.project-button') @@ -72,13 +75,18 @@ const metrics = await page.evaluate(() => { statusbarHeight: statusbar?.getBoundingClientRect().height, searchHeight: search?.getBoundingClientRect().height, workbenchMain: rect(main), + hasWorkspacePage: Boolean(workspacePage), projectRailWidth: rail?.getBoundingClientRect().width, projectRail: rect(rail), channelSidebar: rect(sidebar), + sidebarTitleText: sidebarTitle?.textContent ?? '', stage: rect(stage), inspector: rect(inspector), + inspectorTitleText: inspectorTitle?.textContent ?? '', dashboardButton: rect(dashboard), + dashboardButtonActive: dashboard?.classList.contains('active') ?? false, projectButton: rect(project), + projectButtonActive: project?.classList.contains('active') ?? false, createProjectButton: rect(create), firstProjectBadge: rect(firstBadge), horizontalInsets, @@ -101,6 +109,15 @@ if (metrics.statusbarHeight !== 32) failures.push(`expected statusbar height 32, if (metrics.searchHeight !== 42) failures.push(`expected search height 42, got ${metrics.searchHeight}`) 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.projectButtonActive) failures.push('expected first project button not to be active on workspace landing page') +if (!metrics.sidebarTitleText.includes('工作台')) { + failures.push(`expected sidebar title to describe workspace landing page, got ${JSON.stringify(metrics.sidebarTitleText)}`) +} +if (!metrics.inspectorTitleText.includes('工作台')) { + failures.push(`expected inspector title to describe workspace landing page, got ${JSON.stringify(metrics.inspectorTitleText)}`) +} if (!metrics.projectRail || !metrics.channelSidebar || !metrics.stage || !metrics.inspector) { failures.push( `missing main layout regions: rail=${JSON.stringify(metrics.projectRail)}, sidebar=${JSON.stringify(metrics.channelSidebar)}, stage=${JSON.stringify(metrics.stage)}, inspector=${JSON.stringify(metrics.inspector)}`, diff --git a/apps/senlinai-acro-react/src/App.css b/apps/senlinai-acro-react/src/App.css index fd6640c..a7d8a26 100644 --- a/apps/senlinai-acro-react/src/App.css +++ b/apps/senlinai-acro-react/src/App.css @@ -312,6 +312,12 @@ border-radius: 8px; } +.dashboard-button.active { + border-color: var(--senlin-primary); + background: var(--senlin-primary); + color: white; +} + .create-project { font-size: 11px; white-space: normal; diff --git a/apps/senlinai-acro-react/src/app/App.tsx b/apps/senlinai-acro-react/src/app/App.tsx index 6193b2c..47649c3 100644 --- a/apps/senlinai-acro-react/src/app/App.tsx +++ b/apps/senlinai-acro-react/src/app/App.tsx @@ -5,11 +5,12 @@ import '../App.css' import { LoginPage } from '../pages/login' import { ProjectPage } from '../pages/project' import { projects } from '../pages/projects/project-data' -import type { ChannelKey, Screen, Theme } from '../pages/projects/project-types' +import type { ChannelKey, Screen, Theme, WorkbenchView } from '../pages/projects/project-types' function App() { const [screen, setScreen] = useState('login') const [theme, setTheme] = useState('light') + const [activeView, setActiveView] = useState('workspace') const [activeProject, setActiveProject] = useState(projects[0]) const [activeChannel, setActiveChannel] = useState('overview') const [selectedItem, setSelectedItem] = useState('Inbox 待处理(36)') @@ -23,11 +24,16 @@ function App() { setScreen('workbench')} /> ) : ( setActiveView('workspace')} + onSelectProject={(project) => { + setActiveProject(project) + setActiveView('project') + }} onSelectChannel={setActiveChannel} onSelectItem={setSelectedItem} onToggleTheme={() => setTheme(dark ? 'light' : 'dark')} diff --git a/apps/senlinai-acro-react/src/pages/project.tsx b/apps/senlinai-acro-react/src/pages/project.tsx index d5ebed0..f381ac6 100644 --- a/apps/senlinai-acro-react/src/pages/project.tsx +++ b/apps/senlinai-acro-react/src/pages/project.tsx @@ -1,29 +1,34 @@ import { Layout } from '@arco-design/web-react' +import { WorkspacePage } from './workspace' import { ProjectOverview } from './projects/project-overview' import { ProjectInspector } from './projects/project-inspector' import { ProjectRail } from './projects/project-rail' import { ProjectSidebar } from './projects/project-sidebar' import { ProjectStatusbar } from './projects/project-statusbar' import { ProjectTopbar } from './projects/project-topbar' -import type { ChannelKey, Project, Theme } from './projects/project-types' +import type { ChannelKey, Project, Theme, WorkbenchView } from './projects/project-types' const { Content } = Layout export function ProjectPage({ + activeView, activeProject, activeChannel, selectedItem, theme, onSelectProject, + onSelectWorkspace, onSelectChannel, onSelectItem, onToggleTheme, }: { + activeView: WorkbenchView activeProject: Project activeChannel: ChannelKey selectedItem: string theme: Theme onSelectProject: (project: Project) => void + onSelectWorkspace: () => void onSelectChannel: (key: ChannelKey) => void onSelectItem: (title: string) => void onToggleTheme: () => void @@ -33,9 +38,15 @@ export function ProjectPage({ - + - + {activeView === 'workspace' ? ( + + ) : ( + + )} - + diff --git a/apps/senlinai-acro-react/src/pages/projects/project-inspector.tsx b/apps/senlinai-acro-react/src/pages/projects/project-inspector.tsx index de2b565..7c9c6ef 100644 --- a/apps/senlinai-acro-react/src/pages/projects/project-inspector.tsx +++ b/apps/senlinai-acro-react/src/pages/projects/project-inspector.tsx @@ -1,18 +1,45 @@ import { Avatar, Button, Divider, Input, Layout, Message, Space, Tabs, Typography } from '@arco-design/web-react' import { IconLink, IconMessage, IconMore } from '@arco-design/web-react/icon' import { channels, discussions } from './project-data' -import type { ChannelKey, Project } from './project-types' +import type { ChannelKey, Project, WorkbenchView } from './project-types' const { Sider } = Layout const { Title, Text, Paragraph } = Typography -export function ProjectInspector({ activeProject, activeChannel, selectedItem }: { activeProject: Project; activeChannel: ChannelKey; selectedItem: string }) { +export function ProjectInspector({ + activeView, + activeProject, + activeChannel, + selectedItem, +}: { + activeView: WorkbenchView + activeProject: Project + activeChannel: ChannelKey + selectedItem: string +}) { + const workspaceMode = activeView === 'workspace' + return ( - -
c.key === activeChannel)?.label ?? '概况'} />
- + + + + +
+ + channel.key === activeChannel)?.label ?? '概况'} /> + + +
+
+ + + + + + +
) 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 cc33df7..6d83b19 100644 --- a/apps/senlinai-acro-react/src/pages/projects/project-rail.tsx +++ b/apps/senlinai-acro-react/src/pages/projects/project-rail.tsx @@ -2,18 +2,34 @@ 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 { projects } from './project-data' -import type { Project } from './project-types' +import type { Project, WorkbenchView } from './project-types' const { Sider } = Layout -export function ProjectRail({ activeProject, onSelectProject }: { activeProject: Project; onSelectProject: (project: Project) => void }) { +export function ProjectRail({ + activeProject, + activeView, + onSelectWorkspace, + onSelectProject, +}: { + activeProject: Project + activeView: WorkbenchView + onSelectWorkspace: () => void + onSelectProject: (project: Project) => void +}) { return ( - diff --git a/apps/senlinai-acro-react/src/pages/projects/project-sidebar.tsx b/apps/senlinai-acro-react/src/pages/projects/project-sidebar.tsx index e0e35af..e37a558 100644 --- a/apps/senlinai-acro-react/src/pages/projects/project-sidebar.tsx +++ b/apps/senlinai-acro-react/src/pages/projects/project-sidebar.tsx @@ -1,34 +1,81 @@ import { Badge, Button, Divider, Layout, Space, Tag, Typography } from '@arco-design/web-react' -import { IconDashboard, IconMore, IconSettings } from '@arco-design/web-react/icon' +import { IconApps, IconDashboard, IconMore, IconSettings } from '@arco-design/web-react/icon' import { channels } from './project-data' -import type { ChannelKey, Project } from './project-types' +import type { ChannelKey, Project, WorkbenchView } from './project-types' const { Sider } = Layout const { Title, Text } = Typography -export function ProjectSidebar({ activeProject, activeChannel, onSelectChannel, onSelectItem }: { activeProject: Project; activeChannel: ChannelKey; onSelectChannel: (key: ChannelKey) => void; onSelectItem: (title: string) => void }) { +export function ProjectSidebar({ + activeView, + activeProject, + activeChannel, + onSelectChannel, + onSelectItem, +}: { + activeView: WorkbenchView + activeProject: Project + activeChannel: ChannelKey + onSelectChannel: (key: ChannelKey) => void + onSelectItem: (title: string) => void +}) { + const workspaceMode = activeView === 'workspace' + return ( -
{activeProject.name}
-
- {channels.map((channel) => ( - - ))} +
+ + {workspaceMode ? : } + {workspaceMode ? '工作台' : activeProject.name} + +
+ +
+ {workspaceMode ? ( + <> + + + + ) : ( + channels.map((channel) => ( + + )) + )} +
+
最近会话 {['需求评审要点整理', '生成 Q3 营销策略初稿', 'npx 安装 skill 至 codex', '产品定价模型讨论'].map((title, index) => ( - + ))}
+
标签 - {['全部', '需求', '设计', '研发', '运营', 'AI'].map((tag) => {tag})} + + {['全部', '需求', '设计', '研发', '运营', 'AI'].map((tag) => ( + {tag} + ))} +
) diff --git a/apps/senlinai-acro-react/src/pages/projects/project-types.ts b/apps/senlinai-acro-react/src/pages/projects/project-types.ts index 3951b49..e6b69bd 100644 --- a/apps/senlinai-acro-react/src/pages/projects/project-types.ts +++ b/apps/senlinai-acro-react/src/pages/projects/project-types.ts @@ -2,6 +2,7 @@ import type { ReactElement } from 'react' export type Screen = 'login' | 'workbench' export type Theme = 'light' | 'dark' +export type WorkbenchView = 'workspace' | 'project' export type ChannelKey = 'overview' | 'inbox' | 'tasks' | 'ai' | 'notes' | 'cron' | 'research' | 'design' export type Project = { diff --git a/apps/senlinai-acro-react/src/pages/workspace.tsx b/apps/senlinai-acro-react/src/pages/workspace.tsx new file mode 100644 index 0000000..d44dcbf --- /dev/null +++ b/apps/senlinai-acro-react/src/pages/workspace.tsx @@ -0,0 +1,91 @@ +import { Button, Card, Grid, Space, Tag, Typography } from '@arco-design/web-react' +import { + IconCheckCircleFill, + IconDashboard, + IconEmail, + IconFile, + IconRobot, + IconRight, + IconUserGroup, +} from '@arco-design/web-react/icon' +import { aiSessions, inbox, notes, projects, tasks } from './projects/project-data' +import type { Project } from './projects/project-types' + +const { Row, Col } = Grid +const { Title, Text } = Typography + +export function WorkspacePage({ onSelectProject }: { onSelectProject: (project: Project) => void }) { + 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: , color: 'arcoblue' }, + { title: '全局待处理', value: totalProjectBadges, delta: '跨项目聚合', icon: , color: 'red' }, + { title: '进行中任务', value: tasks.length, delta: '本周持续推进', icon: , color: 'green' }, + { title: 'AI 会话', value: aiSessions.length, delta: '队列空闲', icon: , color: 'purple' }, + { title: '知识资料', value: notes.length, delta: '最近 7 天更新', icon: , color: 'cyan' }, + ] + + return ( +
+
+
+ 所有项目的统一工作台,汇总项目动态、任务压力和知识资产。 + 工作台 +
+ + + + +
+ + + {workspaceMetrics.map((metric) => ( + + + + {metric.icon} +
+ {metric.title} + {metric.value} + {metric.delta} +
+
+
+ + ))} +
+ + +
+ 项目健康度 + +
+ {projects.map((project) => ( + + ))} +
+ + +
+ 跨项目待处理 + +
+ {inbox.slice(0, 4).map((item) => ( +
+ {item.title} + {item.meta} + {item.tag} + {item.owner} + +
+ ))} +
+
+ ) +}