Add workspace landing page

This commit is contained in:
2026-07-20 11:40:45 +08:00
parent 9c4ad986c0
commit da76abf2d0
10 changed files with 260 additions and 28 deletions

View File

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

View File

@@ -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<Screen>('login')
const [theme, setTheme] = useState<Theme>('light')
const [activeView, setActiveView] = useState<WorkbenchView>('workspace')
const [activeProject, setActiveProject] = useState(projects[0])
const [activeChannel, setActiveChannel] = useState<ChannelKey>('overview')
const [selectedItem, setSelectedItem] = useState('Inbox 待处理36')
@@ -23,11 +24,16 @@ function App() {
<LoginPage onLogin={() => setScreen('workbench')} />
) : (
<ProjectPage
activeView={activeView}
activeProject={activeProject}
activeChannel={activeChannel}
selectedItem={selectedItem}
theme={theme}
onSelectProject={setActiveProject}
onSelectWorkspace={() => setActiveView('workspace')}
onSelectProject={(project) => {
setActiveProject(project)
setActiveView('project')
}}
onSelectChannel={setActiveChannel}
onSelectItem={setSelectedItem}
onToggleTheme={() => setTheme(dark ? 'light' : 'dark')}

View File

@@ -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({
<ProjectTopbar theme={theme} onToggleTheme={onToggleTheme} />
<Layout className="workbench-main" hasSider>
<ProjectRail activeProject={activeProject} onSelectProject={onSelectProject} />
<ProjectRail
activeProject={activeProject}
activeView={activeView}
onSelectWorkspace={onSelectWorkspace}
onSelectProject={onSelectProject}
/>
<ProjectSidebar
activeView={activeView}
activeProject={activeProject}
activeChannel={activeChannel}
onSelectChannel={onSelectChannel}
@@ -43,10 +54,19 @@ export function ProjectPage({
/>
<Content className="stage">
<ProjectOverview onSelectItem={onSelectItem} />
{activeView === 'workspace' ? (
<WorkspacePage onSelectProject={onSelectProject} />
) : (
<ProjectOverview onSelectItem={onSelectItem} />
)}
</Content>
<ProjectInspector activeProject={activeProject} activeChannel={activeChannel} selectedItem={selectedItem} />
<ProjectInspector
activeView={activeView}
activeProject={activeProject}
activeChannel={activeChannel}
selectedItem={activeView === 'workspace' ? '工作台动态' : selectedItem}
/>
</Layout>
<ProjectStatusbar />

View File

@@ -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 (
<Sider className="inspector" width={360}>
<Tabs defaultActiveTab="discussion">
<Tabs.TabPane key="discussion" title="讨论"><InspectorDiscussion selectedItem={selectedItem} /></Tabs.TabPane>
<Tabs.TabPane key="props" title="属性"><div className="property-list"><Property label="所属项目" value={activeProject.name} /><Property label="频道" value={channels.find((c) => c.key === activeChannel)?.label ?? '概况'} /><Property label="创建人" value="张伟" /><Property label="更新时间" value="2026-07-20 10:30" /></div></Tabs.TabPane>
<Tabs.TabPane key="more" title="更多"><Space direction="vertical" className="more-actions"><Button long></Button><Button long></Button><Button long status="danger"></Button></Space></Tabs.TabPane>
<Tabs.TabPane key="discussion" title="讨论">
<InspectorDiscussion selectedItem={selectedItem} />
</Tabs.TabPane>
<Tabs.TabPane key="props" title="属性">
<div className="property-list">
<Property label="所属项目" value={workspaceMode ? '全部项目' : activeProject.name} />
<Property label="频道" value={workspaceMode ? '工作台总览' : channels.find((channel) => channel.key === activeChannel)?.label ?? '概况'} />
<Property label="创建人" value="张伟" />
<Property label="更新时间" value="2026-07-20 10:30" />
</div>
</Tabs.TabPane>
<Tabs.TabPane key="more" title="更多">
<Space direction="vertical" className="more-actions">
<Button long></Button>
<Button long></Button>
<Button long status="danger"></Button>
</Space>
</Tabs.TabPane>
</Tabs>
</Sider>
)

View File

@@ -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 (
<Sider className="project-rail" width={96}>
<Button className="dashboard-button" icon={<IconDashboard />} />
<Button
className={activeView === 'workspace' ? 'dashboard-button active' : 'dashboard-button'}
icon={<IconDashboard />}
onClick={onSelectWorkspace}
aria-label="工作台"
title="工作台"
/>
<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={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,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 (
<Sider className="channel-sidebar" width={248}>
<div className="project-title"><Space><IconDashboard /><Title heading={5}>{activeProject.name}</Title></Space><Button icon={<IconSettings />} size="mini" /></div>
<div className="channel-list">
{channels.map((channel) => (
<button key={channel.key} className={activeChannel === channel.key ? 'channel-button active' : 'channel-button'} onClick={() => onSelectChannel(channel.key)}>
<span className="channel-left">{channel.icon}<Text>{channel.label}</Text></span>
{channel.count !== undefined ? <Badge count={channel.count} maxCount={999} /> : <IconMore />}
</button>
))}
<div className="project-title">
<Space>
{workspaceMode ? <IconApps /> : <IconDashboard />}
<Title heading={5}>{workspaceMode ? '工作台' : activeProject.name}</Title>
</Space>
<Button icon={<IconSettings />} size="mini" />
</div>
<div className="channel-list">
{workspaceMode ? (
<>
<button className="channel-button active">
<span className="channel-left"><IconDashboard /><Text></Text></span>
<Badge count={4} />
</button>
<button className="channel-button">
<span className="channel-left"><IconApps /><Text></Text></span>
<IconMore />
</button>
</>
) : (
channels.map((channel) => (
<button
key={channel.key}
className={activeChannel === channel.key ? 'channel-button active' : 'channel-button'}
onClick={() => onSelectChannel(channel.key)}
>
<span className="channel-left">{channel.icon}<Text>{channel.label}</Text></span>
{channel.count !== undefined ? <Badge count={channel.count} maxCount={999} /> : <IconMore />}
</button>
))
)}
</div>
<Divider />
<section className="recent-sessions">
<Text className="section-title"></Text>
{['需求评审要点整理', '生成 Q3 营销策略初稿', 'npx 安装 skill 至 codex', '产品定价模型讨论'].map((title, index) => (
<button key={title} onClick={() => onSelectItem(title)}><Text>{title}</Text><Text type="secondary">{index === 0 ? '09:15' : index === 1 ? '08:47' : '昨天'}</Text></button>
<button key={title} onClick={() => onSelectItem(title)}>
<Text>{title}</Text>
<Text type="secondary">{index === 0 ? '09:15' : index === 1 ? '08:47' : '昨天'}</Text>
</button>
))}
</section>
<Divider />
<section className="tag-cloud">
<Text className="section-title"></Text>
<Space wrap>{['全部', '需求', '设计', '研发', '运营', 'AI'].map((tag) => <Tag key={tag} color={tag === '全部' ? 'arcoblue' : 'gray'}>{tag}</Tag>)}</Space>
<Space wrap>
{['全部', '需求', '设计', '研发', '运营', 'AI'].map((tag) => (
<Tag key={tag} color={tag === '全部' ? 'arcoblue' : 'gray'}>{tag}</Tag>
))}
</Space>
</section>
</Sider>
)

View File

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

View File

@@ -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: <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>
<Text type="secondary"></Text>
<Title heading={4}></Title>
</div>
<Space>
<Button icon={<IconUserGroup />}></Button>
<Button type="primary" icon={<IconRight />}></Button>
</Space>
</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>
)
}