顶栏固定单行 + 侧栏信息架构重构(接口/历史分段切换)

- 修复窄窗口下顶栏按钮被压缩导致中文逐字竖排:按钮 whitespace-nowrap + shrink-0,
  header flex-nowrap + 横向滚动兜底;协议徽标 xl 以下隐藏;实测 header 恒定 48px、零换行按钮
- 侧栏重构:历史上移——搜索框下「接口 | 历史(计数)」分段切换(历史为高频最近使用),
  历史视图含清空按钮;底部只留跨视图导航(自动化测试)+ 外链(官网与价格),职责单一
- 分段控件样式(panel-3 底、active 白底浮起);复验截图评审通过
This commit is contained in:
logan committed 2026-09-18 13:01:27 +08:00
1 parent 838f76c21b
commit e77b76986e
3 files changed
+75 -46

No files matched your search

+49 -42
View File
@@ -25,23 +25,68 @@ export function Sidebar() {
const history = useWorkspace((s) => s.history);
const clearHistory = useWorkspace((s) => s.clearHistory);
const openTab = useWorkspace((s) => s.openTab);
const [showHistory, setShowHistory] = useState(true);
const [side, setSide] = useState<"api" | "history">("api");
const [menu, setMenu] = useState<{ x: number; y: number; items: MenuItem[] } | null>(null);
const hitIds = useMemo(() => searchTree(collections, search), [collections, search]);
return (
<aside className="ops-panel flex w-64 shrink-0 flex-col" style={{ borderRight: "1px solid var(--border)" }}>
<div className="p-2.5" style={{ borderBottom: "1px solid var(--border)" }}>
<div className="space-y-2 p-2.5" style={{ borderBottom: "1px solid var(--border)" }}>
<input
className="ops-input ops-input-sm"
placeholder="搜索接口 / 目录…"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="side-seg">
<button className={cx("side-seg-item", side === "api" && "active")} onClick={() => setSide("api")}>
<Icon name="library" size={12} /> 接口
</button>
<button className={cx("side-seg-item", side === "history" && "active")} onClick={() => setSide("history")}>
<Icon name="clock" size={12} /> 历史
{history.length > 0 && <span className="ops-count">{Math.min(history.length, 99)}</span>}
</button>
</div>
</div>
<div className="ops-scroll min-h-0 flex-1 p-2">
{side === "history" ? (
<>
{history.length === 0 && (
<div className="ops-faint px-2 py-8 text-center text-xs leading-5">
暂无请求记录
<br />
发送请求后自动归档
</div>
)}
{history.slice(0, 50).map((h) => (
<button
key={h.id}
className="ops-tree-row w-full !py-1 text-left"
onClick={() => {
if (h.nodeId && findNode(useWorkspace.getState().collections, h.nodeId)) {
openTab(h.nodeId);
}
}}
title={`${h.label}\n${h.url}`}
>
<span className="ops-faint w-14 shrink-0 text-[10px]">{formatTime(h.at)}</span>
{h.method && <span className={`ops-method-${h.method} w-12 shrink-0 text-[10px] font-bold`}>{h.method}</span>}
<span className="truncate text-[11px]">{h.label}</span>
{h.status !== undefined && (
<span className={cx("ml-auto text-[10px]", h.ok ? "ops-success" : "ops-danger")}>{h.status}</span>
)}
</button>
))}
{history.length > 0 && (
<button className="ops-btn ops-btn-ghost ops-danger mt-2 w-full py-1 text-[11px]" onClick={clearHistory}>
<Icon name="eraser" size={11} /> 清空历史
</button>
)}
</>
) : (
<>
{collections.length === 0 && (
<div className="ops-faint px-2 py-8 text-center text-xs leading-5">
还没有集合
@@ -73,49 +118,11 @@ export function Sidebar() {
))}
</div>
))}
</div>
{/* 历史 */}
<div style={{ borderTop: "1px solid var(--border)" }}>
<button
className="ops-faint flex w-full items-center justify-between px-3 py-2 text-[11px] font-semibold uppercase tracking-wider"
onClick={() => setShowHistory((v) => !v)}
>
<span>历史记录 · {history.length}</span>
<span>{showHistory ? "▾" : "▸"}</span>
</button>
{showHistory && (
<div className="ops-scroll max-h-44 px-2 pb-2">
{history.length === 0 && <div className="ops-faint px-2 py-3 text-center text-[11px]">暂无记录</div>}
{history.slice(0, 30).map((h) => (
<button
key={h.id}
className="ops-tree-row w-full !py-1 text-left"
onClick={() => {
if (h.nodeId && findNode(useWorkspace.getState().collections, h.nodeId)) {
openTab(h.nodeId);
}
}}
title={`${h.label}\n${h.url}`}
>
<span className="ops-faint w-14 shrink-0 text-[10px]">{formatTime(h.at)}</span>
{h.method && <span className={`ops-method-${h.method} w-12 shrink-0 text-[10px] font-bold`}>{h.method}</span>}
<span className="truncate text-[11px]">{h.label}</span>
{h.status !== undefined && (
<span className={cx("ml-auto text-[10px]", h.ok ? "ops-success" : "ops-danger")}>{h.status}</span>
)}
</button>
))}
{history.length > 0 && (
<button className="ops-btn ops-btn-ghost ops-danger w-full py-1 text-[11px]" onClick={clearHistory}>
清空历史
</button>
)}
</div>
</>
)}
</div>
{/* 自动化测试 / 官网入口 */}
{/* 底部:跨视图导航 + 外链 */}
<div className="space-y-1.5 p-2" style={{ borderTop: "1px solid var(--border)" }}>
<button
className="ops-btn ops-btn-secondary w-full"
+2 -2
View File
@@ -155,7 +155,7 @@ export function TopBar() {
return (
<header
className="ops-panel ops-divider flex h-12 shrink-0 items-center justify-between gap-3 px-4"
className="ops-panel ops-divider flex h-12 shrink-0 flex-nowrap items-center justify-between gap-3 overflow-x-auto px-4"
style={{ zIndex: 30 }}
>
<div className="flex items-center gap-3">
@@ -163,7 +163,7 @@ export function TopBar() {
<Logo size={26} />
<span className="text-[14px] font-bold tracking-tight">OpsxAPI</span>
</div>
<span className="ops-chip" style={{ padding: "1px 8px", fontSize: 10 }}>
<span className="ops-chip hidden whitespace-nowrap xl:inline-flex" style={{ padding: "1px 8px", fontSize: 10 }}>
HTTP · GraphQL · WS · SSE · Socket.IO · MQTT
</span>
</div>