231 lines
8.0 KiB
TypeScript
231 lines
8.0 KiB
TypeScript
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<Screen>('login')
|
||
const [theme, setTheme] = useState<Theme>('light')
|
||
const [activeView, setActiveView] = useState<WorkbenchView>('workspace')
|
||
const [session, setSession] = useState<ApiSession | null>(null)
|
||
const [workspaces, setWorkspaces] = useState<ProjectWorkspace[]>([])
|
||
const [activeProjectID, setActiveProjectID] = useState<string>('')
|
||
const [activeChannel, setActiveChannel] = useState<ChannelKey>('overview')
|
||
const [activeTaskID, setActiveTaskID] = useState<string | null>(null)
|
||
const [, setSelectedItem] = useState('Inbox 待处理(36)')
|
||
const [loading, setLoading] = useState(false)
|
||
const [actionLoading, setActionLoading] = useState(false)
|
||
const [activeModal, setActiveModal] = useState<ProjectActionModal>(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<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)
|
||
}
|
||
|
||
return (
|
||
<ConfigProvider>
|
||
<main className={dark ? 'app theme-dark' : 'app'}>
|
||
{screen === 'login' ? (
|
||
<Spin loading={loading} style={{ width: '100%' }}>
|
||
<LoginPage onLogin={handleLogin} />
|
||
</Spin>
|
||
) : activeWorkspace && session ? (
|
||
<ProjectPage
|
||
activeView={activeView}
|
||
activeWorkspace={activeWorkspace}
|
||
workspaces={workspaces}
|
||
activeChannel={activeChannel}
|
||
activeTaskID={activeTaskID}
|
||
theme={theme}
|
||
onSelectWorkspace={() => 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')}
|
||
/>
|
||
) : (
|
||
<Spin loading />
|
||
)}
|
||
<ProjectActionModals
|
||
activeModal={activeModal}
|
||
loading={actionLoading}
|
||
onClose={() => setActiveModal(null)}
|
||
onCreateProject={handleCreateProject}
|
||
onCreateTask={handleCreateTask}
|
||
onUploadSource={handleUploadSource}
|
||
onCreateCronPlan={handleCreateCronPlan}
|
||
/>
|
||
</main>
|
||
</ConfigProvider>
|
||
)
|
||
}
|
||
|
||
export default App
|