import { useState } from 'react' import { ConfigProvider, Message, Spin } from '@arco-design/web-react' import '@arco-design/web-react/dist/css/arco.css' import '../App.css' import { login, setApiBaseUrl, type ApiSession } from '../api/client' import { mapWorkspace } from '../api/mappers' import { createCronPlan, createProject, createProjectTag, createTask, fetchProjectWorkspace, fetchProjects, updateProject, updateTask, uploadSource, } from '../api/projects' import type { SearchResultDTO } from '../api/search' import { LoginPage } from '../pages/login' import { ProjectActionModals, type CronDraft, type ProjectActionModal, type ProjectDraft, type SourceDraft, type TaskDraft } from '../pages/projects/project-action-modals' import { SearchResultPreview } from '../pages/projects/search-result-preview' import type { ProjectSettingsUpdate } from '../pages/projects/project-sidebar' import { ProjectPage } from '../pages/workspace-home' import type { WorkspaceTaskUpdate } from '../pages/workspace-body' import type { ChannelKey, Project, ProjectWorkspace, Screen, Theme, WorkbenchView } from '../pages/projects/project-types' import { useWorkbenchSearch } from './use-workbench-search' function App() { const [screen, setScreen] = useState('login') const [theme, setTheme] = useState('light') const [activeView, setActiveView] = useState('workspace') const [session, setSession] = useState(null) const [workspaces, setWorkspaces] = useState([]) const [activeProjectID, setActiveProjectID] = useState('') const [activeChannel, setActiveChannel] = useState('overview') const [activeTaskID, setActiveTaskID] = useState(null) const [, setSelectedItem] = useState('探索采集') const [loading, setLoading] = useState(false) const [actionLoading, setActionLoading] = useState(false) const [activeModal, setActiveModal] = useState(null) const [searchResultPreview, setSearchResultPreview] = useState(null) const workspaceSearch = useWorkbenchSearch(session) const dark = theme === 'dark' const activeWorkspace = workspaces.find((workspace) => workspace.project.id === activeProjectID) ?? workspaces[0] const activeTagOptions = activeWorkspace?.tags.filter((tag) => tag !== 'all' && tag !== '全部') ?? [] async function loadWorkspaces(nextSession: ApiSession, preferredProjectID = activeProjectID) { const backendProjects = await fetchProjects(nextSession) const backendWorkspaces = await Promise.all( backendProjects.map((project, index) => fetchProjectWorkspace(nextSession, project.id).then((workspace) => mapWorkspace(workspace, index))), ) setWorkspaces(backendWorkspaces) const nextProjectID = backendWorkspaces.find((workspace) => workspace.project.id === preferredProjectID)?.project.id ?? backendWorkspaces[0]?.project.id ?? '' setActiveProjectID(nextProjectID) return backendWorkspaces } async function handleLogin(input: { server: string; email: string; password: string }) { setLoading(true) try { setApiBaseUrl(input.server) const nextSession = await login(input.email, input.password) const backendWorkspaces = await loadWorkspaces(nextSession, '') setSession(nextSession) setActiveProjectID(backendWorkspaces[0]?.project.id ?? '') setActiveChannel('overview') setActiveView('workspace') setScreen('workbench') } catch (error) { Message.error(error instanceof Error ? error.message : '登录失败') } finally { setLoading(false) } } async function refreshAfterAction(nextProjectID = activeProjectID) { if (!session) return await loadWorkspaces(session, nextProjectID) } async function runAction(action: () => Promise, success: string) { setActionLoading(true) try { await action() setActiveModal(null) Message.success(success) } catch (error) { Message.error(error instanceof Error ? error.message : '操作失败') } finally { setActionLoading(false) } } function requireSession() { if (!session) throw new Error('未登录') return session } function requireActiveProject() { if (!activeWorkspace?.project.id) throw new Error('未选择项目') return activeWorkspace.project.id } function normalizeOptionalTime(value: string) { const trimmed = value.trim() return trimmed === '' ? undefined : trimmed } function handleCreateProject(draft: ProjectDraft) { if (!draft.name.trim()) { Message.warning('请输入项目名称') return } void runAction(async () => { const created = await createProject(requireSession(), { name: draft.name.trim(), identifier: draft.identifier.trim(), icon: draft.icon.trim(), background: draft.background.trim(), description: draft.description.trim(), }) const nextProjectID = created.id await refreshAfterAction(nextProjectID) if (nextProjectID) { setActiveProjectID(nextProjectID) setActiveView('project') setActiveChannel('overview') } }, '项目已创建') } function handleCreateTask(draft: TaskDraft) { if (!draft.title.trim()) { Message.warning('请输入任务标题') return } void runAction(async () => { await createTask(requireSession(), requireActiveProject(), { title: draft.title.trim(), description: draft.description.trim(), status: 'open', tag: draft.tag.trim(), }) await refreshAfterAction() setActiveChannel('tasks') }, '任务已创建') } function handleUploadSource(draft: SourceDraft) { if (!draft.file) { Message.warning('请选择文件') return } const file = draft.file void runAction(async () => { await uploadSource(requireSession(), requireActiveProject(), { title: draft.title.trim(), file, }) await refreshAfterAction() setActiveChannel('notes') }, '文件已上传') } function handleCreateCronPlan(draft: CronDraft) { if (!draft.title.trim()) { Message.warning('请输入计划名称') return } if (!draft.schedule.trim()) { Message.warning('请输入 Cron 表达式') return } void runAction(async () => { await createCronPlan(requireSession(), requireActiveProject(), { title: draft.title.trim(), schedule: draft.schedule.trim(), enabled: draft.enabled, nextRunAt: normalizeOptionalTime(draft.nextRunAt), }) await refreshAfterAction() setActiveChannel('cron') }, '计划任务已创建') } function handleCreateProjectTag(name: string) { const trimmedName = name.trim() if (!trimmedName) { Message.warning('请输入标签名称') return } void runAction(async () => { await createProjectTag(requireSession(), requireActiveProject(), { name: trimmedName }) await refreshAfterAction() }, '标签已创建') } function openTask(project: Project, taskID: string) { setActiveProjectID(project.id) setActiveView('project') setActiveChannel('tasks') setActiveTaskID(taskID) } function handleUpdateWorkspaceTask(update: WorkspaceTaskUpdate) { void runAction(async () => { await updateTask(requireSession(), update.originalProjectId, update.taskId, { title: update.title, description: update.summary, completed: update.completed, nextProjectId: update.nextProjectId, tag: update.tag, }) await refreshAfterAction(update.nextProjectId) }, '任务已更新') } async function handleUpdateProject(update: ProjectSettingsUpdate) { const currentSession = requireSession() await updateProject(currentSession, update.projectId, { name: update.name, identifier: update.identifier, icon: update.icon, background: update.background, description: update.description, }) await loadWorkspaces(currentSession, update.projectId) Message.success('项目设置已更新') } function handleSelectSearchResult(result: SearchResultDTO) { const target = searchResultTarget(result.type) if (!target) { Message.warning('该搜索结果暂不支持导航') return } const owningWorkspace = workspaces.find((workspace) => workspace.project.id === result.projectId) if (!owningWorkspace) { if (isAuthorizedExternalPreview(result)) { setSearchResultPreview(result) } else { Message.warning('该搜索结果所在项目当前不可访问') } return } setActiveProjectID(owningWorkspace.project.id) setActiveView('project') setActiveChannel(target.channel) setActiveTaskID(target.openTask ? result.id : null) if (result.type === 'note') setSelectedItem(result.title) } return (
{screen === 'login' ? ( ) : activeWorkspace && session ? ( setActiveView('workspace')} onSelectWorkspaceExplore={() => setActiveView('workspace-explore')} onSelectProject={(project) => { setActiveProjectID(project.id) setActiveView('project') setActiveChannel('overview') setActiveTaskID(null) }} onSelectChannel={(channel) => { setActiveChannel(channel) setActiveTaskID(null) }} onSelectItem={setSelectedItem} onOpenTask={openTask} onCloseTask={() => setActiveTaskID(null)} onToggleTheme={() => setTheme(dark ? 'light' : 'dark')} onCreateProject={() => setActiveModal('project')} onCreateTask={() => setActiveModal('task')} onUploadSource={() => setActiveModal('source')} onCreateCronPlan={() => setActiveModal('cron')} onUpdateWorkspaceTask={handleUpdateWorkspaceTask} onUpdateProject={handleUpdateProject} onCreateProjectTag={handleCreateProjectTag} searchQuery={workspaceSearch.query} searchLoading={workspaceSearch.loading} searchSearched={workspaceSearch.searched} searchResults={workspaceSearch.results} onSearchQueryChange={workspaceSearch.onQueryChange} onSearch={() => void workspaceSearch.onSearch()} onSelectSearchResult={handleSelectSearchResult} /> ) : ( )} setActiveModal(null)} onCreateProject={handleCreateProject} onCreateTask={handleCreateTask} onUploadSource={handleUploadSource} onCreateCronPlan={handleCreateCronPlan} tagOptions={activeTagOptions} /> setSearchResultPreview(null)} />
) } function searchResultTarget(type: string): { channel: ChannelKey; openTask: boolean } | null { if (type === 'project') return { channel: 'overview', openTask: false } if (type === 'task') return { channel: 'tasks', openTask: true } if (type === 'note') return { channel: 'notes', openTask: false } return null } function isAuthorizedExternalPreview(result: SearchResultDTO) { return ( (result.type === 'task' || result.type === 'note') && typeof result.projectId === 'string' && result.projectId.trim() !== '' && typeof result.title === 'string' && result.title.trim() !== '' && typeof result.snippet === 'string' ) } export default App