Files
agent/apps/senlinai-acro-react/src/app/App.tsx

231 lines
8.0 KiB
TypeScript
Raw Normal View History

2026-07-20 10:50:42 +08:00
import { useState } from 'react'
import { ConfigProvider, Message, Spin } from '@arco-design/web-react'
2026-07-20 10:50:42 +08:00
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'
2026-07-20 10:50:42 +08:00
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'
2026-07-20 10:50:42 +08:00
function App() {
const [screen, setScreen] = useState<Screen>('login')
const [theme, setTheme] = useState<Theme>('light')
2026-07-20 11:40:45 +08:00
const [activeView, setActiveView] = useState<WorkbenchView>('workspace')
const [session, setSession] = useState<ApiSession | null>(null)
const [workspaces, setWorkspaces] = useState<ProjectWorkspace[]>([])
const [activeProjectID, setActiveProjectID] = useState<string>('')
2026-07-20 10:50:42 +08:00
const [activeChannel, setActiveChannel] = useState<ChannelKey>('overview')
const [activeTaskID, setActiveTaskID] = useState<string | null>(null)
2026-07-20 12:45:13 +08:00
const [, setSelectedItem] = useState('Inbox 待处理36')
const [loading, setLoading] = useState(false)
const [actionLoading, setActionLoading] = useState(false)
const [activeModal, setActiveModal] = useState<ProjectActionModal>(null)
2026-07-20 10:50:42 +08:00
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)
}
}
2026-07-20 10:50:42 +08:00
async function refreshAfterAction(nextProjectID = activeProjectID) {
if (!session) return
await loadWorkspaces(session, nextProjectID)
}
async function runAction(action: () => Promise<void>, 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)
}
2026-07-20 10:50:42 +08:00
return (
<ConfigProvider>
<main className={dark ? 'app theme-dark' : 'app'}>
{screen === 'login' ? (
<Spin loading={loading} style={{ width: '100%' }}>
<LoginPage onLogin={handleLogin} />
</Spin>
) : activeWorkspace && session ? (
2026-07-20 10:50:42 +08:00
<ProjectPage
2026-07-20 11:40:45 +08:00
activeView={activeView}
activeWorkspace={activeWorkspace}
workspaces={workspaces}
2026-07-20 10:50:42 +08:00
activeChannel={activeChannel}
activeTaskID={activeTaskID}
2026-07-20 10:50:42 +08:00
theme={theme}
2026-07-20 11:40:45 +08:00
onSelectWorkspace={() => setActiveView('workspace')}
onSelectWorkspaceInbox={() => setActiveView('workspace-inbox')}
2026-07-20 11:40:45 +08:00
onSelectProject={(project) => {
setActiveProjectID(project.id)
2026-07-20 11:40:45 +08:00
setActiveView('project')
setActiveChannel('overview')
setActiveTaskID(null)
}}
onSelectChannel={(channel) => {
setActiveChannel(channel)
setActiveTaskID(null)
2026-07-20 11:40:45 +08:00
}}
2026-07-20 10:50:42 +08:00
onSelectItem={setSelectedItem}
onOpenTask={openTask}
onCloseTask={() => setActiveTaskID(null)}
2026-07-20 10:50:42 +08:00
onToggleTheme={() => setTheme(dark ? 'light' : 'dark')}
onCreateProject={() => setActiveModal('project')}
onCreateTask={() => setActiveModal('task')}
onUploadSource={() => setActiveModal('source')}
onCreateCronPlan={() => setActiveModal('cron')}
2026-07-20 10:50:42 +08:00
/>
) : (
<Spin loading />
2026-07-20 10:50:42 +08:00
)}
<ProjectActionModals
activeModal={activeModal}
loading={actionLoading}
onClose={() => setActiveModal(null)}
onCreateProject={handleCreateProject}
onCreateTask={handleCreateTask}
onUploadSource={handleUploadSource}
onCreateCronPlan={handleCreateCronPlan}
/>
2026-07-20 10:50:42 +08:00
</main>
</ConfigProvider>
)
}
export default App