feat(web): add responsive workbench navigation
This commit is contained in:
@@ -231,6 +231,10 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.mobile-nav-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dock-icon {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
@@ -1898,6 +1902,13 @@
|
||||
|
||||
.status-user {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.status-profile {
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.86);
|
||||
@@ -1905,10 +1916,22 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-user:hover .status-name {
|
||||
.status-profile:hover .status-name {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-grid;
|
||||
flex: 0 0 24px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-tasks {
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -2046,8 +2069,77 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.workbench-shell {
|
||||
min-width: 1180px;
|
||||
@media (max-width: 767px) {
|
||||
.topbar {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--space-2);
|
||||
padding: 0 var(--space-3);
|
||||
}
|
||||
|
||||
.topbar .global-search {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.topbar-actions .arco-space-item:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-nav-button {
|
||||
display: inline-flex;
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.workbench-main {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.project-rail,
|
||||
.channel-sidebar {
|
||||
position: fixed !important;
|
||||
top: 58px;
|
||||
bottom: 32px;
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
height: auto !important;
|
||||
max-width: calc(100vw - 48px);
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 160ms ease, visibility 160ms ease;
|
||||
}
|
||||
|
||||
.project-rail.open,
|
||||
.channel-sidebar.open {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.nav-backdrop {
|
||||
position: fixed;
|
||||
inset: 58px 0 32px;
|
||||
z-index: 19;
|
||||
border: 0;
|
||||
background: rgba(29, 33, 41, 0.38);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.stage {
|
||||
width: 100%;
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.statusbar {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
padding: 0 var(--space-3);
|
||||
}
|
||||
|
||||
.status-system {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ export function ProjectRail({
|
||||
activeProject,
|
||||
projects,
|
||||
activeView,
|
||||
open,
|
||||
onClose,
|
||||
onSelectWorkspace,
|
||||
onSelectWorkspaceExplore,
|
||||
onSelectProject,
|
||||
@@ -17,27 +19,34 @@ export function ProjectRail({
|
||||
activeProject: Project
|
||||
projects: Project[]
|
||||
activeView: WorkbenchView
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onSelectWorkspace: () => void
|
||||
onSelectWorkspaceExplore: () => void
|
||||
onSelectProject: (project: Project) => void
|
||||
onCreateProject: () => void
|
||||
}) {
|
||||
const selectAndClose = (action: () => void) => {
|
||||
action()
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Sider className="project-rail" width={96}>
|
||||
<button className="rail-brand" aria-label="森林AI 工作台" title="森林AI 工作台" onClick={onSelectWorkspace}>
|
||||
<Sider className={open ? 'project-rail open' : 'project-rail'} width={96}>
|
||||
<button className="rail-brand" aria-label="森林AI 工作台" title="森林AI 工作台" onClick={() => selectAndClose(onSelectWorkspace)}>
|
||||
<img className="brand-icon" src="/senlinai-icon.svg" alt="" />
|
||||
</button>
|
||||
<Button
|
||||
className={activeView === 'workspace' ? 'dashboard-button active' : 'dashboard-button'}
|
||||
icon={<IconDashboard />}
|
||||
onClick={onSelectWorkspace}
|
||||
onClick={() => selectAndClose(onSelectWorkspace)}
|
||||
aria-label="工作台"
|
||||
title="工作台"
|
||||
/>
|
||||
<Button
|
||||
className={activeView === 'workspace-explore' ? 'dashboard-button active' : 'dashboard-button'}
|
||||
icon={<IconCompass />}
|
||||
onClick={onSelectWorkspaceExplore}
|
||||
onClick={() => selectAndClose(onSelectWorkspaceExplore)}
|
||||
aria-label="探索"
|
||||
title="探索"
|
||||
/>
|
||||
@@ -46,7 +55,7 @@ export function ProjectRail({
|
||||
<Badge key={project.id} count={project.badge} dot={false} className={project.urgent ? 'project-badge urgent' : 'project-badge'}>
|
||||
<button
|
||||
className={activeView === 'project' && activeProject.id === project.id ? 'project-button active' : 'project-button'}
|
||||
onClick={() => onSelectProject(project)}
|
||||
onClick={() => selectAndClose(() => onSelectProject(project))}
|
||||
style={{ '--project-color': project.color } as CSSProperties}
|
||||
title={project.name}
|
||||
>
|
||||
@@ -55,7 +64,7 @@ export function ProjectRail({
|
||||
</Badge>
|
||||
))}
|
||||
</Space>
|
||||
<Button className="create-project" aria-label="新建项目" icon={<IconPlus />} title="新建项目" onClick={onCreateProject} />
|
||||
<Button className="create-project" aria-label="新建项目" icon={<IconPlus />} title="新建项目" onClick={() => selectAndClose(onCreateProject)} />
|
||||
</Sider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ export function ProjectSidebar({
|
||||
activeView,
|
||||
activeWorkspace,
|
||||
activeChannel,
|
||||
open,
|
||||
onClose,
|
||||
onSelectChannel,
|
||||
onSelectItem,
|
||||
onUpdateProject,
|
||||
@@ -27,12 +29,18 @@ export function ProjectSidebar({
|
||||
activeView: WorkbenchView
|
||||
activeWorkspace: ProjectWorkspace
|
||||
activeChannel: ChannelKey
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onSelectChannel: (key: ChannelKey) => void
|
||||
onSelectItem: (title: string) => void
|
||||
onUpdateProject: (update: ProjectSettingsUpdate) => void
|
||||
}) {
|
||||
const workspaceMode = activeView === 'workspace'
|
||||
const channels = activeWorkspace.channels
|
||||
const selectChannelAndClose = (key: ChannelKey) => {
|
||||
onSelectChannel(key)
|
||||
onClose()
|
||||
}
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
const [draft, setDraft] = useState({
|
||||
name: activeWorkspace.project.name,
|
||||
@@ -55,7 +63,7 @@ export function ProjectSidebar({
|
||||
|
||||
const projectIcon = activeWorkspace.project.short || activeWorkspace.project.identifier || activeWorkspace.project.name.slice(0, 2)
|
||||
return (
|
||||
<Sider className="channel-sidebar" width={248}>
|
||||
<Sider className={open ? 'channel-sidebar open' : 'channel-sidebar'} width={248}>
|
||||
<div className="project-title">
|
||||
<Space>
|
||||
{workspaceMode ? <IconApps /> : <span className="project-title-icon">{projectIcon}</span>}
|
||||
@@ -79,13 +87,13 @@ export function ProjectSidebar({
|
||||
) : (
|
||||
<>
|
||||
{channels.flatMap((channel) => {
|
||||
const items = [renderChannelButton(channel, activeChannel, onSelectChannel)]
|
||||
const items = [renderChannelButton(channel, activeChannel, selectChannelAndClose)]
|
||||
if (channel.key === 'cron') {
|
||||
items.push(
|
||||
<button
|
||||
key="new-channel"
|
||||
className={activeChannel === 'new-channel' ? 'channel-button active' : 'channel-button'}
|
||||
onClick={() => onSelectChannel('new-channel')}
|
||||
onClick={() => selectChannelAndClose('new-channel')}
|
||||
>
|
||||
<span className="channel-left"><IconPlus /><Text>新建频道</Text></span>
|
||||
<IconMore />
|
||||
@@ -102,7 +110,10 @@ export function ProjectSidebar({
|
||||
<section className="recent-sessions">
|
||||
<Text className="section-title">最近会话</Text>
|
||||
{activeWorkspace.recentSessions.slice(0, 4).map((session) => (
|
||||
<button key={session.id} onClick={() => onSelectItem(session.title)}>
|
||||
<button key={session.id} onClick={() => {
|
||||
onSelectItem(session.title)
|
||||
onClose()
|
||||
}}>
|
||||
<Text>{session.title}</Text>
|
||||
<Text type="secondary">{session.time}</Text>
|
||||
</button>
|
||||
|
||||
@@ -25,17 +25,13 @@ export function ProjectStatusbar() {
|
||||
return (
|
||||
<>
|
||||
<Footer className="statusbar">
|
||||
<button className="status-user" type="button" onClick={() => setProfileOpen(true)}>
|
||||
<Avatar size={24} style={{ backgroundColor: 'var(--color-primary)' }}>张</Avatar>
|
||||
<span className="status-identity">
|
||||
<Text className="status-name">张明</Text>
|
||||
<span className="status-online-dot" aria-label="在线" title="在线" />
|
||||
</span>
|
||||
<Button className="status-upgrade" size="mini" type="primary" onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setUpgradeOpen(true)
|
||||
}}>升级</Button>
|
||||
</button>
|
||||
<div className="status-user">
|
||||
<button className="status-profile" type="button" onClick={() => setProfileOpen(true)}>
|
||||
<span className="status-avatar" aria-hidden="true">张</span>
|
||||
<span className="status-name">张明</span>
|
||||
</button>
|
||||
<Button className="status-upgrade" size="mini" type="primary" onClick={() => setUpgradeOpen(true)}>升级</Button>
|
||||
</div>
|
||||
|
||||
<Space className="status-system" size={18}>
|
||||
<span><IconInteractionFallback /> 3 个计划进行中</span>
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import { Button, Input, Layout, Space, Typography } from '@arco-design/web-react'
|
||||
import { IconApps, IconMoon, IconSearch, IconSun } from '@arco-design/web-react/icon'
|
||||
import { IconApps, IconMenuUnfold, IconMoon, IconSearch, IconSun } from '@arco-design/web-react/icon'
|
||||
import type { Theme } from './project-types'
|
||||
|
||||
const { Header } = Layout
|
||||
const { Text } = Typography
|
||||
|
||||
export function ProjectTopbar({ theme, onToggleTheme }: { theme: Theme; onToggleTheme: () => void }) {
|
||||
export function ProjectTopbar({
|
||||
theme,
|
||||
onToggleTheme,
|
||||
showChannels,
|
||||
onOpenProjects,
|
||||
onOpenChannels,
|
||||
}: {
|
||||
theme: Theme
|
||||
onToggleTheme: () => void
|
||||
showChannels: boolean
|
||||
onOpenProjects: () => void
|
||||
onOpenChannels: () => void
|
||||
}) {
|
||||
const desktop = isDesktopRuntime()
|
||||
|
||||
return (
|
||||
@@ -19,6 +31,10 @@ export function ProjectTopbar({ theme, onToggleTheme }: { theme: Theme; onToggle
|
||||
<Button size="large" type="primary">搜索</Button>
|
||||
</div>
|
||||
<Space className="topbar-actions">
|
||||
<Button className="mobile-nav-button" aria-label="打开项目导航" icon={<IconApps />} onClick={onOpenProjects} />
|
||||
{showChannels && (
|
||||
<Button className="mobile-nav-button" aria-label="打开频道导航" icon={<IconMenuUnfold />} onClick={onOpenChannels} />
|
||||
)}
|
||||
<Button aria-label={theme === 'dark' ? '切换浅色模式' : '切换深色模式'} icon={theme === 'dark' ? <IconSun /> : <IconMoon />} onClick={onToggleTheme} />
|
||||
{desktop && (
|
||||
<>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { Layout } from '@arco-design/web-react'
|
||||
import { WorkspacePage, type WorkspaceTaskUpdate } from './workspace-body'
|
||||
import { WorkspaceExplorePage } from './workspace-explore'
|
||||
@@ -57,6 +58,7 @@ export function ProjectPage({
|
||||
onCreateProjectTag: (name: string) => void
|
||||
}) {
|
||||
const isProject = activeView === 'project'
|
||||
const [navOpen, setNavOpen] = useState<'projects' | 'channels' | null>(null)
|
||||
const projects = workspaces.map((workspace) => workspace.project)
|
||||
const updateActiveProjectTask = (update: ProjectTaskUpdate) => {
|
||||
onUpdateWorkspaceTask({
|
||||
@@ -72,13 +74,21 @@ export function ProjectPage({
|
||||
|
||||
return (
|
||||
<Layout className="workbench-shell">
|
||||
<ProjectTopbar theme={theme} onToggleTheme={onToggleTheme} />
|
||||
<ProjectTopbar
|
||||
theme={theme}
|
||||
onToggleTheme={onToggleTheme}
|
||||
showChannels={isProject}
|
||||
onOpenProjects={() => setNavOpen('projects')}
|
||||
onOpenChannels={() => setNavOpen('channels')}
|
||||
/>
|
||||
|
||||
<Layout className="workbench-main" hasSider>
|
||||
<ProjectRail
|
||||
activeProject={activeWorkspace.project}
|
||||
projects={projects}
|
||||
activeView={activeView}
|
||||
open={navOpen === 'projects'}
|
||||
onClose={() => setNavOpen(null)}
|
||||
onSelectWorkspace={onSelectWorkspace}
|
||||
onSelectWorkspaceExplore={onSelectWorkspaceExplore}
|
||||
onSelectProject={onSelectProject}
|
||||
@@ -90,12 +100,18 @@ export function ProjectPage({
|
||||
activeView={activeView}
|
||||
activeWorkspace={activeWorkspace}
|
||||
activeChannel={activeChannel}
|
||||
open={navOpen === 'channels'}
|
||||
onClose={() => setNavOpen(null)}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onSelectItem={onSelectItem}
|
||||
onUpdateProject={onUpdateProject}
|
||||
/>
|
||||
)}
|
||||
|
||||
{navOpen && (
|
||||
<button className="nav-backdrop" type="button" aria-label="关闭导航" onClick={() => setNavOpen(null)} />
|
||||
)}
|
||||
|
||||
<Content className="stage">
|
||||
{activeView === 'workspace' ? (
|
||||
<WorkspacePage workspaces={workspaces} onOpenTask={onOpenTask} onUpdateTask={onUpdateWorkspaceTask} />
|
||||
|
||||
Reference in New Issue
Block a user