feat(desktop): refine project workspace navigation
This commit is contained in:
@@ -13,13 +13,12 @@ import {
|
|||||||
|
|
||||||
const { Text, Title } = Typography
|
const { Text, Title } = Typography
|
||||||
const SESSION_KEY = 'senlin-app-session'
|
const SESSION_KEY = 'senlin-app-session'
|
||||||
type View = 'overview' | 'tasks' | 'documents' | 'ai' | 'cron'
|
type View = 'tasks' | 'documents' | 'ai' | 'cron'
|
||||||
type WorkspaceMode = 'workbench' | 'explore'
|
type WorkspaceMode = 'workbench' | 'explore'
|
||||||
type SavedSession = ApiSession & { user?: CurrentUser }
|
type SavedSession = ApiSession & { user?: CurrentUser }
|
||||||
type TaskWithProject = WorkspaceTask & { projectName: string }
|
type TaskWithProject = WorkspaceTask & { projectName: string }
|
||||||
|
|
||||||
const navItems: Array<{ id: View; label: string; icon: React.ReactNode }> = [
|
const navItems: Array<{ id: View; label: string; icon: React.ReactNode }> = [
|
||||||
{ id: 'overview', label: '概况', icon: <IconApps /> },
|
|
||||||
{ id: 'tasks', label: '计划', icon: <IconList /> },
|
{ id: 'tasks', label: '计划', icon: <IconList /> },
|
||||||
{ id: 'documents', label: '资料', icon: <IconFile /> },
|
{ id: 'documents', label: '资料', icon: <IconFile /> },
|
||||||
{ id: 'ai', label: 'AI', icon: <IconRobot /> },
|
{ id: 'ai', label: 'AI', icon: <IconRobot /> },
|
||||||
@@ -34,7 +33,7 @@ export function App() {
|
|||||||
const [workspaces, setWorkspaces] = useState<Record<string, Workspace>>({})
|
const [workspaces, setWorkspaces] = useState<Record<string, Workspace>>({})
|
||||||
const [projectId, setProjectId] = useState('')
|
const [projectId, setProjectId] = useState('')
|
||||||
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>('workbench')
|
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>('workbench')
|
||||||
const [view, setView] = useState<View>('overview')
|
const [view, setView] = useState<View>('tasks')
|
||||||
const [dark, setDark] = useState(false)
|
const [dark, setDark] = useState(false)
|
||||||
const [loading, setLoading] = useState(Boolean(stored))
|
const [loading, setLoading] = useState(Boolean(stored))
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
@@ -80,12 +79,14 @@ export function App() {
|
|||||||
const index = projects.findIndex((project) => project.id === projectId)
|
const index = projects.findIndex((project) => project.id === projectId)
|
||||||
const next = projectId ? (index + direction + projects.length) % projects.length : direction > 0 ? 0 : projects.length - 1
|
const next = projectId ? (index + direction + projects.length) % projects.length : direction > 0 ? 0 : projects.length - 1
|
||||||
setProjectId(projects[next]?.id ?? '')
|
setProjectId(projects[next]?.id ?? '')
|
||||||
|
setWorkspaceMode('workbench')
|
||||||
|
setView('tasks')
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
localStorage.removeItem(SESSION_KEY)
|
localStorage.removeItem(SESSION_KEY)
|
||||||
localStorage.removeItem('senlin-mini-session')
|
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) }} />
|
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="项目列表">
|
<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 === '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>
|
<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>
|
<Select.Option value="all">全部项目</Select.Option>
|
||||||
{projects.map((project) => <Select.Option key={project.id} value={project.id}>{project.name}</Select.Option>)}
|
{projects.map((project) => <Select.Option key={project.id} value={project.id}>{project.name}</Select.Option>)}
|
||||||
</Select>
|
</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[] }) {
|
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} />
|
let panel: React.ReactNode
|
||||||
if (view === 'documents') return <DocumentPanel title={title} workspaces={workspaces} />
|
if (view === 'tasks') panel = <TaskBoard title="工作计划" subtitle="" tasks={tasks} onEditTask={onEditTask} />
|
||||||
if (view === 'ai') return <AIPanel title={title} workspaces={workspaces} experts={experts} onLoad={onLoadAI} />
|
else if (view === 'documents') panel = <DocumentPanel title="" workspaces={workspaces} />
|
||||||
return <CronPanel title={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 }) {
|
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>
|
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 }) {
|
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>
|
const menu = <Menu><Menu.Item key="profile" onClick={onProfile}>修改资料</Menu.Item><Menu.Item key="logout" onClick={onLogout}>退出登录</Menu.Item></Menu>
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ button { font: inherit; }
|
|||||||
.app-shell.no-rail .app-main { grid-column: 1 / -1; }
|
.app-shell.no-rail .app-main { grid-column: 1 / -1; }
|
||||||
.app-main > .arco-spin { min-height: 100%; }
|
.app-main > .arco-spin { min-height: 100%; }
|
||||||
.content-page { max-width: 650px; margin: 0 auto; }
|
.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 { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
||||||
.content-heading .arco-typography { margin: 0; }
|
.content-heading .arco-typography { margin: 0; }
|
||||||
.content-heading h4 { margin-top: 2px; font-size: 20px; }
|
.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; }
|
.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; }
|
.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); } }
|
||||||
|
|||||||
Reference in New Issue
Block a user