From 9566a5336ce3b26c2f47b5e393c61a3df611e7dd Mon Sep 17 00:00:00 2001 From: logan Date: Fri, 18 Sep 2026 13:20:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=BC=E8=88=AA=E9=87=8D=E6=9E=84=EF=BC=9AAp?= =?UTF-8?q?ifox=20=E5=BC=8F=E5=B7=A6=E4=BE=A7=E6=B4=BB=E5=8A=A8=E6=A0=8F?= =?UTF-8?q?=EF=BC=88=E7=A9=BA=E9=97=B4=E5=88=87=E6=8D=A2/=E5=AF=BC?= =?UTF-8?q?=E8=88=AA/=E8=B4=A6=E5=8F=B7=EF=BC=89=EF=BC=8C=E9=A1=B6?= =?UTF-8?q?=E6=A0=8F=E7=98=A6=E8=BA=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ActivityRail:顶部空间切换(本地/个人/团队组织菜单)、接口/历史/测试/Mock 导航、底部文档/主题/设置/账号(含云端同步状态点) - TopBar 仅保留新建/导入/导出 + 环境切换,账号/主题/文档/设置全部移入活动栏 - Sidebar 接口|历史 分段切换改由 store sidebarSide 驱动,与活动栏联动;移除底部导航区 - 900px 窄屏顶栏固定 48px 单行验证通过;53 项测试全绿 --- apps/web/src/styles.css | 42 ++++ packages/ui/src/App.tsx | 8 +- packages/ui/src/components/ActivityRail.tsx | 254 ++++++++++++++++++++ packages/ui/src/components/Sidebar.tsx | 38 +-- packages/ui/src/components/TopBar.tsx | 161 +------------ packages/ui/src/workspaceStore.ts | 8 + 6 files changed, 327 insertions(+), 184 deletions(-) create mode 100644 packages/ui/src/components/ActivityRail.tsx diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index d445bf8..efa863b 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -514,6 +514,48 @@ } .side-seg-item .ops-count { margin-left: 2px; } + /* ---------- 左侧活动栏(空间切换 / 导航 / 账号) ---------- */ + .rail-space { + @apply flex flex-col items-center gap-0.5 rounded-lg px-1 py-1.5 transition; + } + .rail-space:hover { background: var(--panel-2); } + .rail-item { + @apply relative flex w-12 flex-col items-center gap-0.5 rounded-lg py-1.5 transition whitespace-nowrap; + color: var(--text-dim); + } + .rail-item:hover { background: var(--panel-2); color: var(--text); } + .rail-item.active { + background: color-mix(in srgb, var(--accent) 12%, transparent); + color: var(--accent-strong); + } + .rail-item.active::before { + content: ""; + position: absolute; + left: -4px; + top: 20%; + height: 60%; + width: 3px; + border-radius: 2px; + background: var(--accent); + } + .rail-item.rail-login { + background: color-mix(in srgb, var(--accent) 12%, transparent); + color: var(--accent-strong); + } + .rail-sync-dot { + position: absolute; + right: -5px; + bottom: -2px; + width: 8px; + height: 8px; + border-radius: 999px; + border: 2px solid var(--panel); + } + .rail-sync-dot.ok { background: var(--success); } + .rail-sync-dot.error { background: var(--danger); } + .rail-sync-dot.syncing { background: var(--accent); animation: rail-pulse 1s ease-in-out infinite; } + @keyframes rail-pulse { 50% { opacity: 0.35; } } + /* ---------- 方法 chip 着色 ---------- */ .ops-chip.ops-method-GET { background: color-mix(in srgb, var(--m-GET) 14%, transparent); color: var(--m-GET); } .ops-chip.ops-method-POST { background: color-mix(in srgb, var(--m-POST) 16%, transparent); color: var(--m-POST); } diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 88d6aec..b13442d 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -7,6 +7,7 @@ import { closeSession } from "./send"; import { Icon } from "./components/icons"; import { Logo } from "./components/Logo"; import { useWorkspace } from "./workspaceStore"; +import { ActivityRail } from "./components/ActivityRail"; import { Sidebar } from "./components/Sidebar"; import { TestingView } from "./components/TestingView"; import { startScheduler, stopScheduler } from "./scheduler"; @@ -62,8 +63,12 @@ export function OpsxWorkspace() { return (
+
+ {view === "testing" ? ( - +
+ +
) : (
@@ -89,6 +94,7 @@ export function OpsxWorkspace() {
)} +
{dialog?.type === "new" && } {dialog?.type === "import" && } {dialog?.type === "export" && } diff --git a/packages/ui/src/components/ActivityRail.tsx b/packages/ui/src/components/ActivityRail.tsx new file mode 100644 index 0000000..633281d --- /dev/null +++ b/packages/ui/src/components/ActivityRail.tsx @@ -0,0 +1,254 @@ +/** + * 最左活动栏(借鉴 Apifox):空间切换(本地/个人/团队)+ 主导航 + 底部账号与工具。 + * 顶栏因此只保留与当前编辑相关的操作。 + */ +import { useState } from "react"; +import { useWorkspace } from "../workspaceStore"; +import { ContextMenu, cx, type MenuItem } from "./common"; +import { EnvBadge } from "./EnvBadge"; +import { Icon, type IconName } from "./icons"; +import { Logo } from "./Logo"; + +export function ActivityRail() { + const view = useWorkspace((s) => s.view); + const setView = useWorkspace((s) => s.setView); + const sidebarSide = useWorkspace((s) => s.sidebarSide); + const setSidebarSide = useWorkspace((s) => s.setSidebarSide); + const account = useWorkspace((s) => s.account); + const switchSpace = useWorkspace((s) => s.switchSpace); + const authLogout = useWorkspace((s) => s.authLogout); + const syncNow = useWorkspace((s) => s.syncNow); + const setDialog = useWorkspace((s) => s.setDialog); + const theme = useWorkspace((s) => s.settings.theme); + const setTheme = useWorkspace((s) => s.setTheme); + + const [spaceMenu, setSpaceMenu] = useState<{ x: number; y: number } | null>(null); + const [accountMenu, setAccountMenu] = useState<{ x: number; y: number } | null>(null); + + const activeSpace = account.spaces.find((sp) => sp.id === account.activeSpaceId) ?? null; + const spaceName = activeSpace ? `${activeSpace.teamId !== null ? "团队 · " : ""}${activeSpace.name}` : "本地空间"; + + const spaceItems: MenuItem[] = [ + { + label: ( + + + 本地空间 + {account.activeSpaceId === null && ● 当前} + + ), + onClick: () => void switchSpace(null), + }, + ...account.spaces.map((sp) => ({ + label: ( + + + {sp.teamId !== null ? `团队 · ${sp.name}` : sp.name} + {sp.id === account.activeSpaceId ? ( + ● 当前 + ) : ( + + {new Date(sp.updatedAt * 1000).toLocaleDateString("zh-CN")} + + )} + + ), + onClick: () => void switchSpace(sp.id), + })), + { type: "sep" as const }, + { + label: "团队管理…", + icon: , + onClick: () => setDialog({ type: "team" }), + }, + ]; + + const siteItem: MenuItem = { + label: ( + + 官网与价格 + + + ), + icon: , + onClick: () => window.open("/home", "_blank", "noopener,noreferrer"), + }; + + const accountItems: MenuItem[] = account.user + ? [ + ...(account.activeSpaceId !== null + ? [ + { + label: account.syncing + ? "同步中…" + : account.lastSyncAt + ? `已同步 ${new Date(account.lastSyncAt).toLocaleTimeString("zh-CN")}` + : "立即同步到云端", + icon: , + onClick: () => void syncNow(), + }, + ] + : []), + { + label: "团队管理…", + icon: , + onClick: () => setDialog({ type: "team" }), + }, + { type: "sep" as const }, + siteItem, + { + label: "退出登录", + icon: , + onClick: () => void authLogout(), + }, + ] + : [ + { + label: "登录 / 注册", + icon: , + onClick: () => setDialog({ type: "auth" }), + }, + { type: "sep" as const }, + siteItem, + ]; + + const navs: { id: string; icon: IconName; label: string; active: boolean; onClick: () => void }[] = [ + { + id: "api", + icon: "globe", + label: "接口", + active: view === "api" && sidebarSide === "api", + onClick: () => { + setView("api"); + setSidebarSide("api"); + }, + }, + { + id: "history", + icon: "clock", + label: "历史", + active: view === "api" && sidebarSide === "history", + onClick: () => { + setView("api"); + setSidebarSide("history"); + }, + }, + { + id: "testing", + icon: "list-checks", + label: "测试", + active: view === "testing", + onClick: () => setView("testing"), + }, + ]; + + const syncTitle = !account.user + ? "登录 / 注册,开启云端个人与团队空间" + : `${account.user.name}(${account.user.email})\n当前空间:${spaceName}${ + account.syncing + ? "\n同步中…" + : account.syncError + ? `\n同步失败:${account.syncError}` + : account.lastSyncAt && account.activeSpaceId !== null + ? `\n已同步 ${new Date(account.lastSyncAt).toLocaleTimeString("zh-CN")}` + : "" + }`; + + return ( + + ); +} diff --git a/packages/ui/src/components/Sidebar.tsx b/packages/ui/src/components/Sidebar.tsx index d76681a..62b8c85 100644 --- a/packages/ui/src/components/Sidebar.tsx +++ b/packages/ui/src/components/Sidebar.tsx @@ -25,7 +25,9 @@ export function Sidebar() { const history = useWorkspace((s) => s.history); const clearHistory = useWorkspace((s) => s.clearHistory); const openTab = useWorkspace((s) => s.openTab); - const [side, setSide] = useState<"api" | "history">("api"); + // 由活动栏驱动:接口树 / 历史记录 两个展示面共用本侧栏 + const side = useWorkspace((s) => s.sidebarSide); + const setSide = useWorkspace((s) => s.setSidebarSide); const [menu, setMenu] = useState<{ x: number; y: number; items: MenuItem[] } | null>(null); const hitIds = useMemo(() => searchTree(collections, search), [collections, search]); @@ -33,12 +35,14 @@ export function Sidebar() { return ( ); diff --git a/packages/ui/src/components/TopBar.tsx b/packages/ui/src/components/TopBar.tsx index ec29332..e98829e 100644 --- a/packages/ui/src/components/TopBar.tsx +++ b/packages/ui/src/components/TopBar.tsx @@ -1,9 +1,10 @@ /** - * 顶栏:logo / 新建 / 导入导出 / 环境 / Mock / 账号 / 主题 / 文档 / 设置。 + * 顶栏(瘦身版):品牌 + 新建/导入/导出 + 环境切换。 + * 空间切换、导航、账号、主题、文档、设置均在左侧活动栏(ActivityRail)。 */ import { useEffect, useState } from "react"; import { useWorkspace } from "../workspaceStore"; -import { ContextMenu, cx, type MenuItem } from "./common"; +import { ContextMenu, type MenuItem } from "./common"; import { EnvBadge } from "./EnvBadge"; import { Icon, type IconName } from "./icons"; import { Logo } from "./Logo"; @@ -21,20 +22,14 @@ export function TopBar() { const collections = useWorkspace((s) => s.collections); const environments = useWorkspace((s) => s.environments); const activeEnvironmentId = useWorkspace((s) => s.activeEnvironmentId); - const settings = useWorkspace((s) => s.settings); const setActiveEnvironment = useWorkspace((s) => s.setActiveEnvironment); - const setTheme = useWorkspace((s) => s.setTheme); const setDialog = useWorkspace((s) => s.setDialog); const addRequest = useWorkspace((s) => s.addRequest); const addFolder = useWorkspace((s) => s.addFolder); const [newMenu, setNewMenu] = useState<{ x: number; y: number } | null>(null); const [exportMenu, setExportMenu] = useState<{ x: number; y: number } | null>(null); - const [spaceMenu, setSpaceMenu] = useState<{ x: number; y: number } | null>(null); - const [accountMenu, setAccountMenu] = useState<{ x: number; y: number } | null>(null); - const account = useWorkspace((s) => s.account); - const switchSpace = useWorkspace((s) => s.switchSpace); - const syncNow = useWorkspace((s) => s.syncNow); + const [envMenu, setEnvMenu] = useState<{ x: number; y: number } | null>(null); useEffect(() => { document.title = "OpsxAPI · 接口工作台"; @@ -62,7 +57,6 @@ export function TopBar() { const activeEnv = environments.find((e) => e.id === activeEnvironmentId); - const [envMenu, setEnvMenu] = useState<{ x: number; y: number } | null>(null); const envItems: MenuItem[] = [ ...environments.map((env) => ({ label: ( @@ -93,69 +87,9 @@ export function TopBar() { }, ]; - const activeSpace = - account.activeSpaceId === null ? null : account.spaces.find((sp) => sp.id === account.activeSpaceId) ?? null; - const spaceName = activeSpace - ? `${activeSpace.teamId !== null ? "团队 · " : ""}${activeSpace.name}` - : "本地空间"; - - const spaceItems: MenuItem[] = [ - { - label: ( - - - 本地空间 - {account.activeSpaceId === null && ● 当前} - - ), - onClick: () => void switchSpace(null), - }, - ...account.spaces.map((sp) => ({ - label: ( - - - {sp.teamId !== null ? `团队 · ${sp.name}` : sp.name} - {sp.id === account.activeSpaceId ? ( - ● 当前 - ) : ( - - {new Date(sp.updatedAt * 1000).toLocaleDateString("zh-CN")} - - )} - - ), - onClick: () => void switchSpace(sp.id), - })), - ]; - - const accountItems: MenuItem[] = account.user - ? [ - ...(account.activeSpaceId !== null - ? [ - { - label: account.syncing ? "同步中…" : account.lastSyncAt ? `已同步 ${new Date(account.lastSyncAt).toLocaleTimeString("zh-CN")}` : "立即同步到云端", - icon: , - onClick: () => void syncNow(), - }, - ] - : []), - { type: "sep" as const }, - { - label: "团队管理…", - icon: , - onClick: () => setDialog({ type: "team" }), - }, - { - label: "退出登录", - icon: , - onClick: () => void useWorkspace.getState().authLogout(), - }, - ] - : []; - return (
@@ -220,93 +154,8 @@ export function TopBar() { )} - - - -
- - {account.user ? ( - <> - - - - ) : ( - - )} - -
- -
- - - 文档 - -
- setSpaceMenu(null)} - /> - setAccountMenu(null)} - /> setNewMenu(null)} diff --git a/packages/ui/src/workspaceStore.ts b/packages/ui/src/workspaceStore.ts index 7c4e3f4..c4cfb32 100644 --- a/packages/ui/src/workspaceStore.ts +++ b/packages/ui/src/workspaceStore.ts @@ -127,6 +127,8 @@ type WorkspaceStore = PersistedShape & { runtime: Record; /** 主视图:接口工作台 / 自动化测试 */ view: "api" | "testing"; + /** 接口视图下侧栏的展示面:集合树 / 历史记录(由活动栏驱动) */ + sidebarSide: "api" | "history"; /** 账号与云端空间(null 空间 = 本地) */ account: { @@ -142,6 +144,7 @@ type WorkspaceStore = PersistedShape & { init(): void; setView(view: "api" | "testing"): void; + setSidebarSide(side: "api" | "history"): void; authLogin(email: string, password: string): Promise; authRegister(name: string, email: string, password: string): Promise; authLogout(): Promise; @@ -407,6 +410,7 @@ export const useWorkspace = create((set, get) => { testRuns: [], schedules: [], view: "api", + sidebarSide: "api", account: { token: null, user: null, @@ -729,6 +733,10 @@ export const useWorkspace = create((set, get) => { set({ view }); }, + setSidebarSide(sidebarSide) { + set({ sidebarSide }); + }, + setEnvValues(values) { const envId = get().activeEnvironmentId; if (!envId) return;