@@ -100,21 +102,23 @@ export function App() {
-
-
- {projects.map((project) => )}
-
+
+
+
} onClick={() => cycleProject(-1)} />
} onClick={() => cycleProject(1)} />
} onClick={() => setProjectModalOpen(true)} />
{error ?
setError('')} /> : null}
- {visibleWorkspaces.length ? : }
+ {workspaceMode === 'explore' ? { setProjectId(id); setWorkspaceMode('workbench') }} /> : !projectId ? setProjectModalOpen(true)} onUpload={async (project, file) => { await uploadDocument(session, project, file); await loadWorkspaces() }} /> : visibleWorkspaces.length ? : }
- : null}
workspace.tags.map((tag) => tag.name))} onClose={() => setTaskEditor(null)} onSave={async (draft) => {
if (!taskEditor) return
@@ -140,7 +144,6 @@ export function App() {
function Content({ view, title, workspaces, tasks, onEditTask, onLoadAI, experts }: { view: View; title: string; workspaces: Workspace[]; tasks: TaskWithProject[]; onEditTask: (task: TaskWithProject) => void; onLoadAI: () => void; experts: Expert[] }) {
if (view === 'tasks' || view === 'overview') return
- if (view === 'inbox') return
if (view === 'documents') return
if (view === 'ai') return
return
@@ -164,9 +167,23 @@ function TaskRow({ task, onClick }: { task: TaskWithProject; onClick: () => void
return
}
-function InboxPanel({ title, workspaces }: { title: string; workspaces: Workspace[] }) {
- const items = workspaces.flatMap((workspace) => workspace.inbox.map((item) => ({ ...item, projectName: workspace.project.name })))
- return {items.map((item) => {item.title}{item.summary || item.source}
{item.projectName})}{!items.length && }
+
+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 }) {
+ const fileInput = useRef(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])
+ 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 {statItems.map((item) =>
{item.value}{item.label}
)}
} onClick={onCreateProject}>创建项目} disabled={!uploadProject} loading={uploading} onClick={() => fileInput.current?.click()}>上传文件 { 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) } }} />
最新文件{latestFiles.length}
{latestFiles.map((file) =>
{file.name}{file.extension || file.mimeType || file.kind} · {formatDate(file.updatedAt)}
{file.projectName})}{!latestFiles.length &&
}
+}
+
+function ExplorePanel({ projects, onOpen }: { projects: Project[]; onOpen: (id: string) => void }) {
+ return 探索
项目空间{projects.length} 个项目{projects.map((project) => )}{!projects.length && }
}
function DocumentPanel({ title, workspaces }: { title: string; workspaces: Workspace[] }) {
@@ -182,7 +199,7 @@ function AIPanel({ title, workspaces, experts, onLoad }: { title: string; worksp
function CronPanel({ title, workspaces }: { title: string; workspaces: Workspace[] }) {
const plans = workspaces.flatMap((workspace) => workspace.cronPlans.map((item) => ({ ...item, projectName: workspace.project.name })))
- return {plans.map((item) => {item.title}{item.schedule} · {item.enabled ? '已启用' : '已停用'}
{item.projectName})}{!plans.length && }
+ return {plans.map((item) => {item.title}{item.schedule} · {item.enabled ? '已启用' : '已停用'}
{item.projectName})}{!plans.length && }
}
function Panel({ title, subtitle, children }: { title: string; subtitle: string; children: React.ReactNode }) { return }
diff --git a/apps/desktop/src/api.ts b/apps/desktop/src/api.ts
index 767209f..1e1188e 100644
--- a/apps/desktop/src/api.ts
+++ b/apps/desktop/src/api.ts
@@ -111,6 +111,14 @@ export function updateTask(session: ApiSession, projectId: string, taskId: strin
return apiRequest(`/api/v1/projects/${projectId}/tasks/${taskId}`, { method: 'PATCH', token: session.token, body: input })
}
+export function uploadDocument(session: ApiSession, projectId: string, file: File, parentId = '') {
+ useSessionBase(session)
+ const body = new FormData()
+ body.append('file', file)
+ if (parentId) body.append('parentId', parentId)
+ return apiRequest(`/api/v1/projects/${projectId}/documents/uploads`, { method: 'POST', token: session.token, body })
+}
+
export function listAIExperts(session: ApiSession) {
useSessionBase(session)
return apiRequest('/api/v1/ai-experts', { token: session.token })
diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css
index 78a4d63..e4def1f 100644
--- a/apps/desktop/src/styles.css
+++ b/apps/desktop/src/styles.css
@@ -20,9 +20,11 @@ button { font: inherit; }
.project-pill:hover, .project-pill.active { color: #165dff; background: #e8f3ff; border-color: #d3e7ff; }
.project-pill i { display: grid; place-items: center; width: 19px; height: 19px; border-radius: 6px; color: #fff; font-style: normal; font-size: 9px; font-weight: 700; }
.project-pill span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; font-weight: 600; }
+.project-select { min-width: 0; flex: 1; }.project-select .arco-select-view { border: 0; padding: 0; background: transparent; }.project-select .arco-select-view-value { display: none; }.project-current { width: 100%; max-width: none; justify-content: flex-start; }.project-current > svg { margin-left: auto; color: var(--muted); }
.app-alert { grid-column: 1 / 2; z-index: 2; margin: 8px 12px -46px; align-self: start; }
.app-main { grid-column: 1; grid-row: 3; min-width: 0; overflow: auto; padding: 18px 15px 24px; }
+.app-shell.no-rail .app-main { grid-column: 1 / -1; }
.app-main > .arco-spin { min-height: 100%; }
.content-page { max-width: 650px; margin: 0 auto; }
.content-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
@@ -63,8 +65,10 @@ button { font: inherit; }
.detail-row strong { font-size: 13px; }.detail-row p { margin-top: 3px; color: var(--muted); font-size: 11px; }
.expert-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
.expert-chips span { display: inline-flex; align-items: center; gap: 4px; padding: 4px 7px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface); font-size: 11px; }.expert-chips i { display: grid; place-items: center; width: 17px; height: 17px; border-radius: 50%; font-style: normal; }
+.stat-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-bottom: 13px; }.stat-card { display: grid; gap: 3px; padding: 11px; border: 1px solid var(--line); border-radius: 11px; background: var(--surface); }.stat-card strong { font-size: 22px; line-height: 1; }.stat-card span { color: var(--muted); font-size: 11px; }.stat-card.blue strong { color: #165dff; }.stat-card.orange strong { color: #ff7d00; }.stat-card.green strong { color: #00b42a; }.stat-card.purple strong { color: #722ed1; }
+.quick-workspace-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 18px; }.quick-workspace-actions .arco-select { grid-column: 1 / -1; width: 100%; }.hidden-file { display: none; }.all-projects-home .task-page { margin-top: 4px; }.latest-files { padding: 13px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); }.project-card-grid { display: grid; gap: 9px; }.project-explore-card { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto; align-items: center; gap: 10px; width: 100%; padding: 13px; border: 1px solid var(--line); border-radius: 11px; color: var(--text); background: var(--surface); text-align: left; cursor: pointer; }.project-explore-card:hover { border-color: #9fc9ff; background: #f7faff; }.project-explore-card > i { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 10px; color: #fff; font-style: normal; font-weight: 700; font-size: 11px; }.project-explore-card span { display: grid; min-width: 0; gap: 3px; }.project-explore-card strong, .project-explore-card small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.project-explore-card strong { font-size: 13px; }.project-explore-card small { color: var(--muted); font-size: 11px; }
.login-page { display: grid; min-height: 100vh; place-items: center; padding: 20px; background: #edf2f9; }.login-card { display: grid; width: min(100%, 360px); gap: 12px; padding: 28px; border: 1px solid #e5e6eb; border-radius: 16px; background: #fff; box-shadow: 0 18px 48px rgba(31, 35, 41, .12); }.login-card > img { width: 34px; }.login-card .arco-typography { margin: 0; }.login-card label { display: grid; gap: 5px; color: #4e5969; font-size: 12px; }
.theme-dark .project-strip, .theme-dark .app-header, .theme-dark .task-section, .theme-dark .detail-row, .theme-dark .tag-filter button { color: var(--text); background: var(--surface); }.theme-dark .project-pill { color: var(--muted); }.theme-dark .project-pill.active, .theme-dark .channel-rail button.active { color: #8ac7ff; background: #1d4d77; }.theme-dark .task-row:hover { background: #293847; border-color: #477da7; }.theme-dark .section-label span { color: #8ac7ff; background: #1d4d77; }
-@media (max-width: 360px) { .profile-trigger > span { display: none; }.project-pill { max-width: 90px; }.app-main { padding: 13px 9px 20px; }.task-meta .arco-tag { max-width: 64px; overflow: hidden; }.content-heading h4 { font-size: 18px; } }
+@media (max-width: 360px) { .profile-trigger > span { display: none; }.project-pill { max-width: 90px; }.project-strip { gap: 2px; padding-inline: 5px; }.app-main { padding: 13px 9px 20px; }.task-meta .arco-tag { max-width: 64px; overflow: hidden; }.content-heading h4 { font-size: 18px; }.stat-grid { grid-template-columns: repeat(2, 1fr); } }