feat(desktop): refine project workspace navigation

This commit is contained in:
2026-07-25 23:23:55 +08:00
parent 0cbf6d1311
commit dbf5c33c99
2 changed files with 17 additions and 11 deletions

View File

@@ -13,13 +13,12 @@ import {
const { Text, Title } = Typography
const SESSION_KEY = 'senlin-app-session'
type View = 'overview' | 'tasks' | 'documents' | 'ai' | 'cron'
type View = 'tasks' | 'documents' | 'ai' | 'cron'
type WorkspaceMode = 'workbench' | 'explore'
type SavedSession = ApiSession & { user?: CurrentUser }
type TaskWithProject = WorkspaceTask & { projectName: string }
const navItems: Array<{ id: View; label: string; icon: React.ReactNode }> = [
{ id: 'overview', label: '概况', icon: <IconApps /> },
{ id: 'tasks', label: '计划', icon: <IconList /> },
{ id: 'documents', label: '资料', icon: <IconFile /> },
{ id: 'ai', label: 'AI', icon: <IconRobot /> },
@@ -34,7 +33,7 @@ export function App() {
const [workspaces, setWorkspaces] = useState<Record<string, Workspace>>({})
const [projectId, setProjectId] = useState('')
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>('workbench')
const [view, setView] = useState<View>('overview')
const [view, setView] = useState<View>('tasks')
const [dark, setDark] = useState(false)
const [loading, setLoading] = useState(Boolean(stored))
const [error, setError] = useState('')
@@ -80,12 +79,14 @@ export function App() {
const index = projects.findIndex((project) => project.id === projectId)
const next = projectId ? (index + direction + projects.length) % projects.length : direction > 0 ? 0 : projects.length - 1
setProjectId(projects[next]?.id ?? '')
setWorkspaceMode('workbench')
setView('tasks')
}
function logout() {
localStorage.removeItem(SESSION_KEY)
localStorage.removeItem('senlin-mini-session')
setSession(null); setUser(null); setProjects([]); setWorkspaces({}); setProjectId(''); setView('overview')
setSession(null); setUser(null); setProjects([]); setWorkspaces({}); setProjectId(''); setView('tasks')
}
if (!session) return <Login onLogin={(next) => { setSession(next); setUser(next.user); persistSession(next) }} />
@@ -103,7 +104,7 @@ export function App() {
<section className="project-strip" aria-label="项目列表">
<button className={workspaceMode === 'workbench' ? 'project-pill active' : 'project-pill'} onClick={() => setWorkspaceMode('workbench')}><IconApps /><span></span></button>
<button className={workspaceMode === 'explore' ? 'project-pill active' : 'project-pill'} onClick={() => setWorkspaceMode('explore')}><IconCompass /><span></span></button>
<Select className="project-select" value={projectId || 'all'} onChange={(value) => { setProjectId(value === 'all' ? '' : value); setWorkspaceMode('workbench') }} triggerElement={() => <button className="project-pill project-current" aria-label={`切换项目,当前为${title}`}><i style={{ background: activeProject?.background || '#165DFF' }}>{activeProject ? projectLabel(activeProject) : <IconFolder />}</i><span>{title}</span><IconDown className="project-dropdown-icon" /></button>}>
<Select className="project-select" value={projectId || 'all'} onChange={(value) => { setProjectId(value === 'all' ? '' : value); setWorkspaceMode('workbench'); setView('tasks') }} triggerElement={() => <button className={`project-pill project-current ${projectId ? 'active' : ''}`} aria-label={`切换项目,当前为${title}`}><i style={{ background: activeProject?.background || '#165DFF' }}>{activeProject ? projectLabel(activeProject) : <IconFolder />}</i><span>{title}</span><IconDown className="project-dropdown-icon" /></button>}>
<Select.Option value="all"></Select.Option>
{projects.map((project) => <Select.Option key={project.id} value={project.id}>{project.name}</Select.Option>)}
</Select>
@@ -142,10 +143,12 @@ 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 <TaskBoard title={view === 'overview' ? '待办任务' : '工作计划'} subtitle={title} tasks={tasks} onEditTask={onEditTask} />
if (view === 'documents') return <DocumentPanel title={title} workspaces={workspaces} />
if (view === 'ai') return <AIPanel title={title} workspaces={workspaces} experts={experts} onLoad={onLoadAI} />
return <CronPanel title={title} workspaces={workspaces} />
let panel: React.ReactNode
if (view === 'tasks') panel = <TaskBoard title="工作计划" subtitle="" tasks={tasks} onEditTask={onEditTask} />
else if (view === 'documents') panel = <DocumentPanel title="" workspaces={workspaces} />
else if (view === 'ai') panel = <AIPanel title="" workspaces={workspaces} experts={experts} onLoad={onLoadAI} />
else panel = <CronPanel title="" workspaces={workspaces} />
return <section className="project-content"><header className="project-page-header"><Title heading={3}>{title}</Title></header>{panel}</section>
}
function TaskBoard({ title, subtitle, tasks, onEditTask, showCompleted = true }: { title: string; subtitle: string; tasks: TaskWithProject[]; onEditTask: (task: TaskWithProject) => void; showCompleted?: boolean }) {
@@ -219,7 +222,7 @@ function CronPanel({ title, workspaces }: { title: string; workspaces: Workspace
return <Panel title="计划任务" subtitle={title}>{plans.map((item) => <article className="detail-row" key={item.id}><IconClockCircle /><div><strong>{item.title}</strong><p>{item.schedule} · {item.enabled ? '已启用' : '已停用'}</p></div><Tag>{item.projectName}</Tag></article>)}{!plans.length && <Empty description="暂无计划任务" />}</Panel>
}
function Panel({ title, subtitle, children }: { title: string; subtitle: string; children: React.ReactNode }) { return <section className="content-page"><div className="content-heading"><div><Text type="secondary">{subtitle}</Text><Title heading={4}>{title}</Title></div></div><div className="detail-list">{children}</div></section> }
function Panel({ title, subtitle, children }: { title: string; subtitle: string; children: React.ReactNode }) { return <section className="content-page"><div className="content-heading"><div>{subtitle ? <Text type="secondary">{subtitle}</Text> : null}<Title heading={4}>{title}</Title></div></div><div className="detail-list">{children}</div></section> }
function ProfileMenu({ user, onProfile, onLogout }: { user: CurrentUser | null; onProfile: () => void; onLogout: () => void }) {
const menu = <Menu><Menu.Item key="profile" onClick={onProfile}></Menu.Item><Menu.Item key="logout" onClick={onLogout}>退</Menu.Item></Menu>

View File

@@ -27,6 +27,9 @@ button { font: inherit; }
.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; }
.project-content { min-width: 0; }
.project-page-header { width: calc(100% + 30px); margin: -18px -15px 18px; padding: 20px 15px 17px; border-bottom: 1px solid var(--line); background: var(--surface); }
.project-page-header .arco-typography { margin: 0; font-size: 26px; line-height: 1.25; }
.content-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.content-heading .arco-typography { margin: 0; }
.content-heading h4 { margin-top: 2px; font-size: 20px; }
@@ -72,4 +75,4 @@ button { font: inherit; }
.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; }.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); } }
@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; }.project-page-header { width: calc(100% + 18px); margin: -13px -9px 15px; padding: 17px 9px 14px; }.task-meta .arco-tag { max-width: 64px; overflow: hidden; }.content-heading h4 { font-size: 18px; }.stat-grid { grid-template-columns: repeat(2, 1fr); } }