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) => (
+
+ ))}
+
+
+ ))}
+
+
+ )
+}