From 5d2f45ad83ca5083113622b36d3350ac22821550 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Wed, 22 Jul 2026 15:56:04 +0800 Subject: [PATCH] feat(web): add explore source configuration modals --- apps/web_v1/scripts/structure-check.mjs | 2 +- apps/web_v1/scripts/visual-check.mjs | 37 +++ apps/web_v1/src/pages/workspace-explore.tsx | 244 +++++++++++++++----- 3 files changed, 221 insertions(+), 62 deletions(-) diff --git a/apps/web_v1/scripts/structure-check.mjs b/apps/web_v1/scripts/structure-check.mjs index 4d82261..af81095 100644 --- a/apps/web_v1/scripts/structure-check.mjs +++ b/apps/web_v1/scripts/structure-check.mjs @@ -130,7 +130,7 @@ for (const forbidden of ['DeepSeek V4.0 Flash', '给 DeepSeek 发送消息', 'Ic } const explorePageSource = readFileSync('src/pages/workspace-explore.tsx', 'utf8') -for (const required of ['explore-source-card', 'explore-reader-layout', '同步数据源', '添加数据源', 'detectSource']) { +for (const required of ['explore-source-card', 'explore-reader-layout', '同步数据源', '添加数据源', 'detectSource', 'explore-source-modal', 'openAddSource', 'openEditSource']) { if (!explorePageSource.includes(required)) failures.push(`workspace explore page must restore ${required}`) } diff --git a/apps/web_v1/scripts/visual-check.mjs b/apps/web_v1/scripts/visual-check.mjs index 6bb6066..d997cf3 100644 --- a/apps/web_v1/scripts/visual-check.mjs +++ b/apps/web_v1/scripts/visual-check.mjs @@ -200,6 +200,31 @@ await page.waitForTimeout(500) await page.screenshot({ path: 'test-results/workspace-explore-react-acro-light.png', fullPage: true }) const workspaceExploreMetrics = await collectMetrics() +await page.getByRole('button', { name: '添加数据源' }).click() +await page.waitForSelector('.explore-source-modal') +await page.waitForTimeout(250) +await page.screenshot({ path: 'test-results/explore-add-source-modal.png', fullPage: true }) +const addSourceModalCheck = await page.evaluate(() => ({ + title: document.querySelector('.explore-source-modal .arco-modal-title')?.textContent ?? '', + inputCount: document.querySelectorAll('.explore-source-modal input, .explore-source-modal textarea').length, +})) +await page.locator('.explore-source-modal input').first().fill('行业资讯') +await page.locator('.explore-source-modal .arco-modal-footer .arco-btn-primary').click() +const addedSourceVisible = await page.locator('.explore-source-card', { hasText: '行业资讯' }).count() === 1 + +await page.locator('.explore-source-card').nth(1).hover() +await page.getByRole('button', { name: '编辑手动收集' }).click() +await page.waitForSelector('.explore-source-modal') +await page.waitForTimeout(250) +await page.screenshot({ path: 'test-results/explore-edit-source-modal.png', fullPage: true }) +const editSourceModalCheck = await page.evaluate(() => ({ + title: document.querySelector('.explore-source-modal .arco-modal-title')?.textContent ?? '', + name: document.querySelector('.explore-source-modal input')?.value ?? '', +})) +await page.locator('.explore-source-modal input').first().fill('手动整理') +await page.locator('.explore-source-modal .arco-modal-footer .arco-btn-primary').click() +const editedSourceVisible = await page.locator('.explore-source-card', { hasText: '手动整理' }).count() === 1 + await page.locator('.project-button').first().click() await page.waitForTimeout(500) await page.screenshot({ path: 'test-results/project-react-acro-light.png', fullPage: true }) @@ -246,6 +271,10 @@ await server.close() console.log(JSON.stringify({ workspaceMetrics, workspaceExploreMetrics, + addSourceModalCheck, + addedSourceVisible, + editSourceModalCheck, + editedSourceVisible, projectMetrics, channelSidebarHoverMetrics, stageHoverMetrics, @@ -341,6 +370,14 @@ if (workspaceExploreMetrics.channelSidebar) failures.push(`expected workspace ex 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 (!addSourceModalCheck.title.includes('添加数据源') || addSourceModalCheck.inputCount < 3) { + failures.push(`expected add data source modal with fields, got ${JSON.stringify(addSourceModalCheck)}`) +} +if (!addedSourceVisible) failures.push('expected saved data source to appear in the source cards') +if (!editSourceModalCheck.title.includes('编辑数据源') || editSourceModalCheck.name !== '手动收集') { + failures.push(`expected prefilled edit data source modal, got ${JSON.stringify(editSourceModalCheck)}`) +} +if (!editedSourceVisible) failures.push('expected edited data source name to update its card') 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 227e320..eceaaf1 100644 --- a/apps/web_v1/src/pages/workspace-explore.tsx +++ b/apps/web_v1/src/pages/workspace-explore.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react' import type { ReactNode } from 'react' -import { Button, Card, Empty, Grid, Space, Typography } from '@arco-design/web-react' +import { Alert, Button, Card, Empty, Grid, Input, Modal, Select, Space, Typography } from '@arco-design/web-react' import { IconBook, IconCheckCircle, @@ -17,27 +17,50 @@ import type { InboxItem, Project, ProjectWorkspace } from './projects/project-ty const { Row, Col } = Grid const { Title, Text, Paragraph } = Typography +const { TextArea } = Input -type DataSourceID = 'all' | 'manual' | 'requirements' | 'architecture' +type BuiltinDataSourceID = 'all' | 'manual' | 'requirements' | 'architecture' +type DataSourceID = BuiltinDataSourceID | `custom:${number}` +type DataSourceKind = 'manual' | 'link' | 'rss' type ExploreArticle = InboxItem & { project: Project - sourceID: DataSourceID + sourceID: BuiltinDataSourceID } -type DataSourceCard = { +type DataSourceConfig = { id: DataSourceID name: string - count: number icon: ReactNode color: string + kind: DataSourceKind + url: string + description: 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' }, +type DataSourceCard = DataSourceConfig & { + count: number +} + +type SourceDraft = { + name: string + kind: DataSourceKind + url: string + description: string +} + +const INITIAL_SOURCES: DataSourceConfig[] = [ + { id: 'all', name: '全部', icon: , color: 'blue', kind: 'manual', url: '', description: '全部探索内容' }, + { id: 'manual', name: '手动收集', icon: , color: 'green', kind: 'manual', url: '', description: '手动收集的文章与线索' }, + { id: 'requirements', name: '需求文档', icon: , color: 'orange', kind: 'link', url: '', description: '产品需求与业务文档' }, + { id: 'architecture', name: '架构讨论', icon: , color: 'purple', kind: 'link', url: '', description: '架构方案与系统设计讨论' }, +] + +const EMPTY_SOURCE_DRAFT: SourceDraft = { + name: '', + kind: 'link', + url: '', + description: '', } export function WorkspaceExplorePage({ @@ -58,12 +81,17 @@ export function WorkspaceExplorePage({ ), [workspaces], ) + const [sourceConfigs, setSourceConfigs] = useState(INITIAL_SOURCES) const [activeSourceID, setActiveSourceID] = useState('all') + const [sourceModalMode, setSourceModalMode] = useState<'add' | 'edit' | null>(null) + const [editingSourceID, setEditingSourceID] = useState(null) + const [sourceDraft, setSourceDraft] = useState(EMPTY_SOURCE_DRAFT) + const [sourceError, setSourceError] = useState('') const filteredArticles = useMemo( () => (activeSourceID === 'all' ? articles : articles.filter((article) => article.sourceID === activeSourceID)), [activeSourceID, articles], ) - const sources = useMemo(() => dataSources(articles), [articles]) + const sources = useMemo(() => dataSources(articles, sourceConfigs), [articles, sourceConfigs]) const [activeArticleID, setActiveArticleID] = useState(filteredArticles[0]?.id ?? null) useEffect(() => { @@ -77,7 +105,62 @@ export function WorkspaceExplorePage({ }, [activeArticleID, filteredArticles]) const selected = filteredArticles.find((article) => article.id === activeArticleID) ?? filteredArticles[0] - const activeSource = SOURCE_META[activeSourceID] + const activeSource = sourceByID(sources, activeSourceID) + const selectedSource = selected ? sourceByID(sources, selected.sourceID) : activeSource + + function openAddSource() { + setEditingSourceID(null) + setSourceDraft(EMPTY_SOURCE_DRAFT) + setSourceError('') + setSourceModalMode('add') + } + + function openEditSource(source: DataSourceCard) { + setEditingSourceID(source.id) + setSourceDraft({ + name: source.name, + kind: source.kind, + url: source.url, + description: source.description, + }) + setSourceError('') + setSourceModalMode('edit') + } + + function saveSource() { + const name = sourceDraft.name.trim() + if (!name) { + setSourceError('请输入数据源名称') + return + } + const nextDraft = { ...sourceDraft, name, url: sourceDraft.url.trim(), description: sourceDraft.description.trim() } + if (sourceModalMode === 'edit' && editingSourceID) { + setSourceConfigs((current) => current.map((source) => source.id === editingSourceID ? { ...source, ...nextDraft } : source)) + } else { + setSourceConfigs((current) => [ + ...current, + { + id: `custom:${Date.now()}`, + ...nextDraft, + icon: sourceIcon(nextDraft.kind), + color: sourceColor(nextDraft.kind), + }, + ]) + } + setSourceModalMode(null) + } + + function deleteSource(source: DataSourceCard) { + Modal.confirm({ + title: `删除“${source.name}”数据源?`, + content: '删除后,该数据源将从探索页移除。', + okButtonProps: { status: 'danger' }, + onOk: () => { + setSourceConfigs((current) => current.filter((item) => item.id !== source.id)) + if (activeSourceID === source.id) setActiveSourceID('all') + }, + }) + } return (
@@ -88,7 +171,7 @@ export function WorkspaceExplorePage({
- @@ -109,8 +192,8 @@ export function WorkspaceExplorePage({ {source.id !== 'all' ? ( event.stopPropagation()}> - - ))} + {filteredArticles.map((article) => { + const articleSource = sourceByID(sources, article.sourceID) + return ( + + ) + })} @@ -168,10 +254,10 @@ export function WorkspaceExplorePage({
- - {sourceInitial(SOURCE_META[selected.sourceID].name)} + + {sourceInitial(selectedSource.name)} - {SOURCE_META[selected.sourceID].name} + {selectedSource.name} {selected.project.name} {selected.time} @@ -187,37 +273,73 @@ export function WorkspaceExplorePage({ )} + + setSourceModalMode(null)} + onOk={saveSource} + > + + {sourceError && } + + + +