Connect React workspace to backend data

This commit is contained in:
2026-07-20 13:02:26 +08:00
parent 215268e836
commit 875b326ddc
28 changed files with 1017 additions and 257 deletions

View File

@@ -5,8 +5,10 @@ import { IconBook, IconCheckCircleFill, IconLaunch, IconSafe, IconUserGroup } fr
const { Title, Text, Paragraph } = Typography
export function LoginPage({ onLogin }: { onLogin: () => void }) {
const [server, setServer] = useState('https://10.0.0.15:8080')
export function LoginPage({ onLogin }: { onLogin: (input: { server: string; email: string; password: string }) => void }) {
const [server, setServer] = useState('http://127.0.0.1:8080')
const [email, setEmail] = useState('demo@senlin.ai')
const [password, setPassword] = useState('password123')
return (
<section className="login-screen">
@@ -34,10 +36,10 @@ export function LoginPage({ onLogin }: { onLogin: () => void }) {
<Input value={server} onChange={setServer} placeholder="例如https://10.0.0.15:8080" suffix={<IconLaunch />} />
</Form.Item>
<Form.Item label="账号">
<Input placeholder="请输入用户名或邮箱" />
<Input value={email} onChange={setEmail} placeholder="请输入用户名或邮箱" />
</Form.Item>
<Form.Item label="密码">
<Input.Password placeholder="请输入密码" />
<Input.Password value={password} onChange={setPassword} placeholder="请输入密码" />
</Form.Item>
<div className="login-row">
<Space size={8}>
@@ -46,7 +48,7 @@ export function LoginPage({ onLogin }: { onLogin: () => void }) {
</Space>
<Button type="text" size="mini">?</Button>
</div>
<Button type="primary" long size="large" onClick={onLogin}>
<Button type="primary" long size="large" onClick={() => onLogin({ server, email, password })}>
</Button>
<div className="connection-card">

View File

@@ -5,13 +5,14 @@ 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, WorkbenchView } from './projects/project-types'
import type { ChannelKey, Project, ProjectWorkspace, Theme, WorkbenchView } from './projects/project-types'
const { Content } = Layout
export function ProjectPage({
activeView,
activeProject,
activeWorkspace,
workspaces,
activeChannel,
theme,
onSelectProject,
@@ -21,7 +22,8 @@ export function ProjectPage({
onToggleTheme,
}: {
activeView: WorkbenchView
activeProject: Project
activeWorkspace: ProjectWorkspace
workspaces: ProjectWorkspace[]
activeChannel: ChannelKey
theme: Theme
onSelectProject: (project: Project) => void
@@ -31,6 +33,7 @@ export function ProjectPage({
onToggleTheme: () => void
}) {
const isWorkspace = activeView === 'workspace'
const projects = workspaces.map((workspace) => workspace.project)
return (
<Layout className="workbench-shell">
@@ -38,7 +41,8 @@ export function ProjectPage({
<Layout className="workbench-main" hasSider>
<ProjectRail
activeProject={activeProject}
activeProject={activeWorkspace.project}
projects={projects}
activeView={activeView}
onSelectWorkspace={onSelectWorkspace}
onSelectProject={onSelectProject}
@@ -47,7 +51,7 @@ export function ProjectPage({
{!isWorkspace && (
<ProjectSidebar
activeView={activeView}
activeProject={activeProject}
activeWorkspace={activeWorkspace}
activeChannel={activeChannel}
onSelectChannel={onSelectChannel}
onSelectItem={onSelectItem}
@@ -56,11 +60,11 @@ export function ProjectPage({
<Content className="stage">
{isWorkspace ? (
<WorkspacePage onSelectProject={onSelectProject} />
<WorkspacePage workspaces={workspaces} onSelectProject={onSelectProject} />
) : (
<ProjectChannelPage
activeChannel={activeChannel}
activeProject={activeProject}
activeWorkspace={activeWorkspace}
onSelectItem={onSelectItem}
/>
)}

View File

@@ -1,7 +1,6 @@
import { Avatar, Button, Card, Input, Space, Tag, Typography } from '@arco-design/web-react'
import { IconPlus, IconRobot, IconSend } from '@arco-design/web-react/icon'
import { aiSessions } from './project-data'
import type { Project } from './project-types'
import type { ProjectWorkspace } from './project-types'
const { Title, Text, Paragraph } = Typography
@@ -11,13 +10,16 @@ const messages = [
{ role: 'user', text: '把建议拆成可执行任务。' },
]
export function ProjectAi({ activeProject, onSelectItem }: { activeProject: Project; onSelectItem: (title: string) => void }) {
export function ProjectAi({ activeWorkspace, onSelectItem }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void }) {
const { aiSessions, project } = activeWorkspace
const selected = aiSessions[0]
return (
<div className="project-channel-page project-ai-page overview-page">
<div className="overview-head">
<div>
<Title heading={4}>AI </Title>
<Text type="secondary">{activeProject.name} AI session</Text>
<Text type="secondary">{project.name} AI session</Text>
</div>
<Button type="primary" icon={<IconPlus />}></Button>
</div>
@@ -43,7 +45,7 @@ export function ProjectAi({ activeProject, onSelectItem }: { activeProject: Proj
<Card className="ai-chat-panel queue-section" bordered>
<div className="section-header">
<Title heading={6}>{aiSessions[0].title}</Title>
<Title heading={6}>{selected?.title ?? '暂无会话'}</Title>
<Tag color="green"></Tag>
</div>
<div className="chat-thread">

View File

@@ -4,32 +4,30 @@ import { ProjectInbox } from './project-inbox'
import { ProjectNotes } from './project-notes'
import { ProjectOverview } from './project-overview'
import { ProjectTasks } from './project-tasks'
import type { ChannelKey, Project } from './project-types'
import type { ChannelKey, ProjectWorkspace } from './project-types'
export function ProjectChannelPage({
activeChannel,
activeProject,
activeWorkspace,
onSelectItem,
}: {
activeChannel: ChannelKey
activeProject: Project
activeWorkspace: ProjectWorkspace
onSelectItem: (title: string) => void
}) {
switch (activeChannel) {
case 'inbox':
return <ProjectInbox activeProject={activeProject} onSelectItem={onSelectItem} />
return <ProjectInbox activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
case 'tasks':
return <ProjectTasks activeProject={activeProject} onSelectItem={onSelectItem} />
return <ProjectTasks activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
case 'ai':
return <ProjectAi activeProject={activeProject} onSelectItem={onSelectItem} />
return <ProjectAi activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
case 'notes':
return <ProjectNotes activeProject={activeProject} onSelectItem={onSelectItem} />
return <ProjectNotes activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
case 'cron':
return <ProjectCron activeProject={activeProject} onSelectItem={onSelectItem} />
return <ProjectCron activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
case 'overview':
case 'research':
case 'design':
default:
return <ProjectOverview onSelectItem={onSelectItem} />
return <ProjectOverview activeWorkspace={activeWorkspace} onSelectItem={onSelectItem} />
}
}

View File

@@ -1,23 +1,20 @@
import { Button, Card, Space, Switch, Tag, Typography } from '@arco-design/web-react'
import { IconClockCircle, IconPlus, IconThunderbolt } from '@arco-design/web-react/icon'
import type { Project } from './project-types'
import type { ProjectWorkspace } from './project-types'
const { Title, Text } = Typography
const cronJobs = [
{ name: '每日竞品价格抓取', expr: '0 8 * * *', status: '运行中', lastRun: '今天 08:00', nextRun: '明天 08:00', owner: '李明' },
{ name: '周报摘要生成', expr: '0 18 * * 5', status: '运行中', lastRun: '上周五 18:00', nextRun: '本周五 18:00', owner: '张伟' },
{ name: '过期任务提醒', expr: '*/30 9-18 * * 1-5', status: '暂停', lastRun: '昨天 17:30', nextRun: '暂停中', owner: '系统' },
{ name: '资料索引刷新', expr: '0 */4 * * *', status: '运行中', lastRun: '今天 12:00', nextRun: '今天 16:00', owner: '系统' },
]
export function ProjectCron({ activeWorkspace, onSelectItem }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void }) {
const { cronJobs, project } = activeWorkspace
const enabledCount = cronJobs.filter((job) => job.enabled).length
const pausedCount = cronJobs.length - enabledCount
export function ProjectCron({ activeProject, onSelectItem }: { activeProject: Project; onSelectItem: (title: string) => void }) {
return (
<div className="project-channel-page project-cron-page overview-page">
<div className="overview-head">
<div>
<Title heading={4}>Cron </Title>
<Text type="secondary">{activeProject.name} AI </Text>
<Text type="secondary">{project.name} AI </Text>
</div>
<Button type="primary" icon={<IconPlus />}></Button>
</div>
@@ -26,8 +23,8 @@ export function ProjectCron({ activeProject, onSelectItem }: { activeProject: Pr
<div className="section-header">
<Title heading={6}></Title>
<Space>
<Tag color="green">3 </Tag>
<Tag color="gray">1 </Tag>
<Tag color="green">{enabledCount} </Tag>
<Tag color="gray">{pausedCount} </Tag>
</Space>
</div>
{cronJobs.map((job) => (
@@ -42,12 +39,12 @@ export function ProjectCron({ activeProject, onSelectItem }: { activeProject: Pr
}}
>
<span className="row-title"><IconClockCircle /> {job.name}</span>
<Tag color={job.status === '运行中' ? 'green' : 'gray'}>{job.status}</Tag>
<Tag color={job.enabled ? 'green' : 'gray'}>{job.status}</Tag>
<span>{job.expr}</span>
<span>{job.lastRun}</span>
<span>{job.nextRun}</span>
<span>{job.owner}</span>
<Switch size="small" checked={job.status === '运行中'} />
<Switch size="small" checked={job.enabled} />
</div>
))}
</Card>

View File

@@ -1,53 +0,0 @@
import { IconClockCircle, IconEmail, IconFile, IconHome, IconLink, IconList, IconRobot, IconUserGroup } from '@arco-design/web-react/icon'
import type { Project, ProjectChannel } from './project-types'
export const projects: Project[] = [
{ id: 'a1', name: '项目 A1', short: 'A1', badge: 635, urgent: true, color: '#165DFF' },
{ id: 'a2', name: '项目 A2', short: 'PM', badge: 36, color: '#00B42A' },
{ id: 'b1', name: '项目 B1', short: 'DS', badge: 4, urgent: true, color: '#722ED1' },
{ id: 'c3', name: '项目 C3', short: 'OP', badge: 45, color: '#FF7D00' },
]
export const channels: ProjectChannel[] = [
{ key: 'overview', label: '概况', count: 635, icon: <IconHome /> },
{ key: 'inbox', label: 'Inbox 消息流', count: 36, icon: <IconEmail /> },
{ key: 'tasks', label: '工作计划', count: 12, icon: <IconList /> },
{ key: 'ai', label: 'AI 会话', count: 4, icon: <IconRobot /> },
{ key: 'notes', label: '笔记资料', count: 343, icon: <IconFile /> },
{ key: 'cron', label: 'Cron 计划任务', count: 45, icon: <IconClockCircle /> },
{ key: 'research', label: '客户研究频道', icon: <IconUserGroup /> },
{ key: 'design', label: '产品设计频道', icon: <IconLink /> },
]
export const inbox = [
{ title: '竞争对手定价更新监控', meta: '来源:产品研究 · 竞品追踪', owner: '李明', time: '2 小时前', tag: '市场情报' },
{ title: '客户反馈:导出功能报错', meta: '来源:客户支持 · 工单 #CS-1287', owner: '王芳', time: '4 小时前', tag: '客户反馈' },
{ title: 'API 调用策略评估建议', meta: '来源:后端架构讨论', owner: '张伟', time: '6 小时前', tag: '架构' },
{ title: '更新需求文档 v1.3.2', meta: '来源:产品需求 · 文档评审', owner: '陈晨', time: '昨天', tag: '需求文档' },
]
export const tasks = [
{ title: '智能报表导出功能', owner: '张伟', progress: '60%', due: '2026-07-24', tag: '进行中' },
{ title: '企业版权限体系梳理', owner: '李明', progress: '30%', due: '2026-07-28', tag: '进行中' },
{ title: 'Q3 营销策略制定', owner: '王芳', progress: '45%', due: '2026-07-31', tag: '进行中' },
]
export const aiSessions = [
{ title: '生成 Q3 营销策略初稿', summary: '基于市场分析和竞品数据,输出 Q3 营销策略重点...', owner: '张伟', time: '今天 08:47' },
{ title: '产品定价模型讨论', summary: '针对企业版与标准版定价模型进行对比分析...', owner: '李明', time: '昨天 16:05' },
{ title: '客户流失原因分析', summary: '分析近三个月客户流失数据与反馈原因...', owner: '王芳', time: '昨天 10:22' },
{ title: '行业趋势与机会洞察', summary: '围绕 AI 在企业协同领域的趋势与机会...', owner: '张伟', time: '7 月 18 日' },
]
export const notes = [
{ title: '用户权限体系设计文档 v2.1', type: '文档', updated: '2026-07-19', owner: '李明' },
{ title: '导出功能技术方案评审', type: '文档', updated: '2026-07-18', owner: '张伟' },
{ title: '2026 Q3 竞品功能对比表', type: '表格', updated: '2026-07-18', owner: '王芳' },
]
export const discussions = [
{ name: '李明', time: '2026-07-20 09:12', text: '关于导出报错的问题,已在测试环境复现,初步定位为权限校验逻辑异常。' },
{ name: '张伟', time: '2026-07-20 09:18', text: '@李明 麻烦同步修复预计完成时间,我这边需要同步给客户。' },
{ name: '王芳', time: '2026-07-20 10:05', text: '补充:客户反馈影响范围主要集中在企业版用户。' },
{ name: '系统通知', time: '2026-07-20 10:30', text: '已将此讨论关联到任务:智能报表导出功能。' },
]

View File

@@ -1,7 +1,6 @@
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 { inbox } from './project-data'
import type { Project } from './project-types'
import type { ProjectWorkspace } from './project-types'
const { Row, Col } = Grid
const { Title, Text, Paragraph } = Typography
@@ -12,7 +11,8 @@ const folders = [
{ label: '已归档', count: 42, color: 'gray' },
]
export function ProjectInbox({ activeProject, onSelectItem }: { activeProject: Project; onSelectItem: (title: string) => void }) {
export function ProjectInbox({ activeWorkspace, onSelectItem }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void }) {
const { inbox, project } = activeWorkspace
const selected = inbox[0]
return (
@@ -20,7 +20,7 @@ export function ProjectInbox({ activeProject, onSelectItem }: { activeProject: P
<div className="overview-head">
<div>
<Title heading={4}>Inbox </Title>
<Text type="secondary">{activeProject.name} 线</Text>
<Text type="secondary">{project.name} 线</Text>
</div>
<Space>
<Button icon={<IconRefresh />}></Button>
@@ -65,13 +65,13 @@ export function ProjectInbox({ activeProject, onSelectItem }: { activeProject: P
<Card className="mail-detail queue-section" bordered>
<div className="section-header">
<Title heading={6}>{selected.title}</Title>
<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.owner}</Text>
<Text type="secondary">{selected ? `${selected.meta} · ${selected.owner}` : '真实接口暂无消息'}</Text>
<Paragraph>
</Paragraph>

View File

@@ -1,97 +0,0 @@
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, WorkbenchView } from './project-types'
const { Sider } = Layout
const { Title, Text, Paragraph } = Typography
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={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>
)
}
function InspectorDiscussion({ selectedItem }: { selectedItem: string }) {
return (
<div className="discussion-panel">
<div className="inspector-title">
<Title heading={5}>{selectedItem}</Title>
<Button size="mini" icon={<IconMore />} />
</div>
<Text type="secondary"></Text>
<Divider />
<Space direction="vertical" size={18} className="discussion-list">
{discussions.map((item) => (
<div className="discussion-item" key={`${item.name}-${item.time}`}>
<Avatar size={30}>{item.name.slice(0, 1)}</Avatar>
<div>
<Space>
<Text className="discussion-name">{item.name}</Text>
<Text type="secondary">{item.time}</Text>
</Space>
<Paragraph>{item.text}</Paragraph>
<Button type="text" size="mini"></Button>
</div>
</div>
))}
</Space>
<div className="comment-box">
<Input.TextArea placeholder="写下你的评论Enter 发送,⌘ + Enter 换行" autoSize={{ minRows: 3, maxRows: 4 }} />
<div className="comment-actions">
<Space>
<Button type="text" icon={<IconAttachmentFallback />} />
<Button type="text" icon={<IconMessage />} />
</Space>
<Button type="primary" onClick={() => Message.success('评论已发送(原型)')}></Button>
</div>
</div>
</div>
)
}
function IconAttachmentFallback() {
return <IconLink />
}
function Property({ label, value }: { label: string; value: string }) {
return (
<div className="property-row">
<Text type="secondary">{label}</Text>
<Text>{value}</Text>
</div>
)
}

View File

@@ -1,19 +1,20 @@
import { Button, Card, Space, Tag, Typography } from '@arco-design/web-react'
import { IconFile, IconFolder, IconPlus, IconSearch } from '@arco-design/web-react/icon'
import { notes } from './project-data'
import type { Project } from './project-types'
import type { ProjectWorkspace } from './project-types'
const { Title, Text } = Typography
const folders = ['需求文档', '技术方案', '会议纪要', '竞品资料']
export function ProjectNotes({ activeProject, onSelectItem }: { activeProject: Project; onSelectItem: (title: string) => void }) {
export function ProjectNotes({ activeWorkspace, onSelectItem }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void }) {
const { notes, project } = activeWorkspace
return (
<div className="project-channel-page project-notes-page overview-page">
<div className="overview-head">
<div>
<Title heading={4}></Title>
<Text type="secondary">{activeProject.name} 线</Text>
<Text type="secondary">{project.name} 线</Text>
</div>
<Space>
<Button icon={<IconSearch />}></Button>

View File

@@ -1,19 +1,20 @@
import { useMemo } from 'react'
import { Button, Card, Grid, Space, Tag, Typography } from '@arco-design/web-react'
import { IconCalendar, IconCheckCircleFill, IconClockCircle, IconEmail, IconFile, IconMore, IconRobot } from '@arco-design/web-react/icon'
import { aiSessions, inbox, notes, tasks } from './project-data'
import type { InboxItem, ProjectWorkspace } from './project-types'
const { Row, Col } = Grid
const { Title, Text } = Typography
export function ProjectOverview({ onSelectItem }: { onSelectItem: (title: string) => void }) {
export function ProjectOverview({ activeWorkspace, onSelectItem }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void }) {
const { inbox, tasks, aiSessions, notes, cronJobs } = activeWorkspace
const metrics = useMemo(() => [
{ title: 'Inbox 待处理', value: 36, delta: '较昨日 -8', icon: <IconEmail />, color: 'arcoblue' },
{ title: '进行中的任务', value: 12, delta: '较昨日 -2', icon: <IconCheckCircleFill />, color: 'green' },
{ title: 'AI 会话活跃', value: 4, delta: '较昨日 +1', icon: <IconRobot />, color: 'purple' },
{ title: '笔记资料总数', value: 343, delta: '较昨日 +5', icon: <IconFile />, color: 'cyan' },
{ title: '计划任务', value: 45, delta: '较昨日 +0', icon: <IconClockCircle />, color: 'orange' },
], [])
{ title: 'Inbox 待处理', value: inbox.length, delta: '实时接口', icon: <IconEmail />, color: 'arcoblue' },
{ title: '进行中的任务', value: tasks.filter((task) => !task.completed).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' },
{ title: '计划任务', value: cronJobs.length, delta: '实时接口', icon: <IconClockCircle />, color: 'orange' },
], [aiSessions.length, cronJobs.length, inbox.length, notes.length, tasks])
return (
<div className="overview-page">
@@ -44,15 +45,15 @@ export function ProjectOverview({ onSelectItem }: { onSelectItem: (title: string
))}
</Row>
<QueueSection title="Inbox 待处理(36" items={inbox} onSelectItem={onSelectItem} />
<TaskSection onSelectItem={onSelectItem} />
<AISessionSection onSelectItem={onSelectItem} />
<NoteSection onSelectItem={onSelectItem} />
<QueueSection title={`Inbox 待处理(${inbox.length}`} items={inbox} onSelectItem={onSelectItem} />
<TaskSection tasks={tasks} onSelectItem={onSelectItem} />
<AISessionSection aiSessions={aiSessions} onSelectItem={onSelectItem} />
<NoteSection notes={notes} onSelectItem={onSelectItem} />
</div>
)
}
function QueueSection({ title, items, onSelectItem }: { title: string; items: typeof inbox; onSelectItem: (title: string) => void }) {
function QueueSection({ title, items, onSelectItem }: { title: string; items: InboxItem[]; onSelectItem: (title: string) => void }) {
return (
<Card className="queue-section" bordered>
<div className="section-header">
@@ -72,11 +73,11 @@ function QueueSection({ title, items, onSelectItem }: { title: string; items: ty
)
}
function TaskSection({ onSelectItem }: { onSelectItem: (title: string) => void }) {
function TaskSection({ tasks, onSelectItem }: { tasks: ProjectWorkspace['tasks']; onSelectItem: (title: string) => void }) {
return (
<Card className="queue-section" bordered>
<div className="section-header">
<Title heading={6}>12</Title>
<Title heading={6}>{tasks.length}</Title>
<Button type="text" size="mini"></Button>
</div>
{tasks.map((task) => (
@@ -92,11 +93,11 @@ function TaskSection({ onSelectItem }: { onSelectItem: (title: string) => void }
)
}
function AISessionSection({ onSelectItem }: { onSelectItem: (title: string) => void }) {
function AISessionSection({ aiSessions, onSelectItem }: { aiSessions: ProjectWorkspace['aiSessions']; onSelectItem: (title: string) => void }) {
return (
<Card className="queue-section" bordered>
<div className="section-header">
<Title heading={6}>AI 4</Title>
<Title heading={6}>AI {aiSessions.length}</Title>
<Button type="text" size="mini"></Button>
</div>
{aiSessions.map((session) => (
@@ -111,7 +112,7 @@ function AISessionSection({ onSelectItem }: { onSelectItem: (title: string) => v
)
}
function NoteSection({ onSelectItem }: { onSelectItem: (title: string) => void }) {
function NoteSection({ notes, onSelectItem }: { notes: ProjectWorkspace['notes']; onSelectItem: (title: string) => void }) {
return (
<Card className="queue-section" bordered>
<div className="section-header">

View File

@@ -1,18 +1,19 @@
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, WorkbenchView } from './project-types'
const { Sider } = Layout
export function ProjectRail({
activeProject,
projects,
activeView,
onSelectWorkspace,
onSelectProject,
}: {
activeProject: Project
projects: Project[]
activeView: WorkbenchView
onSelectWorkspace: () => void
onSelectProject: (project: Project) => void

View File

@@ -1,32 +1,32 @@
import { Badge, Button, Divider, Layout, Space, Tag, Typography } from '@arco-design/web-react'
import { IconApps, IconDashboard, IconMore, IconSettings } from '@arco-design/web-react/icon'
import { channels } from './project-data'
import type { ChannelKey, Project, WorkbenchView } from './project-types'
import type { ChannelKey, ProjectWorkspace, WorkbenchView } from './project-types'
const { Sider } = Layout
const { Title, Text } = Typography
export function ProjectSidebar({
activeView,
activeProject,
activeWorkspace,
activeChannel,
onSelectChannel,
onSelectItem,
}: {
activeView: WorkbenchView
activeProject: Project
activeWorkspace: ProjectWorkspace
activeChannel: ChannelKey
onSelectChannel: (key: ChannelKey) => void
onSelectItem: (title: string) => void
}) {
const workspaceMode = activeView === 'workspace'
const channels = activeWorkspace.channels
return (
<Sider className="channel-sidebar" width={248}>
<div className="project-title">
<Space>
{workspaceMode ? <IconApps /> : <IconDashboard />}
<Title heading={5}>{workspaceMode ? '工作台' : activeProject.name}</Title>
<Title heading={5}>{workspaceMode ? '工作台' : activeWorkspace.project.name}</Title>
</Space>
<Button icon={<IconSettings />} size="mini" />
</div>
@@ -36,7 +36,7 @@ export function ProjectSidebar({
<>
<button className="channel-button active">
<span className="channel-left"><IconDashboard /><Text></Text></span>
<Badge count={4} />
<Badge count={channels.length} />
</button>
<button className="channel-button">
<span className="channel-left"><IconApps /><Text></Text></span>
@@ -46,7 +46,7 @@ export function ProjectSidebar({
) : (
channels.map((channel) => (
<button
key={channel.key}
key={channel.id}
className={activeChannel === channel.key ? 'channel-button active' : 'channel-button'}
onClick={() => onSelectChannel(channel.key)}
>
@@ -61,7 +61,7 @@ export function ProjectSidebar({
<section className="tag-cloud">
<Text className="section-title"></Text>
<Space wrap>
{['全部', '需求', '设计', '研发', '运营', 'AI'].map((tag) => (
{activeWorkspace.tags.map((tag) => (
<Tag key={tag} color={tag === '全部' ? 'arcoblue' : 'gray'}>{tag}</Tag>
))}
</Space>
@@ -70,10 +70,10 @@ export function ProjectSidebar({
<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>
{activeWorkspace.recentSessions.slice(0, 4).map((session) => (
<button key={session.id} onClick={() => onSelectItem(session.title)}>
<Text>{session.title}</Text>
<Text type="secondary">{session.time}</Text>
</button>
))}
</section>

View File

@@ -1,23 +1,18 @@
import { Button, Card, Progress, Space, Tag, Typography } from '@arco-design/web-react'
import { IconCalendar, IconCheckCircle, IconPlus, IconUser } from '@arco-design/web-react/icon'
import { tasks } from './project-data'
import type { Project } from './project-types'
import type { ProjectWorkspace, TaskItem } from './project-types'
const { Title, Text } = Typography
const lanes = [
{ title: '待开始', color: 'gray', items: ['权限边界确认', '客户案例素材整理'] },
{ title: '进行中', color: 'arcoblue', items: tasks.map((task) => task.title) },
{ title: '已完成', color: 'green', items: ['项目模板初始化', '会议纪要归档'] },
]
export function ProjectTasks({ activeProject, onSelectItem }: { activeProject: Project; onSelectItem: (title: string) => void }) {
export function ProjectTasks({ activeWorkspace, onSelectItem }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void }) {
const { tasks, project } = activeWorkspace
const lanes = makeLanes(tasks)
return (
<div className="project-channel-page project-tasks-page overview-page">
<div className="overview-head">
<div>
<Title heading={4}></Title>
<Text type="secondary">{activeProject.name} TODO </Text>
<Text type="secondary">{project.name} TODO </Text>
</div>
<Button type="primary" icon={<IconPlus />}></Button>
</div>
@@ -59,3 +54,13 @@ export function ProjectTasks({ activeProject, onSelectItem }: { activeProject: P
</div>
)
}
function makeLanes(tasks: TaskItem[]) {
const done = tasks.filter((task) => task.completed)
const active = tasks.filter((task) => !task.completed)
return [
{ title: '待开始', color: 'gray', items: active.slice(0, 1).map((task) => task.title) },
{ title: '进行中', color: 'arcoblue', items: active.map((task) => task.title) },
{ title: '已完成', color: 'green', items: done.length ? done.map((task) => task.title) : ['暂无已完成任务'] },
]
}

View File

@@ -3,7 +3,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 ChannelKey = 'overview' | 'inbox' | 'tasks' | 'ai' | 'notes' | 'cron' | `custom:${string}`
export type Project = {
id: string
@@ -16,7 +16,72 @@ export type Project = {
export type ProjectChannel = {
key: ChannelKey
id: string
label: string
count?: number
icon: ReactElement
url?: string
sortOrder: number
}
export type InboxItem = {
id: string
title: string
meta: string
owner: string
time: string
tag: string
summary: string
status: string
}
export type TaskItem = {
id: string
title: string
owner: string
progress: string
due: string
tag: string
summary: string
completed: boolean
}
export type AISession = {
id: string
title: string
summary: string
owner: string
time: string
}
export type NoteSource = {
id: string
title: string
type: string
updated: string
owner: string
source: string
}
export type CronJob = {
id: string
name: string
expr: string
status: string
lastRun: string
nextRun: string
owner: string
enabled: boolean
}
export type ProjectWorkspace = {
project: Project
channels: ProjectChannel[]
tags: string[]
recentSessions: AISession[]
inbox: InboxItem[]
tasks: TaskItem[]
aiSessions: AISession[]
notes: NoteSource[]
cronJobs: CronJob[]
}

View File

@@ -6,13 +6,17 @@ import {
IconFile,
IconRobot,
} from '@arco-design/web-react/icon'
import { aiSessions, inbox, notes, projects, tasks } from './projects/project-data'
import type { Project } from './projects/project-types'
import type { Project, ProjectWorkspace } from './projects/project-types'
const { Row, Col } = Grid
const { Title, Text } = Typography
export function WorkspacePage({ onSelectProject }: { onSelectProject: (project: Project) => void }) {
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 = [