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

- 修复窄窗口下顶栏按钮被压缩导致中文逐字竖排:按钮 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

+24 -2
View File
@@ -147,7 +147,7 @@
/* ---------- 按钮 ---------- */
.ops-btn {
@apply inline-flex items-center justify-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold transition;
@apply inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-lg px-3 py-1.5 text-xs font-semibold transition;
border: 1px solid transparent;
cursor: pointer;
}
@@ -172,6 +172,7 @@
.ops-btn-ghost:hover:not(:disabled) { background: var(--hover); color: var(--text); }
/* ---------- chip / badge ---------- */
header button, header a { white-space: nowrap; }
.ops-chip {
@apply rounded-full px-2.5 py-1 text-[11px] font-medium transition;
border: 1px solid var(--border);
@@ -324,7 +325,7 @@
/* ---------- 顶栏环境切换器 ---------- */
.ops-envswitch {
@apply inline-flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-xs font-medium transition;
@apply inline-flex shrink-0 items-center gap-2 whitespace-nowrap rounded-lg px-2.5 py-1.5 text-xs font-medium transition;
border: 1px solid var(--border);
background: var(--panel-2);
color: var(--text);
@@ -492,6 +493,27 @@
color: var(--accent-strong);
}
/* ---------- 侧栏分段切换(接口/历史) ---------- */
.side-seg {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2px;
padding: 2px;
border-radius: 0.5rem;
background: var(--panel-3);
}
.side-seg-item {
@apply flex items-center justify-center gap-1 rounded-md px-2 py-1 text-[11px] font-medium transition;
color: var(--text-dim);
}
.side-seg-item:hover { color: var(--text); }
.side-seg-item.active {
background: var(--panel);
color: var(--accent-strong);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
.side-seg-item .ops-count { margin-left: 2px; }
/* ---------- 方法 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); }
+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>