Add project channel pages
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Layout } from '@arco-design/web-react'
|
||||
import { WorkspacePage } from './workspace'
|
||||
import { ProjectOverview } from './projects/project-overview'
|
||||
import { ProjectChannelPage } from './projects/project-channel-page'
|
||||
import { ProjectInspector } from './projects/project-inspector'
|
||||
import { ProjectRail } from './projects/project-rail'
|
||||
import { ProjectSidebar } from './projects/project-sidebar'
|
||||
@@ -61,7 +61,11 @@ export function ProjectPage({
|
||||
{isWorkspace ? (
|
||||
<WorkspacePage onSelectProject={onSelectProject} />
|
||||
) : (
|
||||
<ProjectOverview onSelectItem={onSelectItem} />
|
||||
<ProjectChannelPage
|
||||
activeChannel={activeChannel}
|
||||
activeProject={activeProject}
|
||||
onSelectItem={onSelectItem}
|
||||
/>
|
||||
)}
|
||||
</Content>
|
||||
|
||||
|
||||
68
apps/senlinai-acro-react/src/pages/projects/project-ai.tsx
Normal file
68
apps/senlinai-acro-react/src/pages/projects/project-ai.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
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'
|
||||
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
|
||||
const messages = [
|
||||
{ role: 'user', text: '请基于当前项目资料,整理本周风险和推进建议。' },
|
||||
{ role: 'assistant', text: '我建议优先处理导出问题、权限边界和 Q3 策略评审,并把客户反馈同步到工作计划。' },
|
||||
{ role: 'user', text: '把建议拆成可执行任务。' },
|
||||
]
|
||||
|
||||
export function ProjectAi({ activeProject, onSelectItem }: { activeProject: Project; onSelectItem: (title: string) => void }) {
|
||||
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>
|
||||
</div>
|
||||
<Button type="primary" icon={<IconPlus />}>新建会话</Button>
|
||||
</div>
|
||||
|
||||
<div className="ai-workspace">
|
||||
<Card className="ai-session-list queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>Session</Title>
|
||||
<Tag color="purple">{aiSessions.length}</Tag>
|
||||
</div>
|
||||
{aiSessions.map((session, index) => (
|
||||
<button
|
||||
className={index === 0 ? 'ai-session active' : 'ai-session'}
|
||||
key={session.title}
|
||||
onClick={() => onSelectItem(session.title)}
|
||||
>
|
||||
<Text>{session.title}</Text>
|
||||
<Text type="secondary">{session.summary}</Text>
|
||||
<time>{session.time}</time>
|
||||
</button>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
<Card className="ai-chat-panel queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>{aiSessions[0].title}</Title>
|
||||
<Tag color="green">队列空闲</Tag>
|
||||
</div>
|
||||
<div className="chat-thread">
|
||||
{messages.map((message, index) => (
|
||||
<div className={`chat-message ${message.role}`} key={`${message.role}-${index}`}>
|
||||
<Avatar size={30}>{message.role === 'assistant' ? <IconRobot /> : '张'}</Avatar>
|
||||
<Paragraph>{message.text}</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="chat-input">
|
||||
<Input.TextArea placeholder="输入你的问题,结合项目上下文继续追问" autoSize={{ minRows: 3, maxRows: 4 }} />
|
||||
<Space>
|
||||
<Button>引用资料</Button>
|
||||
<Button type="primary" icon={<IconSend />}>发送</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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'
|
||||
import type { ChannelKey, Project } from './project-types'
|
||||
|
||||
export function ProjectChannelPage({
|
||||
activeChannel,
|
||||
activeProject,
|
||||
onSelectItem,
|
||||
}: {
|
||||
activeChannel: ChannelKey
|
||||
activeProject: Project
|
||||
onSelectItem: (title: string) => void
|
||||
}) {
|
||||
switch (activeChannel) {
|
||||
case 'inbox':
|
||||
return <ProjectInbox activeProject={activeProject} onSelectItem={onSelectItem} />
|
||||
case 'tasks':
|
||||
return <ProjectTasks activeProject={activeProject} onSelectItem={onSelectItem} />
|
||||
case 'ai':
|
||||
return <ProjectAi activeProject={activeProject} onSelectItem={onSelectItem} />
|
||||
case 'notes':
|
||||
return <ProjectNotes activeProject={activeProject} onSelectItem={onSelectItem} />
|
||||
case 'cron':
|
||||
return <ProjectCron activeProject={activeProject} onSelectItem={onSelectItem} />
|
||||
case 'overview':
|
||||
case 'research':
|
||||
case 'design':
|
||||
default:
|
||||
return <ProjectOverview onSelectItem={onSelectItem} />
|
||||
}
|
||||
}
|
||||
63
apps/senlinai-acro-react/src/pages/projects/project-cron.tsx
Normal file
63
apps/senlinai-acro-react/src/pages/projects/project-cron.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
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'
|
||||
|
||||
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({ 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>
|
||||
</div>
|
||||
<Button type="primary" icon={<IconPlus />}>新建计划</Button>
|
||||
</div>
|
||||
|
||||
<Card className="queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>任务列表</Title>
|
||||
<Space>
|
||||
<Tag color="green">3 运行中</Tag>
|
||||
<Tag color="gray">1 暂停</Tag>
|
||||
</Space>
|
||||
</div>
|
||||
{cronJobs.map((job) => (
|
||||
<div
|
||||
className="data-row cron-row"
|
||||
key={job.name}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onSelectItem(job.name)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') onSelectItem(job.name)
|
||||
}}
|
||||
>
|
||||
<span className="row-title"><IconClockCircle /> {job.name}</span>
|
||||
<Tag color={job.status === '运行中' ? '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 === '运行中'} />
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
<Card className="queue-section cron-summary" bordered>
|
||||
<Space>
|
||||
<Tag color="arcoblue"><IconThunderbolt /></Tag>
|
||||
<Text>最近一次自动任务完成于今天 12:00,资料索引刷新成功,无异常重试。</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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'
|
||||
|
||||
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({ activeProject, onSelectItem }: { activeProject: Project; onSelectItem: (title: string) => void }) {
|
||||
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">{activeProject.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.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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
const folders = ['需求文档', '技术方案', '会议纪要', '竞品资料']
|
||||
|
||||
export function ProjectNotes({ activeProject, onSelectItem }: { activeProject: Project; onSelectItem: (title: string) => void }) {
|
||||
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>
|
||||
</div>
|
||||
<Space>
|
||||
<Button icon={<IconSearch />}>搜索资料</Button>
|
||||
<Button type="primary" icon={<IconPlus />}>上传/新建</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className="folder-grid">
|
||||
{folders.map((folder) => (
|
||||
<Card className="folder-card" bordered key={folder}>
|
||||
<Space>
|
||||
<Tag color="arcoblue"><IconFolder /></Tag>
|
||||
<Text>{folder}</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card className="queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>最近更新</Title>
|
||||
<Button type="text" size="mini">按更新时间排序</Button>
|
||||
</div>
|
||||
{notes.map((note) => (
|
||||
<button className="data-row note-row" key={note.title} onClick={() => onSelectItem(note.title)}>
|
||||
<span className="row-title"><IconFile /> {note.title}</span>
|
||||
<Tag>{note.type}</Tag>
|
||||
<span>{note.updated}</span>
|
||||
<span>{note.owner}</span>
|
||||
</button>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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'
|
||||
|
||||
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 }) {
|
||||
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>
|
||||
</div>
|
||||
<Button type="primary" icon={<IconPlus />}>新建任务</Button>
|
||||
</div>
|
||||
|
||||
<Card className="queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>任务清单</Title>
|
||||
<Tag color="arcoblue">{tasks.length} 个进行中</Tag>
|
||||
</div>
|
||||
{tasks.map((task) => (
|
||||
<button className="data-row task-row" key={task.title} onClick={() => onSelectItem(task.title)}>
|
||||
<span className="row-title"><IconCheckCircle /> {task.title}</span>
|
||||
<Tag color="arcoblue">{task.tag}</Tag>
|
||||
<span><IconUser /> {task.owner}</span>
|
||||
<span><IconCalendar /> {task.due}</span>
|
||||
<Progress percent={Number.parseInt(task.progress, 10)} size="small" />
|
||||
</button>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
<div className="task-board">
|
||||
{lanes.map((lane) => (
|
||||
<Card className="task-lane" bordered key={lane.title}>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>{lane.title}</Title>
|
||||
<Tag color={lane.color}>{lane.items.length}</Tag>
|
||||
</div>
|
||||
<Space direction="vertical" size={8}>
|
||||
{lane.items.map((title) => (
|
||||
<button className="task-card-button" key={title} onClick={() => onSelectItem(title)}>
|
||||
<Text>{title}</Text>
|
||||
<Text type="secondary">优先级 · 本周</Text>
|
||||
</button>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user