From 465e7bea9ba9fc0a9e97a5a0087704ddb5b023db Mon Sep 17 00:00:00 2001 From: yanweidong Date: Tue, 21 Jul 2026 11:16:34 +0800 Subject: [PATCH] feat: refine workspace explore sources --- apps/senlinai-acro-react/src/App.css | 129 ++++++++++++--- .../src/pages/workspace-explore.tsx | 150 ++++++++++-------- 2 files changed, 191 insertions(+), 88 deletions(-) diff --git a/apps/senlinai-acro-react/src/App.css b/apps/senlinai-acro-react/src/App.css index 7db13cc..863252b 100644 --- a/apps/senlinai-acro-react/src/App.css +++ b/apps/senlinai-acro-react/src/App.css @@ -644,22 +644,45 @@ min-width: 0; } +.explore-source-card.arco-card { + cursor: pointer; + transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease; +} + +.explore-source-card.arco-card:hover, +.explore-source-card.arco-card.active { + border-color: color-mix(in srgb, var(--senlin-primary) 52%, var(--senlin-border)); + box-shadow: 0 8px 20px color-mix(in srgb, var(--senlin-primary) 10%, transparent); +} + +.explore-source-card.arco-card.active { + background: color-mix(in srgb, var(--senlin-primary) 7%, var(--senlin-panel)); +} + .explore-source-card .arco-card-body { - display: flex; + display: grid; + grid-template-columns: 30px minmax(0, 1fr) auto; align-items: center; - gap: 12px; - min-height: 54px; - padding: 12px 14px; + gap: 9px; + min-height: 46px; + padding: 9px 10px; } .explore-source-icon { - width: 34px; - height: 34px; + width: 30px; + height: 30px; flex: 0 0 auto; display: inline-grid; place-items: center; border-radius: 8px; - font-size: 18px; + font-size: 16px; +} + +.explore-source-icon.mini { + width: 22px; + height: 22px; + border-radius: 6px; + font-size: 13px; } .explore-source-icon.blue { @@ -677,10 +700,16 @@ color: rgb(var(--orange-6)); } +.explore-source-icon.purple { + background: rgb(var(--purple-1)); + color: rgb(var(--purple-6)); +} + .explore-source-copy { min-width: 0; - display: grid; - gap: 2px; + display: flex; + align-items: baseline; + gap: 8px; } .explore-source-name { @@ -690,6 +719,20 @@ white-space: nowrap; } +.explore-source-actions { + display: flex; + align-items: center; + gap: 2px; + opacity: 0; + pointer-events: none; + transition: opacity 0.16s ease; +} + +.explore-source-card:hover .explore-source-actions { + opacity: 1; + pointer-events: auto; +} + .explore-reader-layout { min-height: 0; display: grid; @@ -704,7 +747,7 @@ } .explore-list-header { - height: 54px; + height: 46px; display: flex; align-items: center; justify-content: space-between; @@ -739,13 +782,13 @@ .explore-article-item { width: 100%; display: grid; - grid-template-columns: 34px minmax(0, 1fr); - gap: 12px; + grid-template-columns: 30px minmax(0, 1fr); + gap: 10px; border: 0; border-bottom: 1px solid var(--senlin-border); background: transparent; color: var(--senlin-text); - padding: 14px 12px; + padding: 9px 12px; text-align: left; cursor: pointer; } @@ -756,8 +799,8 @@ } .explore-source-logo { - width: 28px; - height: 28px; + width: 26px; + height: 26px; display: inline-grid; place-items: center; border-radius: 6px; @@ -767,6 +810,22 @@ font-weight: 800; } +.explore-source-logo.green { + background: rgb(var(--green-6)); +} + +.explore-source-logo.orange { + background: rgb(var(--orange-6)); +} + +.explore-source-logo.purple { + background: rgb(var(--purple-6)); +} + +.explore-source-logo.blue { + background: rgb(var(--arcoblue-6)); +} + .explore-source-logo.small { width: 20px; height: 20px; @@ -777,33 +836,47 @@ .explore-article-copy { min-width: 0; display: grid; - gap: 4px; + gap: 2px; } .explore-article-title { display: -webkit-box; overflow: hidden; color: var(--senlin-text); - font-size: 15px; + font-size: 14px; font-weight: 700; - line-height: 1.45; + line-height: 1.35; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } +.explore-article-summary { + font-size: 12px; +} + .explore-detail-toolbar { - height: 54px; + min-height: 46px; display: flex; align-items: center; + gap: 16px; justify-content: flex-end; - padding: 0 18px; + padding: 8px 14px 8px 18px; border-bottom: 1px solid var(--senlin-border); } +.explore-detail-toolbar h5 { + min-width: 0; + flex: 1; + overflow: hidden; + margin: 0; + text-overflow: ellipsis; + white-space: nowrap; +} + .explore-article-body { max-width: 780px; margin: 0 auto; - padding: 54px 28px 72px; + padding: 30px 28px 72px; } .explore-article-body h2 { @@ -814,7 +887,14 @@ } .explore-article-meta { - margin-top: 20px; + margin-bottom: 24px; +} + +.explore-article-content { + margin: 0; + color: var(--senlin-text); + font-size: 16px; + line-height: 1.9; } .explore-avatar-strip { @@ -880,7 +960,8 @@ .theme-dark .explore-source-icon.blue, .theme-dark .explore-source-icon.green, -.theme-dark .explore-source-icon.orange { +.theme-dark .explore-source-icon.orange, +.theme-dark .explore-source-icon.purple { background: color-mix(in srgb, currentcolor 18%, transparent); } @@ -895,7 +976,7 @@ gap: 10px; } - .workspace-explore-page .arco-col-8 { + .workspace-explore-page .arco-col-6 { flex: 0 0 100%; max-width: 100%; } diff --git a/apps/senlinai-acro-react/src/pages/workspace-explore.tsx b/apps/senlinai-acro-react/src/pages/workspace-explore.tsx index 30b3391..227e320 100644 --- a/apps/senlinai-acro-react/src/pages/workspace-explore.tsx +++ b/apps/senlinai-acro-react/src/pages/workspace-explore.tsx @@ -5,29 +5,41 @@ import { IconBook, IconCheckCircle, IconCompass, - IconFire, + IconDelete, + IconEdit, + IconFile, IconLink, - IconMessage, IconRefresh, - IconRobot, IconStar, + IconStorage, } from '@arco-design/web-react/icon' import type { InboxItem, Project, ProjectWorkspace } from './projects/project-types' const { Row, Col } = Grid const { Title, Text, Paragraph } = Typography +type DataSourceID = 'all' | 'manual' | 'requirements' | 'architecture' + type ExploreArticle = InboxItem & { project: Project + sourceID: DataSourceID } -type DataSource = { +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, @@ -41,31 +53,38 @@ export function WorkspaceExplorePage({ 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(articles[0]?.id ?? null) + const [activeArticleID, setActiveArticleID] = useState(filteredArticles[0]?.id ?? null) useEffect(() => { - if (articles.length === 0) { + if (filteredArticles.length === 0) { setActiveArticleID(null) return } - if (!activeArticleID || !articles.some((article) => article.id === activeArticleID)) { - setActiveArticleID(articles[0].id) + if (!activeArticleID || !filteredArticles.some((article) => article.id === activeArticleID)) { + setActiveArticleID(filteredArticles[0].id) } - }, [activeArticleID, articles]) + }, [activeArticleID, filteredArticles]) - const selected = articles.find((article) => article.id === activeArticleID) ?? articles[0] + const selected = filteredArticles.find((article) => article.id === activeArticleID) ?? filteredArticles[0] + const activeSource = SOURCE_META[activeSourceID] return (
探索 - 多个外部数据源的集中阅读页,采集文章、AI 总结并沉淀到项目。 + 多个外部数据源的集中阅读页,采集文章并沉淀到项目。
@@ -75,15 +94,25 @@ export function WorkspaceExplorePage({
- + {sources.map((source) => ( - - + + setActiveSourceID(source.id)} + > {source.icon} {source.name} {source.count} 条 + {source.id !== 'all' ? ( + event.stopPropagation()}> +
- {articles.map((article) => ( + {filteredArticles.map((article) => ( @@ -126,38 +159,23 @@ export function WorkspaceExplorePage({
+ {selected.title}
- {selected.title} - {sourceInitial(sourceName(selected))} - {sourceName(selected)} - 最新文章 + + {sourceInitial(SOURCE_META[selected.sourceID].name)} + + {SOURCE_META[selected.sourceID].name} + {selected.project.name} {selected.time} -
- {workspaces.slice(0, 8).map((workspace) => ( - - {workspace.project.short} - - ))} - +{Math.max(workspaces.length - 8, 0)} -
- - - - AI 总结 - - - {selected.summary || - '暂无正文摘要。同步数据源后,系统会在这里沉淀文章核心观点、风险点和可转化为项目计划的线索。'} - - + {selected.summary || '暂无正文'}
推荐:将外部文章中的项目机会、风险提示和资料线索归档到对应项目,形成可追踪的计划和知识资产。
@@ -173,27 +191,31 @@ export function WorkspaceExplorePage({ ) } -function dataSources(articles: ExploreArticle[]): DataSource[] { - const counts = new Map() - articles.forEach((article) => counts.set(sourceName(article), (counts.get(sourceName(article)) ?? 0) + 1)) - const iconSet = [ - { icon: , color: 'blue' }, - { icon: , color: 'green' }, - { icon: , color: 'orange' }, - ] +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 Array.from(counts.entries()) - .slice(0, 3) - .map(([name, count], index) => ({ - name, - count, - icon: iconSet[index]?.icon ?? , - color: iconSet[index]?.color ?? 'blue', - })) + return (Object.keys(SOURCE_META) as DataSourceID[]).map((id) => ({ + id, + ...SOURCE_META[id], + count: counts.get(id) ?? 0, + })) } -function sourceName(article: ExploreArticle) { - return article.meta?.replace(/^来源:/, '') || article.project.name +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) {