From a299da63e7183cb3837e566a9d1b547a5c405ae6 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Mon, 20 Jul 2026 12:26:21 +0800 Subject: [PATCH] Add project channel pages --- .../scripts/structure-check.mjs | 6 + .../scripts/visual-check.mjs | 35 +++++ apps/senlinai-acro-react/src/App.css | 148 ++++++++++++++++++ .../senlinai-acro-react/src/pages/project.tsx | 8 +- .../src/pages/projects/project-ai.tsx | 68 ++++++++ .../pages/projects/project-channel-page.tsx | 35 +++++ .../src/pages/projects/project-cron.tsx | 63 ++++++++ .../src/pages/projects/project-inbox.tsx | 90 +++++++++++ .../src/pages/projects/project-notes.tsx | 51 ++++++ .../src/pages/projects/project-tasks.tsx | 61 ++++++++ 10 files changed, 563 insertions(+), 2 deletions(-) create mode 100644 apps/senlinai-acro-react/src/pages/projects/project-ai.tsx create mode 100644 apps/senlinai-acro-react/src/pages/projects/project-channel-page.tsx create mode 100644 apps/senlinai-acro-react/src/pages/projects/project-cron.tsx create mode 100644 apps/senlinai-acro-react/src/pages/projects/project-inbox.tsx create mode 100644 apps/senlinai-acro-react/src/pages/projects/project-notes.tsx create mode 100644 apps/senlinai-acro-react/src/pages/projects/project-tasks.tsx diff --git a/apps/senlinai-acro-react/scripts/structure-check.mjs b/apps/senlinai-acro-react/scripts/structure-check.mjs index 9cca642..d445992 100644 --- a/apps/senlinai-acro-react/scripts/structure-check.mjs +++ b/apps/senlinai-acro-react/scripts/structure-check.mjs @@ -6,6 +6,12 @@ const requiredFiles = [ 'src/pages/workspace.tsx', 'src/pages/project.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', + 'src/pages/projects/project-cron.tsx', 'src/pages/projects/project-data.tsx', 'src/pages/projects/project-rail.tsx', 'src/pages/projects/project-sidebar.tsx', diff --git a/apps/senlinai-acro-react/scripts/visual-check.mjs b/apps/senlinai-acro-react/scripts/visual-check.mjs index c93af1c..b732e73 100644 --- a/apps/senlinai-acro-react/scripts/visual-check.mjs +++ b/apps/senlinai-acro-react/scripts/visual-check.mjs @@ -133,6 +133,29 @@ const propertyMetrics = await collectMetrics() await page.locator('.property-list').hover() const propertyListHoverMetrics = 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: '笔记资料' }, + { label: 'Cron 计划任务', pageClass: 'project-cron-page', expectedHeading: 'Cron 计划任务' }, +]) { + await page.locator('.channel-button', { hasText: channel.label }).click() + await page.waitForTimeout(250) + channelPageChecks.push(await page.evaluate((expected) => { + const pageNode = document.querySelector(`.${expected.pageClass}`) + const heading = pageNode?.querySelector('h4, h5')?.textContent ?? '' + const activeChannel = document.querySelector('.channel-button.active')?.textContent ?? '' + return { + ...expected, + foundPage: Boolean(pageNode), + heading, + activeChannel, + } + }, channel)) +} + await page.locator('.topbar-actions .arco-btn').first().click() await page.screenshot({ path: 'test-results/project-react-acro-dark.png', fullPage: true }) @@ -149,6 +172,7 @@ console.log(JSON.stringify({ inspectorTabsContentHoverMetrics, propertyMetrics, propertyListHoverMetrics, + channelPageChecks, errors, }, null, 2)) @@ -279,6 +303,17 @@ for (const [name, overflow] of [ if (propertyMetrics.propertyListOverflow?.scrollbarWidth !== 'none') { failures.push(`expected property list scrollbar to be hidden until hover, got ${JSON.stringify(propertyMetrics.propertyListOverflow)}`) } +for (const check of channelPageChecks) { + if (!check.foundPage) { + failures.push(`expected ${check.label} to render .${check.pageClass}`) + } + if (!check.heading.includes(check.expectedHeading)) { + failures.push(`expected ${check.label} heading to include ${check.expectedHeading}, got ${JSON.stringify(check.heading)}`) + } + if (!check.activeChannel.includes(check.label)) { + failures.push(`expected ${check.label} sidebar button to stay active, got ${JSON.stringify(check.activeChannel)}`) + } +} if (failures.length) { throw new Error(failures.join('\n')) diff --git a/apps/senlinai-acro-react/src/App.css b/apps/senlinai-acro-react/src/App.css index cbc2597..16626f1 100644 --- a/apps/senlinai-acro-react/src/App.css +++ b/apps/senlinai-acro-react/src/App.css @@ -558,6 +558,154 @@ grid-template-columns: minmax(260px, 1fr) 80px 160px 80px; } +.project-channel-page { + min-width: 0; +} + +.compact-card .arco-card-body { + padding: 12px 14px; +} + +.mail-layout, +.ai-workspace { + min-width: 0; + display: grid; + grid-template-columns: minmax(260px, 0.42fr) minmax(360px, 0.58fr); + gap: 12px; +} + +.mail-list, +.mail-detail, +.ai-session-list, +.ai-chat-panel { + min-width: 0; +} + +.mail-item, +.ai-session, +.task-card-button { + width: 100%; + display: grid; + gap: 4px; + border: 0; + border-top: 1px solid var(--senlin-border); + background: transparent; + color: var(--senlin-text); + padding: 10px 0; + text-align: left; + cursor: pointer; +} + +.mail-item:first-of-type, +.ai-session:first-of-type { + border-top: 0; +} + +.mail-item.active, +.ai-session.active, +.mail-item:hover, +.ai-session:hover, +.task-card-button:hover { + background: color-mix(in srgb, var(--senlin-primary) 6%, transparent); +} + +.mail-item span:first-child, +.ai-session > .arco-typography:first-child { + min-width: 0; + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; +} + +.mail-item div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.mail-item time, +.ai-session time { + color: var(--senlin-muted); + font-size: 12px; +} + +.mail-detail .arco-typography, +.mail-item .arco-typography, +.ai-session .arco-typography { + overflow: hidden; + text-overflow: ellipsis; +} + +.reply-box, +.chat-input { + display: grid; + gap: 10px; + margin-top: 16px; + padding-top: 12px; + border-top: 1px solid var(--senlin-border); +} + +.task-board { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.task-lane .arco-card-body { + display: grid; + gap: 10px; + padding: 12px; +} + +.task-card-button { + border: 1px solid var(--senlin-border); + border-radius: 6px; + padding: 10px; +} + +.chat-thread { + display: grid; + gap: 12px; +} + +.chat-message { + display: grid; + grid-template-columns: 30px 1fr; + gap: 10px; +} + +.chat-message p { + margin: 0; + padding: 10px 12px; + border: 1px solid var(--senlin-border); + border-radius: 6px; + background: var(--senlin-soft-blue); +} + +.chat-message.user p { + background: var(--senlin-panel); +} + +.folder-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.folder-card .arco-card-body { + padding: 14px; +} + +.cron-row { + grid-template-columns: minmax(180px, 1fr) 64px 92px 92px 92px 48px 40px; +} + +.cron-summary .arco-card-body { + padding: 12px; +} + .inspector { min-height: 0; overflow-x: hidden; diff --git a/apps/senlinai-acro-react/src/pages/project.tsx b/apps/senlinai-acro-react/src/pages/project.tsx index 9051cda..6f07d97 100644 --- a/apps/senlinai-acro-react/src/pages/project.tsx +++ b/apps/senlinai-acro-react/src/pages/project.tsx @@ -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 ? ( ) : ( - + )} diff --git a/apps/senlinai-acro-react/src/pages/projects/project-ai.tsx b/apps/senlinai-acro-react/src/pages/projects/project-ai.tsx new file mode 100644 index 0000000..8fccac6 --- /dev/null +++ b/apps/senlinai-acro-react/src/pages/projects/project-ai.tsx @@ -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 ( +
+
+
+ AI 会话 + {activeProject.name} 的项目内 AI session,左侧会话列表,右侧对话详情。 +
+ +
+ +
+ +
+ Session + {aiSessions.length} +
+ {aiSessions.map((session, index) => ( + + ))} +
+ + +
+ {aiSessions[0].title} + 队列空闲 +
+
+ {messages.map((message, index) => ( +
+ {message.role === 'assistant' ? : '张'} + {message.text} +
+ ))} +
+
+ + + + + +
+
+
+
+ ) +} 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 new file mode 100644 index 0000000..fda8166 --- /dev/null +++ b/apps/senlinai-acro-react/src/pages/projects/project-channel-page.tsx @@ -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 + case 'tasks': + return + case 'ai': + return + case 'notes': + return + case 'cron': + return + case 'overview': + case 'research': + case 'design': + default: + return + } +} diff --git a/apps/senlinai-acro-react/src/pages/projects/project-cron.tsx b/apps/senlinai-acro-react/src/pages/projects/project-cron.tsx new file mode 100644 index 0000000..ad6fe53 --- /dev/null +++ b/apps/senlinai-acro-react/src/pages/projects/project-cron.tsx @@ -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 ( +
+
+
+ Cron 计划任务 + {activeProject.name} 的定时任务、自动检查和周期性 AI 工作。 +
+ +
+ + +
+ 任务列表 + + 3 运行中 + 1 暂停 + +
+ {cronJobs.map((job) => ( +
onSelectItem(job.name)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') onSelectItem(job.name) + }} + > + {job.name} + {job.status} + {job.expr} + {job.lastRun} + {job.nextRun} + {job.owner} + +
+ ))} +
+ + + + + 最近一次自动任务完成于今天 12:00,资料索引刷新成功,无异常重试。 + + +
+ ) +} diff --git a/apps/senlinai-acro-react/src/pages/projects/project-inbox.tsx b/apps/senlinai-acro-react/src/pages/projects/project-inbox.tsx new file mode 100644 index 0000000..711c943 --- /dev/null +++ b/apps/senlinai-acro-react/src/pages/projects/project-inbox.tsx @@ -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 ( +
+
+
+ Inbox 消息流 + {activeProject.name} 的邮件式收件箱,聚合外部反馈、系统通知和待处理线索。 +
+ + + + +
+ + + {folders.map((folder) => ( + + + + {folder.count} + {folder.label} + + + + ))} + + +
+ +
+ 今日消息 + +
+ {inbox.map((item, index) => ( + + ))} +
+ + +
+ {selected.title} + +
+ {selected.meta} · {selected.owner} + + 已收到新的项目消息,需要判断是否转为任务、笔记或资料。当前建议先补充影响范围,再同步给相关负责人。 + +
+ 快速处理 + + + + + +
+
+
+
+ ) +} diff --git a/apps/senlinai-acro-react/src/pages/projects/project-notes.tsx b/apps/senlinai-acro-react/src/pages/projects/project-notes.tsx new file mode 100644 index 0000000..26ee787 --- /dev/null +++ b/apps/senlinai-acro-react/src/pages/projects/project-notes.tsx @@ -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 ( +
+
+
+ 笔记资料 + {activeProject.name} 的在线文件管理页,集中管理笔记、资料和附件。 +
+ + + + +
+ +
+ {folders.map((folder) => ( + + + + {folder} + + + ))} +
+ + +
+ 最近更新 + +
+ {notes.map((note) => ( + + ))} +
+
+ ) +} diff --git a/apps/senlinai-acro-react/src/pages/projects/project-tasks.tsx b/apps/senlinai-acro-react/src/pages/projects/project-tasks.tsx new file mode 100644 index 0000000..99d988b --- /dev/null +++ b/apps/senlinai-acro-react/src/pages/projects/project-tasks.tsx @@ -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 ( +
+
+
+ 工作计划 + {activeProject.name} 的 TODO 计划、负责人和截止时间。 +
+ +
+ + +
+ 任务清单 + {tasks.length} 个进行中 +
+ {tasks.map((task) => ( + + ))} +
+ +
+ {lanes.map((lane) => ( + +
+ {lane.title} + {lane.items.length} +
+ + {lane.items.map((title) => ( + + ))} + +
+ ))} +
+
+ ) +}