From 2e60f273fac6f02b42e3217f2887ac135cf1cf13 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Wed, 22 Jul 2026 15:39:57 +0800 Subject: [PATCH] feat(web): restore legacy explore workspace --- apps/web_v1/scripts/structure-check.mjs | 6 +- apps/web_v1/scripts/visual-check.mjs | 27 ++- apps/web_v1/src/pages/workspace-explore.tsx | 218 +++++++++++++++++++- apps/web_v1/src/pages/workspace-home.tsx | 2 +- 4 files changed, 243 insertions(+), 10 deletions(-) diff --git a/apps/web_v1/scripts/structure-check.mjs b/apps/web_v1/scripts/structure-check.mjs index 23ebb0d..e3a52af 100644 --- a/apps/web_v1/scripts/structure-check.mjs +++ b/apps/web_v1/scripts/structure-check.mjs @@ -121,8 +121,12 @@ for (const forbidden of ['DeepSeek V4.0 Flash', '给 DeepSeek 发送消息', 'Ic if (aiPageSource.includes(forbidden)) failures.push(`project AI page contains unsupported chat control ${forbidden}`) } +const explorePageSource = readFileSync('src/pages/workspace-explore.tsx', 'utf8') +for (const required of ['explore-source-card', 'explore-reader-layout', '同步数据源', '添加数据源', 'detectSource']) { + if (!explorePageSource.includes(required)) failures.push(`workspace explore page must restore ${required}`) +} + const unsupportedControls = [ - { file: 'src/pages/workspace-explore.tsx', required: '暂未开放', forbidden: ['同步数据源', '添加数据源', 'IconRefresh', 'IconEdit', 'IconDelete'] }, { file: 'src/pages/projects/project-new-channel.tsx', required: '暂未开放', forbidden: ['保存频道', ' page.evaluate(() => { workbenchMain: rect(main), hasWorkspacePage: Boolean(workspacePage), hasProjectOverview: Boolean(projectOverview), + exploreSourceCardCount: document.querySelectorAll('.explore-source-card').length, + hasExploreReader: Boolean(document.querySelector('.explore-reader-layout')), projectRailWidth: rail?.getBoundingClientRect().width, projectRail: rect(rail), channelSidebar: rect(sidebar), @@ -300,6 +323,8 @@ if (!workspaceExploreMetrics.workspaceExploreButtonActive) failures.push('expect if (workspaceExploreMetrics.dashboardButtonActive) failures.push('expected dashboard button not to be active on workspace explore page') if (workspaceExploreMetrics.channelSidebar) failures.push(`expected workspace explore to hide channel sidebar, got ${JSON.stringify(workspaceExploreMetrics.channelSidebar)}`) if (workspaceExploreMetrics.inspector) failures.push(`expected workspace explore to hide inspector, got ${JSON.stringify(workspaceExploreMetrics.inspector)}`) +if (workspaceExploreMetrics.exploreSourceCardCount !== 4) failures.push(`expected four legacy explore source cards, got ${workspaceExploreMetrics.exploreSourceCardCount}`) +if (!workspaceExploreMetrics.hasExploreReader) failures.push('expected legacy explore article reader to render') if (!workspaceExploreMetrics.stage || !workspaceExploreMetrics.projectRail) { failures.push(`missing workspace explore layout regions: rail=${JSON.stringify(workspaceExploreMetrics.projectRail)}, stage=${JSON.stringify(workspaceExploreMetrics.stage)}`) } else if (Math.abs(workspaceExploreMetrics.stage.left - workspaceExploreMetrics.projectRail.right) > 1) { diff --git a/apps/web_v1/src/pages/workspace-explore.tsx b/apps/web_v1/src/pages/workspace-explore.tsx index 3ad310e..227e320 100644 --- a/apps/web_v1/src/pages/workspace-explore.tsx +++ b/apps/web_v1/src/pages/workspace-explore.tsx @@ -1,19 +1,223 @@ -import { Card, Empty, Typography } from '@arco-design/web-react' +import { useEffect, useMemo, useState } from 'react' +import type { ReactNode } from 'react' +import { Button, Card, Empty, Grid, Space, Typography } from '@arco-design/web-react' +import { + IconBook, + IconCheckCircle, + IconCompass, + IconDelete, + IconEdit, + IconFile, + IconLink, + IconRefresh, + IconStar, + IconStorage, +} from '@arco-design/web-react/icon' +import type { InboxItem, Project, ProjectWorkspace } from './projects/project-types' -const { Title, Text } = Typography +const { Row, Col } = Grid +const { Title, Text, Paragraph } = Typography + +type DataSourceID = 'all' | 'manual' | 'requirements' | 'architecture' + +type ExploreArticle = InboxItem & { + project: Project + sourceID: DataSourceID +} + +type DataSourceCard = { + id: DataSourceID + name: string + count: number + icon: ReactNode + color: string +} + +const SOURCE_META: Record = { + all: { name: '全部', icon: , color: 'blue' }, + manual: { name: '手动收集', icon: , color: 'green' }, + requirements: { name: '需求文档', icon: , color: 'orange' }, + architecture: { name: '架构讨论', icon: , color: 'purple' }, +} + +export function WorkspaceExplorePage({ + workspaces, + onSelectItem, +}: { + workspaces: ProjectWorkspace[] + onSelectItem: (title: string) => void +}) { + const articles = useMemo( + () => + workspaces.flatMap((workspace) => + workspace.inbox.map((item) => ({ + ...item, + project: workspace.project, + sourceID: detectSource(item), + })), + ), + [workspaces], + ) + const [activeSourceID, setActiveSourceID] = useState('all') + const filteredArticles = useMemo( + () => (activeSourceID === 'all' ? articles : articles.filter((article) => article.sourceID === activeSourceID)), + [activeSourceID, articles], + ) + const sources = useMemo(() => dataSources(articles), [articles]) + const [activeArticleID, setActiveArticleID] = useState(filteredArticles[0]?.id ?? null) + + useEffect(() => { + if (filteredArticles.length === 0) { + setActiveArticleID(null) + return + } + if (!activeArticleID || !filteredArticles.some((article) => article.id === activeArticleID)) { + setActiveArticleID(filteredArticles[0].id) + } + }, [activeArticleID, filteredArticles]) + + const selected = filteredArticles.find((article) => article.id === activeArticleID) ?? filteredArticles[0] + const activeSource = SOURCE_META[activeSourceID] -export function WorkspaceExplorePage() { return (
探索 - 外部数据源与同步能力不在当前 MVP 范围内。 + 多个外部数据源的集中阅读页,采集文章并沉淀到项目。
+ + + +
- - - + + + {sources.map((source) => ( + + setActiveSourceID(source.id)} + > + {source.icon} + + {source.name} + {source.count} 条 + + {source.id !== 'all' ? ( + event.stopPropagation()}> +
+
+ {filteredArticles.map((article) => ( + + ))} +
+ + + +
+ {selected.title} + +
+
+ + + {sourceInitial(SOURCE_META[selected.sourceID].name)} + + {SOURCE_META[selected.sourceID].name} + {selected.project.name} + {selected.time} + + {selected.summary || '暂无正文'} +
+ 推荐:将外部文章中的项目机会、风险提示和资料线索归档到对应项目,形成可追踪的计划和知识资产。 +
+
+
+ + ) : ( + + + + )} ) } + +function dataSources(articles: ExploreArticle[]): DataSourceCard[] { + const counts = new Map([ + ['all', articles.length], + ['manual', 0], + ['requirements', 0], + ['architecture', 0], + ]) + articles.forEach((article) => counts.set(article.sourceID, (counts.get(article.sourceID) ?? 0) + 1)) + + return (Object.keys(SOURCE_META) as DataSourceID[]).map((id) => ({ + id, + ...SOURCE_META[id], + count: counts.get(id) ?? 0, + })) +} + +function detectSource(article: InboxItem): DataSourceID { + const text = `${article.title} ${article.meta} ${article.tag} ${article.summary}` + if (/架构|技术方案|系统设计|architecture/i.test(text)) { + return 'architecture' + } + if (/需求|PRD|产品文档|requirement/i.test(text)) { + return 'requirements' + } + return 'manual' +} + +function sourceInitial(name: string) { + return name.trim().slice(0, 1) || '源' +} diff --git a/apps/web_v1/src/pages/workspace-home.tsx b/apps/web_v1/src/pages/workspace-home.tsx index a5877f9..e032f61 100644 --- a/apps/web_v1/src/pages/workspace-home.tsx +++ b/apps/web_v1/src/pages/workspace-home.tsx @@ -135,7 +135,7 @@ export function ProjectPage({ {activeView === 'workspace' ? ( ) : activeView === 'workspace-explore' ? ( - + ) : (