fix: 顶栏文案去重与文档/连库体验更新

顶栏定为:连接 · 新建连接 · 新建查询 · 查询历史 · 快捷键;
同步 docs/落地页、对象树与 connection-types 注册表整理。
This commit is contained in:
logan committed 2026-09-20 16:32:29 +08:00
1 parent f8740d27f7
commit 26f459d1b8
49 files changed
+2682 -492

No files matched your search

+147
View File
@@ -1,8 +1,63 @@
import { useEffect, useState } from "react";
import { execSql } from "./api";
import { ensureSession } from "./ObjectTree";
import { useWorkspaceStore } from "./store";
type MetaRow = Record<string, unknown>;
type TableMeta = {
indexes: MetaRow[];
fks: MetaRow[];
triggers: MetaRow[];
};
const EMPTY_META: TableMeta = { indexes: [], fks: [], triggers: [] };
/** MySQL 协议族才走 information_schema 元数据(PG/Oracle 的系统目录另行适配) */
function isMysqlFamily(driver?: string) {
return ["mysql", "mariadb", "tidb", "oceanbase", "polardb", "tdsql"].includes((driver || "").toLowerCase());
}
/** 标识符转义进 information_schema 字符串比较 */
const esc = (s: string) => s.replace(/'/g, "''");
export function SchemaPane() {
const schema = useWorkspaceStore((s) => s.schemaView);
const set = useWorkspaceStore((s) => s.set);
const conn = useWorkspaceStore((s) => s.connections.find((c) => c.id === s.activeConnectionId));
const [meta, setMeta] = useState<TableMeta>(EMPTY_META);
const mysql = isMysqlFamily(conn?.driver);
useEffect(() => {
let alive = true;
setMeta(EMPTY_META);
if (!conn || !mysql || !schema || schema.loading || schema.error || !schema.table) return;
(async () => {
try {
const sid = await ensureSession(conn);
const db = esc(schema.database);
const t = esc(schema.table);
const run = (sqlText: string) => execSql(conn, sqlText, { database: schema.database, sessionId: sid, limit: 200 });
const [ix, fk, tg] = await Promise.all([
run(
`SELECT INDEX_NAME AS name, IF(NON_UNIQUE=0,'UNIQUE','') AS uniq, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ', ') AS cols FROM information_schema.STATISTICS WHERE TABLE_SCHEMA='${db}' AND TABLE_NAME='${t}' GROUP BY INDEX_NAME, NON_UNIQUE ORDER BY INDEX_NAME`,
),
run(
`SELECT CONSTRAINT_NAME AS name, COLUMN_NAME AS col, REFERENCED_TABLE_NAME AS refTable, REFERENCED_COLUMN_NAME AS refCol FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA='${db}' AND TABLE_NAME='${t}' AND REFERENCED_TABLE_NAME IS NOT NULL`,
),
run(
`SELECT TRIGGER_NAME AS name, ACTION_TIMING AS timing, EVENT_MANIPULATION AS event, EVENT_OBJECT_TABLE AS onTable FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA='${db}' AND EVENT_OBJECT_TABLE='${t}'`,
),
]);
if (alive) setMeta({ indexes: ix.rows, fks: fk.rows, triggers: tg.rows });
} catch {
/* 元数据不可用时静默降级:结构页至少保底显示列 */
}
})();
return () => {
alive = false;
};
}, [conn, mysql, schema?.database, schema?.table, schema?.loading, schema?.error]);
if (!schema) {
return (
@@ -36,6 +91,8 @@ export function SchemaPane() {
);
}
const str = (v: unknown) => (v == null ? "" : String(v));
return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="ops-section-label justify-between border-b">
@@ -86,6 +143,96 @@ export function SchemaPane() {
</tbody>
</table>
)}
{mysql && (
<div className="border-t" style={{ borderColor: "var(--ops-border)" }}>
<details open className="px-3 py-1.5">
<summary className="cursor-pointer text-[12.5px] font-medium" style={{ listStyle: "none" }}>
索引({meta.indexes.length})
</summary>
{meta.indexes.length === 0 ? (
<div className="px-1 py-1 text-[11.5px]" style={{ color: "var(--ops-muted)" }}>无索引</div>
) : (
<table className="ops-grid" style={{ width: "100%" }}>
<thead>
<tr>
<th>索引名</th>
<th style={{ width: 70 }}>唯一</th>
<th>列</th>
</tr>
</thead>
<tbody>
{meta.indexes.map((r, i) => (
<tr key={i}>
<td className="font-mono text-[12px]">{str(r.name)}</td>
<td>{r.uniq ? "✓" : ""}</td>
<td className="font-mono text-[12px]" style={{ color: "var(--ops-muted)" }}>{str(r.cols)}</td>
</tr>
))}
</tbody>
</table>
)}
</details>
<details open className="border-t px-3 py-1.5" style={{ borderColor: "var(--ops-border)" }}>
<summary className="cursor-pointer text-[12.5px] font-medium" style={{ listStyle: "none" }}>
外键({meta.fks.length})
</summary>
{meta.fks.length === 0 ? (
<div className="px-1 py-1 text-[11.5px]" style={{ color: "var(--ops-muted)" }}>无外键</div>
) : (
<table className="ops-grid" style={{ width: "100%" }}>
<thead>
<tr>
<th>约束名</th>
<th>列</th>
<th>引用</th>
</tr>
</thead>
<tbody>
{meta.fks.map((r, i) => (
<tr key={i}>
<td className="font-mono text-[12px]">{str(r.name)}</td>
<td className="font-mono text-[12px]">{str(r.col)}</td>
<td className="font-mono text-[12px]" style={{ color: "var(--ops-muted)" }}>
{str(r.refTable)}.{str(r.refCol)}
</td>
</tr>
))}
</tbody>
</table>
)}
</details>
<details open className="border-t px-3 py-1.5" style={{ borderColor: "var(--ops-border)" }}>
<summary className="cursor-pointer text-[12.5px] font-medium" style={{ listStyle: "none" }}>
触发器({meta.triggers.length})
</summary>
{meta.triggers.length === 0 ? (
<div className="px-1 py-1 text-[11.5px]" style={{ color: "var(--ops-muted)" }}>无触发器</div>
) : (
<table className="ops-grid" style={{ width: "100%" }}>
<thead>
<tr>
<th>触发器</th>
<th style={{ width: 80 }}>时机</th>
<th style={{ width: 80 }}>事件</th>
</tr>
</thead>
<tbody>
{meta.triggers.map((r, i) => (
<tr key={i}>
<td className="font-mono text-[12px]">{str(r.name)}</td>
<td>{str(r.timing)}</td>
<td>{str(r.event)}</td>
</tr>
))}
</tbody>
</table>
)}
</details>
</div>
)}
</div>
</div>
);