chore: rename web client to web_v1
This commit is contained in:
303
apps/web_v1/src/app/App.tsx
Normal file
303
apps/web_v1/src/app/App.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
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,
|
||||
updateTask,
|
||||
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 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'
|
||||
|
||||
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('探索采集')
|
||||
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]
|
||||
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 ?? 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(),
|
||||
identifier: draft.identifier.trim(),
|
||||
icon: draft.icon.trim(),
|
||||
background: draft.background.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',
|
||||
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: Number(update.nextProjectId),
|
||||
tag: update.tag,
|
||||
})
|
||||
await refreshAfterAction(update.nextProjectId)
|
||||
}, '任务已更新')
|
||||
}
|
||||
|
||||
function handleUpdateProject(update: ProjectSettingsUpdate) {
|
||||
setWorkspaces((current) =>
|
||||
current.map((workspace) => {
|
||||
if (workspace.project.id !== update.projectId) return workspace
|
||||
const nextProject = {
|
||||
...workspace.project,
|
||||
name: update.name,
|
||||
identifier: update.identifier,
|
||||
icon: update.icon,
|
||||
background: update.background,
|
||||
description: update.description,
|
||||
short: projectIconLabel(update.icon, update.identifier || update.name.slice(0, 2)),
|
||||
color: projectColor(update.background, workspace.project.color),
|
||||
}
|
||||
return { ...workspace, project: nextProject }
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
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')}
|
||||
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}
|
||||
/>
|
||||
) : (
|
||||
<Spin loading />
|
||||
)}
|
||||
<ProjectActionModals
|
||||
activeModal={activeModal}
|
||||
loading={actionLoading}
|
||||
onClose={() => setActiveModal(null)}
|
||||
onCreateProject={handleCreateProject}
|
||||
onCreateTask={handleCreateTask}
|
||||
onUploadSource={handleUploadSource}
|
||||
onCreateCronPlan={handleCreateCronPlan}
|
||||
tagOptions={activeTagOptions}
|
||||
/>
|
||||
</main>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function projectIconLabel(icon: string, fallback: string) {
|
||||
const trimmed = icon.trim()
|
||||
if (!trimmed) return fallback
|
||||
const chars = Array.from(trimmed)
|
||||
if (chars.length <= 2) return trimmed
|
||||
return chars.slice(0, 2).join('').toUpperCase()
|
||||
}
|
||||
|
||||
function projectColor(background: string, fallback: string) {
|
||||
const trimmed = background.trim()
|
||||
if (trimmed.startsWith('#') || trimmed.startsWith('rgb') || trimmed.startsWith('hsl')) return trimmed
|
||||
return fallback
|
||||
}
|
||||
|
||||
export default App
|
||||
Reference in New Issue
Block a user