chore: 恢复旧布局基线(迁移改动丢失后重建,apps/packages 布局暂存)

This commit is contained in:
logan committed 2026-10-14 20:31:17 +08:00
1 parent a9510235bb
commit 456fa6954b
203 files changed
+18787 -48

No files matched your search

+260
View File
@@ -0,0 +1,260 @@
import { useEffect, useState } from "react";
import { driverStatus, health, type DriverStatusItem } from "./api";
import { OpsModal } from "./OpsModal";
import { useWorkspaceStore } from "./store";
const IS_MAC = typeof navigator !== "undefined" && /Mac/i.test(navigator.platform);
const MOD = IS_MAC ? "⌘" : "Ctrl";
type TabId = "general" | "execution" | "results" | "drivers" | "shortcuts" | "about";
const TABS: Array<{ id: TabId; icon: string; label: string; desc: string }> = [
{ id: "general", icon: "🎨", label: "常规", desc: "主题与界面" },
{ id: "execution", icon: "⚡", label: "执行", desc: "超时与多语句" },
{ id: "results", icon: "📊", label: "结果", desc: "面板与分页" },
{ id: "drivers", icon: "🧩", label: "驱动", desc: "agent 驱动状态" },
{ id: "shortcuts", icon: "⌨️", label: "快捷键", desc: "键盘操作一览" },
{ id: "about", icon: "ℹ️", label: "关于", desc: "版本与数据目录" },
];
function Row({ label, children, hint }: { label: string; children: React.ReactNode; hint?: string }) {
return (
<div className="space-y-1">
<div className="flex items-center justify-between gap-3">
<span className="text-[12.5px]" style={{ color: "var(--ops-text)" }}>{label}</span>
{children}
</div>
{hint && <div className="text-[11px]" style={{ color: "var(--ops-muted)" }}>{hint}</div>}
</div>
);
}
function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
className="h-4.5 w-9 rounded-full border transition"
style={{
height: 18,
borderColor: "var(--ops-border)",
background: checked ? "var(--ops-accent)" : "var(--ops-surface-2)",
}}
onClick={() => onChange(!checked)}
>
<span
className="block rounded-full bg-white transition-all"
style={{ width: 12, height: 12, marginLeft: checked ? 22 : 3, boxShadow: "0 1px 2px rgba(0,0,0,.3)" }}
/>
</button>
);
}
function Seg<T extends string | number>({ value, options, onChange }: { value: T; options: Array<{ v: T; label: string }>; onChange: (v: T) => void }) {
return (
<div className="ops-seg flex">
{options.map((o) => (
<button
key={String(o.v)}
type="button"
className={value === o.v ? "ops-tree-item-active" : ""}
style={{ fontSize: "var(--ops-text-xs)" }}
onClick={() => onChange(o.v)}
>
{o.label}
</button>
))}
</div>
);
}
/** 设置弹框:左分类导航 + 右面板(对标 opsxshell / DBeaver Preferences) */
export function SettingsModal({ onClose }: { onClose: () => void }) {
const [tab, setTab] = useState<TabId>("general");
const theme = useWorkspaceStore((s) => s.theme);
const showAi = useWorkspaceStore((s) => s.showAi);
const stopOnError = useWorkspaceStore((s) => s.stopOnError);
const execTimeoutMs = useWorkspaceStore((s) => s.execTimeoutMs);
const resultRatio = useWorkspaceStore((s) => s.resultRatio);
const set = useWorkspaceStore((s) => s.set);
const notify = useWorkspaceStore((s) => s.notify);
const openConnectionList = useWorkspaceStore((s) => s.openConnectionList);
const [drivers, setDrivers] = useState<DriverStatusItem[] | null>(null);
const [version, setVersion] = useState<string>("");
useEffect(() => {
void driverStatus().then(setDrivers).catch(() => setDrivers([]));
void health().then((h) => setVersion(String((h as { version?: string }).version ?? ""))).catch(() => {});
}, []);
const resetLayout = () => {
set("leftWidth", 260);
set("aiWidth", 280);
set("resultRatio", 0.38);
notify({ kind: "success", title: "已重置", message: "面板布局恢复默认", mode: "toast", durationMs: 1400 });
};
return (
<OpsModal variant="info" title="设置" onOk={onClose}>
<div className="flex" style={{ minWidth: 560, minHeight: 340 }}>
{/* 左导航 */}
<nav className="w-36 shrink-0 space-y-px border-r pr-1" style={{ borderColor: "var(--ops-border-soft)" }}>
{TABS.map((t) => (
<button
key={t.id}
type="button"
className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-[12px] transition ${
tab === t.id ? "ops-tree-item-active font-medium" : "opacity-70 hover:opacity-100"
}`}
onClick={() => setTab(t.id)}
>
<span className="w-4 text-center">{t.icon}</span>
<span>{t.label}</span>
</button>
))}
</nav>
{/* 右面板 */}
<div className="min-w-0 flex-1 space-y-4 pl-4">
{tab === "general" && (
<>
<Row label="主题">
<Seg
value={theme}
options={[{ v: "dark" as const, label: "深色" }, { v: "light" as const, label: "浅色" }]}
onChange={(v) => set("theme", v)}
/>
</Row>
<Row label="显示 AI 面板" hint="工作台右侧的 AI 助手面板">
<Toggle checked={showAi} onChange={(v) => set("showAi", v)} />
</Row>
<Row label="面板布局" hint="侧栏宽度、AI 面板宽度、结果面板占比">
<button type="button" className="ops-btn-secondary h-6 px-2 text-[11px]" onClick={resetLayout}>
恢复默认
</button>
</Row>
</>
)}
{tab === "execution" && (
<>
<Row label="默认执行超时" hint="单语句执行上限,超时由服务端强制中断">
<Seg
value={execTimeoutMs}
options={[
{ v: 0, label: "无" },
{ v: 10000, label: "10s" },
{ v: 30000, label: "30s" },
{ v: 60000, label: "60s" },
{ v: 300000, label: "5min" },
]}
onChange={(v) => set("execTimeoutMs", v)}
/>
</Row>
<Row label="多语句遇错即停" hint="一次执行多条时,某条失败即停止后续">
<Toggle checked={stopOnError} onChange={(v) => set("stopOnError", v)} />
</Row>
<Row label="危险语句确认" hint="DROP/TRUNCATE/无 WHERE 删除等由服务端权威拦截,执行前需二次确认(不可关闭)">
<span className="text-[11px]" style={{ color: "var(--ops-accent)" }}>始终开启</span>
</Row>
</>
)}
{tab === "results" && (
<>
<Row label={`结果面板占比 ${Math.round(resultRatio * 100)}%`} hint="编辑器与结果面板的高度分配">
<input
type="range"
min={25}
max={60}
value={Math.round(resultRatio * 100)}
onChange={(e) => set("resultRatio", Number(e.target.value) / 100)}
style={{ accentColor: "var(--ops-accent)", width: 120 }}
/>
</Row>
<Row label="行数上限" hint="查询默认 1000 行、硬上限 10000 行(超出截断提示),由服务端强制执行">
<span className="text-[11px]" style={{ color: "var(--ops-muted)" }}>1000 / 10000</span>
</Row>
</>
)}
{tab === "drivers" && (
<>
<div className="space-y-1.5">
{drivers === null && <div className="text-[11px]" style={{ color: "var(--ops-muted)" }}>加载中…</div>}
{drivers?.map((d) => (
<div key={d.id} className="flex items-center justify-between rounded border px-2 py-1.5 text-[11.5px]" style={{ borderColor: "var(--ops-border-soft)" }}>
<span>{d.id}</span>
<span style={{ color: d.allInstalled ? "var(--ops-accent)" : "var(--ops-warning)" }}>
{d.allInstalled ? "已安装" : d.license === "proprietary" ? "需自备 jar" : "未安装"}
</span>
</div>
))}
</div>
<Row label="驱动管理" hint="预装下载、离线包导入、JDK 检测">
<button
type="button"
className="ops-btn-secondary h-6 px-2 text-[11px]"
onClick={() => {
onClose();
openConnectionList();
notify({ kind: "info", title: "驱动管理", message: "连接列表页右上角 ⚙ 打开驱动管理", mode: "toast", durationMs: 2600 });
}}
>
去管理
</button>
</Row>
</>
)}
{tab === "shortcuts" && (
<div className="space-y-1.5">
{[
[`${MOD} + Enter`, "执行当前语句或选区"],
[`${MOD} + T`, "新建查询"],
[`${MOD} + W`, "关闭查询"],
[`${MOD} + 1-9`, "切换到第 N 个查询"],
["Ctrl + Tab", "循环切换查询"],
[`${MOD} + ⇧ + F`, "聚焦对象树过滤框"],
[`${MOD} + B`, "显示/隐藏 AI"],
[`${MOD} + /`, "快捷键帮助"],
["双击表名", "打开前 100 行"],
["双击单元格", "进入行编辑"],
].map(([k, v]) => (
<div key={k} className="flex justify-between gap-3 text-[12px]">
<span className="font-mono" style={{ color: "var(--ops-accent)" }}>{k}</span>
<span style={{ color: "var(--ops-muted)" }}>{v}</span>
</div>
))}
</div>
)}
{tab === "about" && (
<div className="space-y-2 text-[12px]" style={{ color: "var(--ops-text)" }}>
<div className="flex justify-between"><span style={{ color: "var(--ops-muted)" }}>版本</span><span className="font-mono">{version || "—"}</span></div>
<div className="flex justify-between"><span style={{ color: "var(--ops-muted)" }}>形态</span><span>桌面客户端(Tauri 2)</span></div>
<div className="flex justify-between gap-4">
<span style={{ color: "var(--ops-muted)" }}>驱动目录</span>
<span className="truncate font-mono" title="~/.opsxdb/drivers">~/.opsxdb/drivers</span>
</div>
<div className="flex justify-between gap-4">
<span style={{ color: "var(--ops-muted)" }}>驱动商店</span>
<a
className="hover:underline"
style={{ color: "var(--ops-accent)" }}
href="https://gitee.com/attacker/opsxdb-driver"
target="_blank"
rel="noreferrer"
>
gitee.com/attacker/opsxdb-driver
</a>
</div>
</div>
)}
</div>
</div>
</OpsModal>
);
}