|
|
|
|
@@ -1,5 +1,5 @@
|
|
|
|
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
|
|
|
import { Alert, Avatar, Button, Dropdown, Empty, Form, Input, Menu, Message, Modal, Select, Spin, Switch, Tag, Typography } from '@arco-design/web-react'
|
|
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
|
|
|
import { Alert, Avatar, Button, Dropdown, Empty, Form, Input, Menu, Modal, Select, Spin, Switch, Tag, Typography } from '@arco-design/web-react'
|
|
|
|
|
import {
|
|
|
|
|
IconApps, IconCheckCircle, IconCheckCircleFill, IconClockCircle, IconCompass, IconDown, IconFile,
|
|
|
|
|
IconFolder, IconLeft, IconList, IconMoon, IconPlus, IconRight, IconRobot,
|
|
|
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
|
|
import {
|
|
|
|
|
ApiError, createProject, fetchProjectWorkspace, fetchProjects, getCurrentUser,
|
|
|
|
|
listAIExperts, login, setApiBaseUrl, updateCurrentUser, updateTask,
|
|
|
|
|
uploadDocument,
|
|
|
|
|
type ApiSession, type CurrentUser, type Expert, type Project, type Workspace, type WorkspaceTask,
|
|
|
|
|
} from './api'
|
|
|
|
|
|
|
|
|
|
@@ -114,7 +113,7 @@ export function App() {
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{error ? <Alert className="app-alert" type="error" content={error} closable onClose={() => setError('')} /> : null}
|
|
|
|
|
<main className="app-main"><Spin loading={loading} block>{workspaceMode === 'explore' ? <ExplorePanel projects={projects} onOpen={(id) => { setProjectId(id); setWorkspaceMode('workbench') }} /> : !projectId ? <AllProjectsHome projects={projects} workspaces={visibleWorkspaces} tasks={tasks} onEditTask={setTaskEditor} onCreateProject={() => setProjectModalOpen(true)} onUpload={async (project, file) => { await uploadDocument(session, project, file); await loadWorkspaces() }} /> : visibleWorkspaces.length ? <Content view={view} title={title} workspaces={visibleWorkspaces} tasks={tasks} onEditTask={setTaskEditor} onLoadAI={loadAI} experts={experts} /> : <Empty description="项目加载失败,请刷新后重试" />}</Spin></main>
|
|
|
|
|
<main className="app-main"><Spin loading={loading} block>{workspaceMode === 'explore' ? <ExplorePanel projects={projects} onOpen={(id) => { setProjectId(id); setWorkspaceMode('workbench') }} /> : !projectId ? <AllProjectsHome projects={projects} workspaces={visibleWorkspaces} tasks={tasks} onEditTask={setTaskEditor} onCreateProject={() => setProjectModalOpen(true)} /> : visibleWorkspaces.length ? <Content view={view} title={title} workspaces={visibleWorkspaces} tasks={tasks} onEditTask={setTaskEditor} onLoadAI={loadAI} experts={experts} /> : <Empty description="项目加载失败,请刷新后重试" />}</Spin></main>
|
|
|
|
|
|
|
|
|
|
{projectId && workspaceMode === 'workbench' ? <aside className="channel-rail" aria-label="项目子菜单">
|
|
|
|
|
{navItems.map((item) => <button key={item.id} title={item.label} className={view === item.id ? 'active' : ''} onClick={() => { setView(item.id); if (item.id === 'ai') void loadAI() }}>{item.icon}<span>{item.label}</span></button>)}
|
|
|
|
|
@@ -149,17 +148,17 @@ function Content({ view, title, workspaces, tasks, onEditTask, onLoadAI, experts
|
|
|
|
|
return <CronPanel title={title} workspaces={workspaces} />
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function TaskBoard({ title, subtitle, tasks, onEditTask }: { title: string; subtitle: string; tasks: TaskWithProject[]; onEditTask: (task: TaskWithProject) => void }) {
|
|
|
|
|
function TaskBoard({ title, subtitle, tasks, onEditTask, showCompleted = true }: { title: string; subtitle: string; tasks: TaskWithProject[]; onEditTask: (task: TaskWithProject) => void; showCompleted?: boolean }) {
|
|
|
|
|
const [tag, setTag] = useState('全部')
|
|
|
|
|
const tags = [...new Set(tasks.map((task) => task.tag).filter(Boolean))]
|
|
|
|
|
const visible = tag === '全部' ? tasks : tasks.filter((task) => task.tag === tag)
|
|
|
|
|
const pending = visible.filter((task) => !task.completed)
|
|
|
|
|
const completed = visible.filter((task) => task.completed)
|
|
|
|
|
return <section className="content-page task-page">
|
|
|
|
|
<div className="content-heading"><div><Text type="secondary">{subtitle}</Text><Title heading={4}>{title}</Title></div><Tag color="arcoblue">{pending.length} 项待办</Tag></div>
|
|
|
|
|
<div className="content-heading"><div>{subtitle ? <Text type="secondary">{subtitle}</Text> : null}<Title heading={4}>{title}</Title></div><Tag color="arcoblue">{pending.length} 项待办</Tag></div>
|
|
|
|
|
<div className="tag-filter"><button className={tag === '全部' ? 'active' : ''} onClick={() => setTag('全部')}>全部</button>{tags.map((item) => <button key={item} className={tag === item ? 'active' : ''} onClick={() => setTag(item)}>{item}</button>)}</div>
|
|
|
|
|
<section className="task-section"><div className="section-label"><strong>待办任务</strong><span>{pending.length}</span></div><div className="task-list">{pending.map((task) => <TaskRow key={task.id} task={task} onClick={() => onEditTask(task)} />)}{!pending.length && <Empty description="当前没有待办任务" />}</div></section>
|
|
|
|
|
<section className="task-section completed-section"><div className="section-label"><strong>已完成</strong><span>{completed.length}</span></div><div className="task-list">{completed.map((task) => <TaskRow key={task.id} task={task} onClick={() => onEditTask(task)} />)}{!completed.length && <Empty description="尚未完成任何任务" />}</div></section>
|
|
|
|
|
{showCompleted ? <section className="task-section completed-section"><div className="section-label"><strong>已完成</strong><span>{completed.length}</span></div><div className="task-list">{completed.map((task) => <TaskRow key={task.id} task={task} onClick={() => onEditTask(task)} />)}{!completed.length && <Empty description="尚未完成任何任务" />}</div></section> : null}
|
|
|
|
|
</section>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -168,18 +167,14 @@ function TaskRow({ task, onClick }: { task: TaskWithProject; onClick: () => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function AllProjectsHome({ projects, workspaces, tasks, onEditTask, onCreateProject, onUpload }: { projects: Project[]; workspaces: Workspace[]; tasks: TaskWithProject[]; onEditTask: (task: TaskWithProject) => void; onCreateProject: () => void; onUpload: (projectId: string, file: File) => Promise<void> }) {
|
|
|
|
|
const fileInput = useRef<HTMLInputElement>(null)
|
|
|
|
|
const [uploadProject, setUploadProject] = useState(projects[0]?.id ?? '')
|
|
|
|
|
const [uploading, setUploading] = useState(false)
|
|
|
|
|
useEffect(() => { if (!projects.some((project) => project.id === uploadProject)) setUploadProject(projects[0]?.id ?? '') }, [projects, uploadProject])
|
|
|
|
|
function AllProjectsHome({ projects, workspaces, tasks, onEditTask, onCreateProject }: { projects: Project[]; workspaces: Workspace[]; tasks: TaskWithProject[]; onEditTask: (task: TaskWithProject) => void; onCreateProject: () => void }) {
|
|
|
|
|
const pending = tasks.filter((task) => !task.completed)
|
|
|
|
|
const latestFiles = workspaces.flatMap((workspace) => workspace.documents.map((document) => ({ ...document, projectName: workspace.project.name }))).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, 6)
|
|
|
|
|
const statItems = [
|
|
|
|
|
{ label: '项目', value: projects.length, color: 'blue' }, { label: '待办', value: pending.length, color: 'orange' },
|
|
|
|
|
{ label: '文件', value: latestFiles.length, color: 'green' }, { label: '计划任务', value: workspaces.reduce((total, workspace) => total + workspace.cronPlans.length, 0), color: 'purple' },
|
|
|
|
|
]
|
|
|
|
|
return <section className="content-page all-projects-home"><div className="content-heading"><div><Text type="secondary">工作台</Text><Title heading={4}>全部项目</Title></div></div><div className="stat-grid">{statItems.map((item) => <div className={`stat-card ${item.color}`} key={item.label}><strong>{item.value}</strong><span>{item.label}</span></div>)}</div><div className="quick-workspace-actions"><Button type="primary" icon={<IconPlus />} onClick={onCreateProject}>创建项目</Button><Button icon={<IconFile />} disabled={!uploadProject} loading={uploading} onClick={() => fileInput.current?.click()}>上传文件</Button><Select value={uploadProject || undefined} placeholder="选择上传项目" onChange={setUploadProject}>{projects.map((project) => <Select.Option key={project.id} value={project.id}>{project.name}</Select.Option>)}</Select><input ref={fileInput} className="hidden-file" type="file" onChange={async (event) => { const file = event.target.files?.[0]; if (!file || !uploadProject) return; setUploading(true); try { await onUpload(uploadProject, file); Message.success('文件已上传') } catch (error) { Message.error(errorMessage(error)) } finally { event.target.value = ''; setUploading(false) } }} /></div><TaskBoard title="待办任务" subtitle="全部项目" tasks={tasks} onEditTask={onEditTask} /><section className="latest-files"><div className="section-label"><strong>最新文件</strong><span>{latestFiles.length}</span></div><div className="detail-list">{latestFiles.map((file) => <article className="detail-row" key={file.id}><IconFile /><div><strong>{file.name}</strong><p>{file.extension || file.mimeType || file.kind} · {formatDate(file.updatedAt)}</p></div><Tag>{file.projectName}</Tag></article>)}{!latestFiles.length && <Empty description="暂无文件,上传一份项目资料开始吧" />}</div></section></section>
|
|
|
|
|
return <section className="content-page all-projects-home"><div className="content-heading"><div><Title heading={4}>全部项目</Title></div><Button type="primary" icon={<IconPlus />} onClick={onCreateProject}>创建项目</Button></div><div className="stat-grid">{statItems.map((item) => <div className={`stat-card ${item.color}`} key={item.label}><strong>{item.value}</strong><span>{item.label}</span></div>)}</div><TaskBoard title="待办任务" subtitle="" tasks={tasks} onEditTask={onEditTask} showCompleted={false} /><section className="latest-files"><div className="section-label"><strong>最新文件</strong><span>{latestFiles.length}</span></div><div className="detail-list">{latestFiles.map((file) => <article className="detail-row" key={file.id}><IconFile /><div><strong>{file.name}</strong><p>{file.extension || file.mimeType || file.kind} · {formatDate(file.updatedAt)}</p></div><Tag>{file.projectName}</Tag></article>)}{!latestFiles.length && <Empty description="暂无文件" />}</div></section></section>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ExplorePanel({ projects, onOpen }: { projects: Project[]; onOpen: (id: string) => void }) {
|
|
|
|
|
|