顶栏固定单行 + 侧栏信息架构重构(接口/历史分段切换)
- 修复窄窗口下顶栏按钮被压缩导致中文逐字竖排:按钮 whitespace-nowrap + shrink-0, header flex-nowrap + 横向滚动兜底;协议徽标 xl 以下隐藏;实测 header 恒定 48px、零换行按钮 - 侧栏重构:历史上移——搜索框下「接口 | 历史(计数)」分段切换(历史为高频最近使用), 历史视图含清空按钮;底部只留跨视图导航(自动化测试)+ 外链(官网与价格),职责单一 - 分段控件样式(panel-3 底、active 白底浮起);复验截图评审通过
This commit is contained in:
1 parent
838f76c21b
commit
e77b76986e
3 files changed
+75
-46
No files matched your search
+24
-2
@@ -147,7 +147,7 @@
|
|||||||
|
|
||||||
/* ---------- 按钮 ---------- */
|
/* ---------- 按钮 ---------- */
|
||||||
.ops-btn {
|
.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;
|
border: 1px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -172,6 +172,7 @@
|
|||||||
.ops-btn-ghost:hover:not(:disabled) { background: var(--hover); color: var(--text); }
|
.ops-btn-ghost:hover:not(:disabled) { background: var(--hover); color: var(--text); }
|
||||||
|
|
||||||
/* ---------- chip / badge ---------- */
|
/* ---------- chip / badge ---------- */
|
||||||
|
header button, header a { white-space: nowrap; }
|
||||||
.ops-chip {
|
.ops-chip {
|
||||||
@apply rounded-full px-2.5 py-1 text-[11px] font-medium transition;
|
@apply rounded-full px-2.5 py-1 text-[11px] font-medium transition;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -324,7 +325,7 @@
|
|||||||
|
|
||||||
/* ---------- 顶栏环境切换器 ---------- */
|
/* ---------- 顶栏环境切换器 ---------- */
|
||||||
.ops-envswitch {
|
.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);
|
border: 1px solid var(--border);
|
||||||
background: var(--panel-2);
|
background: var(--panel-2);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@@ -492,6 +493,27 @@
|
|||||||
color: var(--accent-strong);
|
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 着色 ---------- */
|
/* ---------- 方法 chip 着色 ---------- */
|
||||||
.ops-chip.ops-method-GET { background: color-mix(in srgb, var(--m-GET) 14%, transparent); color: var(--m-GET); }
|
.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); }
|
.ops-chip.ops-method-POST { background: color-mix(in srgb, var(--m-POST) 16%, transparent); color: var(--m-POST); }
|
||||||
|
|||||||
@@ -25,23 +25,68 @@ export function Sidebar() {
|
|||||||
const history = useWorkspace((s) => s.history);
|
const history = useWorkspace((s) => s.history);
|
||||||
const clearHistory = useWorkspace((s) => s.clearHistory);
|
const clearHistory = useWorkspace((s) => s.clearHistory);
|
||||||
const openTab = useWorkspace((s) => s.openTab);
|
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 [menu, setMenu] = useState<{ x: number; y: number; items: MenuItem[] } | null>(null);
|
||||||
|
|
||||||
const hitIds = useMemo(() => searchTree(collections, search), [collections, search]);
|
const hitIds = useMemo(() => searchTree(collections, search), [collections, search]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="ops-panel flex w-64 shrink-0 flex-col" style={{ borderRight: "1px solid var(--border)" }}>
|
<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
|
<input
|
||||||
className="ops-input ops-input-sm"
|
className="ops-input ops-input-sm"
|
||||||
placeholder="搜索接口 / 目录…"
|
placeholder="搜索接口 / 目录…"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
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>
|
||||||
|
|
||||||
<div className="ops-scroll min-h-0 flex-1 p-2">
|
<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 && (
|
{collections.length === 0 && (
|
||||||
<div className="ops-faint px-2 py-8 text-center text-xs leading-5">
|
<div className="ops-faint px-2 py-8 text-center text-xs leading-5">
|
||||||
还没有集合
|
还没有集合
|
||||||
@@ -73,49 +118,11 @@ export function Sidebar() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
{/* 自动化测试 / 官网入口 */}
|
{/* 底部:跨视图导航 + 外链 */}
|
||||||
<div className="space-y-1.5 p-2" style={{ borderTop: "1px solid var(--border)" }}>
|
<div className="space-y-1.5 p-2" style={{ borderTop: "1px solid var(--border)" }}>
|
||||||
<button
|
<button
|
||||||
className="ops-btn ops-btn-secondary w-full"
|
className="ops-btn ops-btn-secondary w-full"
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export function TopBar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<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 }}
|
style={{ zIndex: 30 }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -163,7 +163,7 @@ export function TopBar() {
|
|||||||
<Logo size={26} />
|
<Logo size={26} />
|
||||||
<span className="text-[14px] font-bold tracking-tight">OpsxAPI</span>
|
<span className="text-[14px] font-bold tracking-tight">OpsxAPI</span>
|
||||||
</div>
|
</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
|
HTTP · GraphQL · WS · SSE · Socket.IO · MQTT
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user