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, createTask, fetchProjectWorkspace, fetchProjects, uploadSource, } from '../api/projects' import { LoginPage } from '../pages/login' import { ProjectActionModals, type CronDraft, type ProjectActionModal, type ProjectDraft, type SourceDraft, type TaskDraft } from '../pages/projects/project-action-modals' import { ProjectPage } from '../pages/workspace-home' import type { ChannelKey, Project, ProjectWorkspace, Screen, Theme, WorkbenchView } from '../pages/projects/project-types' 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('Inbox 待处理(36)') const [loading, setLoading] = useState(false) const [actionLoading, setActionLoading] = useState(false) const [activeModal, setActiveModal] = useState(null) const dark = theme === 'dark' const activeWorkspace = workspaces.find((workspace) => workspace.project.id === activeProjectID) ?? workspaces[0] 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 ?? 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(), description: draft.description.trim() }) const nextProjectID = String(created.ID ?? 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', dueAt: normalizeOptionalTime(draft.dueAt), }) 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 openTask(project: Project, taskID: string) { setActiveProjectID(project.id) setActiveView('project') setActiveChannel('tasks') setActiveTaskID(taskID) } return (
{screen === 'login' ? ( ) : activeWorkspace && session ? ( setActiveView('workspace')} onSelectWorkspaceInbox={() => setActiveView('workspace-inbox')} 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')} /> ) : ( )} setActiveModal(null)} onCreateProject={handleCreateProject} onCreateTask={handleCreateTask} onUploadSource={handleUploadSource} onCreateCronPlan={handleCreateCronPlan} />
) } export default App