Commit a0b10c06 authored by Data Governance Dev's avatar Data Governance Dev

feat(web): 数据字典浏览器动态分页(滚动加载 + 筛选重读 + 隐藏全选)

状态机重构:
- 删 tableSummary / dataDict 全量 ref + 4 个前端过滤 computed
- 新增 tableList/fieldList + page/hasMore/total/loading 共 10 个 ref
- 新增 fetchTablesPage/fetchFieldsPage(防 stale 响应 + nextTick 后重挂 scroll)
- 新增 onTablesScroll/onFieldsScroll:EP 2.14.3 + height 时 scroll 不冒泡到 el-table 根,
  改在 .el-scrollbar__wrap 上挂原生 addEventListener('scroll', { passive: true })
- 新增 rebindDictScroll:每次 fetch + nextTick 后重挂兜底(el-table 重建 wrap 时失效)
- 筛选 commit 时从后端重读第 1 页(applyTableSearch/applyFieldSearch)
- watch(form.tables) 触发 fetchFieldsPage(1, true)(勾选表变化自动更新字段表)

跨页勾选同步:
- onTableSelectionChange 改为 set 合并(visible 名 + 新选中 → add;visible - 未选中 → delete;
  不在 visible 的保留)—— 旧实现会丢跨页勾选
- 新增 restoreTableSelection:数据刷新后把 el-table 可见勾选与 form.tables 对齐
  (reserve-selection 在 :data 整体替换时失效,必须手动同步)

按钮重写:
- 全选 / 反选:调 /api/dict/all-table-names 拿全量匹配名字 → 设 form.tables
  → fetchTablesPage(1, true) 把所有匹配表拉回首屏
- 清空:form.tables = [] + clearSelection()

CSS:web/static/style.css
- .dict-tables-table thead .el-checkbox, .dict-fields-table thead .el-checkbox
  { visibility: hidden; pointer-events: none; }
  (display:none 会算错表头高度)

addCustomRule:去除 tableSummary 兜底(后端 /api/dict/fields 每行附 table_comment)

踩坑:
- EP 2.14.3 + height → <el-scrollbar> 包裹 body → @scroll 不触发,必须原生监听
- selection-change 只返 visible 选中行,直接赋值丢跨页勾选 → set 合并
- reserve-selection 在 :data 整体替换时失效 → 自写 restoreTableSelection
parent 6cb0ffa7
...@@ -361,7 +361,7 @@ ...@@ -361,7 +361,7 @@
</el-card> </el-card>
<!-- 卡片 2:分析配置 + 启动 --> <!-- 卡片 2:分析配置 + 启动 -->
<el-card v-if="connected && dataDict.length > 0" class="section-card" :class="{ 'is-collapsed': analysisConfigCollapsed }" shadow="hover"> <el-card v-if="connected && tableTotal > 0" class="section-card" :class="{ 'is-collapsed': analysisConfigCollapsed }" shadow="hover">
<template #header> <template #header>
<div class="card-header collapsible-header" @click="analysisConfigCollapsed = !analysisConfigCollapsed"> <div class="card-header collapsible-header" @click="analysisConfigCollapsed = !analysisConfigCollapsed">
<el-icon><Setting /></el-icon> <el-icon><Setting /></el-icon>
...@@ -543,7 +543,7 @@ ...@@ -543,7 +543,7 @@
</el-card> </el-card>
<!-- 卡片 2.5:自定义规则(点下方数据字典浏览器字段表右侧 + 号累积) --> <!-- 卡片 2.5:自定义规则(点下方数据字典浏览器字段表右侧 + 号累积) -->
<el-card v-if="connected && dataDict.length > 0" class="section-card" :class="{ 'is-collapsed': customConfigCollapsed }" shadow="hover"> <el-card v-if="connected && tableTotal > 0" class="section-card" :class="{ 'is-collapsed': customConfigCollapsed }" shadow="hover">
<template #header> <template #header>
<div class="card-header collapsible-header" @click="customConfigCollapsed = !customConfigCollapsed"> <div class="card-header collapsible-header" @click="customConfigCollapsed = !customConfigCollapsed">
<el-icon><Operation /></el-icon> <el-icon><Operation /></el-icon>
...@@ -661,17 +661,21 @@ ...@@ -661,17 +661,21 @@
</el-card> </el-card>
<!-- 卡片 3:数据字典浏览器(连接成功后才有数据) --> <!-- 卡片 3:数据字典浏览器(连接成功后才有数据) -->
<el-card v-if="connected && dataDict.length > 0" class="section-card" :class="{ 'is-collapsed': dataDictCollapsed }" shadow="hover"> <el-card v-if="connected" class="section-card" :class="{ 'is-collapsed': dataDictCollapsed }" shadow="hover">
<template #header> <template #header>
<div class="card-header collapsible-header" @click="dataDictCollapsed = !dataDictCollapsed"> <div class="card-header collapsible-header" @click="dataDictCollapsed = !dataDictCollapsed">
<el-icon><Document /></el-icon> <el-icon><Document /></el-icon>
<span class="card-title">数据字典浏览器</span> <span class="card-title">数据字典浏览器</span>
<span class="dict-status"> <span class="dict-status">
共 {{ tablesAll.length }} 张表,{{ dataDict.length }} 个字段 共 {{ tableTotal }} 张表,{{ fieldTotal }} 个字段
<template v-if="tableTotal > 0">
(已加载 {{ tableList.length }} / {{ tableTotal }} 张表
<span v-if="fieldTotal > 0">,{{ fieldList.length }} 个字段</span>)
</template>
(已勾选 <strong style="color: #409eff">{{ form.tables.length }}</strong> 张) (已勾选 <strong style="color: #409eff">{{ form.tables.length }}</strong> 张)
</span> </span>
<div style="margin-left: auto"> <div style="margin-left: auto">
<el-button size="small" @click.stop="selectAllTables" :disabled="form.tables.length === tablesAll.length">全选</el-button> <el-button size="small" @click.stop="selectAllTables" :disabled="form.tables.length === tableTotal">全选</el-button>
<el-button size="small" @click.stop="selectNoTables" :disabled="form.tables.length === 0">清空</el-button> <el-button size="small" @click.stop="selectNoTables" :disabled="form.tables.length === 0">清空</el-button>
<el-button size="small" @click.stop="selectTablesInvert">反选</el-button> <el-button size="small" @click.stop="selectTablesInvert">反选</el-button>
<el-button <el-button
...@@ -694,13 +698,22 @@ ...@@ -694,13 +698,22 @@
<!-- 左:表列表(带勾选) --> <!-- 左:表列表(带勾选) -->
<div class="dict-pane-left"> <div class="dict-pane-left">
<div class="dict-toolbar"> <div class="dict-toolbar">
<el-input v-model="tableSearch" placeholder="按表名 / 表注释筛选" clearable size="small" /> <el-input
v-model="tableSearchDraft"
placeholder="按表名 / 表注释筛选"
clearable size="small"
@blur="applyTableSearch"
@clear="applyTableSearch"
@keyup.enter="applyTableSearch"
/>
</div> </div>
<el-table <el-table
ref="tablesTableRef" ref="tablesTableRef"
:data="filteredTables" :data="tableList"
stripe size="small" height="520" stripe size="small" height="520"
class="dict-tables-table"
@selection-change="onTableSelectionChange" @selection-change="onTableSelectionChange"
@scroll="onTablesScroll"
:row-key="row => row.table_name"> :row-key="row => row.table_name">
<!-- 注意:selection 列必须放在最后 —— 放在带 default slot 的列前面时, <!-- 注意:selection 列必须放在最后 —— 放在带 default slot 的列前面时,
Element Plus 会算错 header 的 colspan / scope,导致 body 行 Element Plus 会算错 header 的 colspan / scope,导致 body 行
...@@ -719,7 +732,7 @@ ...@@ -719,7 +732,7 @@
</el-table-column> </el-table-column>
<el-table-column label="字段数" width="72" align="right"> <el-table-column label="字段数" width="72" align="right">
<template #default="{ row }"> <template #default="{ row }">
<el-tag size="small" type="info" effect="plain">{{ fieldCountByTable[row.table_name] || 0 }}</el-tag> <el-tag size="small" type="info" effect="plain">{{ row.field_count || 0 }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column type="selection" width="44" reserve-selection /> <el-table-column type="selection" width="44" reserve-selection />
...@@ -733,15 +746,30 @@ ...@@ -733,15 +746,30 @@
<template v-if="form.tables.length === 0"> <template v-if="form.tables.length === 0">
请在左侧勾选要分析的表 —— 不勾选任何表则无法启动分析 请在左侧勾选要分析的表 —— 不勾选任何表则无法启动分析
</template> </template>
<template v-else-if="fieldTotal === 0 && !fieldLoading">
{{ form.tables.length }} 张选中表暂无字段记录
</template>
<template v-else> <template v-else>
当前展示 {{ form.tables.length }} 张选中表的 {{ filteredFields.length }} 个字段 当前展示 {{ form.tables.length }} 张选中表的 {{ fieldTotal }} 个字段
<span v-if="fieldTotal > fieldList.length">(已加载 {{ fieldList.length }} / {{ fieldTotal }})</span>
</template> </template>
</span> </span>
<el-input v-model="fieldSearch" placeholder="按字段名 / 字段注释筛选" clearable size="small" style="flex: 0 0 240px" /> <el-input
v-model="fieldSearchDraft"
placeholder="按字段名 / 字段注释筛选"
clearable size="small"
style="flex: 0 0 240px"
@blur="applyFieldSearch"
@clear="applyFieldSearch"
@keyup.enter="applyFieldSearch"
/>
</div> </div>
<el-table <el-table
:data="filteredFields" ref="fieldsTableRef"
:data="fieldList"
stripe size="small" height="520" stripe size="small" height="520"
class="dict-fields-table"
@scroll="onFieldsScroll"
> >
<el-table-column prop="table_name" label="表名" width="200" show-overflow-tooltip fixed> <el-table-column prop="table_name" label="表名" width="200" show-overflow-tooltip fixed>
<template #default="{ row }"> <template #default="{ row }">
...@@ -1198,14 +1226,238 @@ ...@@ -1198,14 +1226,238 @@
} }
}, { immediate: true }); }, { immediate: true });
// ── 数据字典(来自 connect/test 响应) ── // ── 数据字典浏览器(2026-08-13 改为后端分页动态加载)──
const tableSummary = ref([]); // 表级 // 全量字典由 /api/connect/test 写进后端 DataDictCache;
const dataDict = ref([]); // 字段级(每行带 table_name) // 浏览器按 page/page_size + q 拉取切片,初次进入只显示第一页。
// - tableList / fieldList = 已加载的当前可见行(累加多页)
// - tablePage / fieldPage = 当前已加载的最大页号
// - tableHasMore / fieldHasMore = 是否还有下一页
// - tableTotal / fieldTotal = 后端应用 q 过滤后的总数(UI 展示用)
// - tableLoading / fieldLoading = 加载下一页时为 true
// - tablesFetchGen / fieldsFetchGen = 请求代次,防 stale 响应
const TABLE_PAGE_SIZE = 50;
const FIELD_PAGE_SIZE = 50;
const tableList = ref([]);
const tablePage = ref(0);
const tableHasMore = ref(false);
const tableTotal = ref(0);
const tableLoading = ref(false);
const fieldList = ref([]);
const fieldPage = ref(0);
const fieldHasMore = ref(false);
const fieldTotal = ref(0);
const fieldLoading = ref(false);
let tablesFetchGen = 0;
let fieldsFetchGen = 0;
// ── 数据字典浏览器的本地状态 ── // ── 数据字典浏览器的本地状态 ──
const tableSearch = ref(''); // 2026-08-13:把筛选时机从「输入即筛」(v-model 默认 @input)改成
const fieldSearch = ref(''); // 「focusoff / 点 X / 回车」才触发筛选。
// - *Draft = 输入框的当前值(v-model 实时绑定,键入即改)
// - *Committed = 真正驱动后端分页查询的查询串(commit 时同步)
// 原因:表/字段数量大时(1971 张表 / 36212 字段),每键入一个字符
// 都重算 + 重新渲染 el-table,会让搜索框输入卡顿;
// 且筛选改为后端 → commit 时从后端拉第一页替换。
const tableSearchDraft = ref('');
const tableSearchCommitted = ref('');
const fieldSearchDraft = ref('');
const fieldSearchCommitted = ref('');
function applyTableSearch() {
const v = (tableSearchDraft.value || '').trim().toLowerCase();
if (v !== tableSearchCommitted.value) {
tableSearchCommitted.value = v;
fetchTablesPage(1, /* replace */ true);
}
}
function applyFieldSearch() {
const v = (fieldSearchDraft.value || '').trim().toLowerCase();
if (v !== fieldSearchCommitted.value) {
fieldSearchCommitted.value = v;
fetchFieldsPage(1, /* replace */ true);
}
}
const tablesTableRef = ref(null); const tablesTableRef = ref(null);
const fieldsTableRef = ref(null);
// ── 数据字典:勾选表变化 → 字段表重读第 1 页 ──
// 深度 watch(form.tables 是普通 array,但 Element Plus 勾选变化时
// onTableSelectionChange 直接 form.tables = Array.from(set) 整体替换,
// 这里只关心最终内容的形状,所以 slice().sort().join(',') 即可触发)。
// 跳过初次赋值(immediate=false;初次在 testConnection 里显式拉)。
watch(
() => (form.tables || []).slice().sort().join(','),
() => { fetchFieldsPage(1, /* replace */ true); }
);
// ── 数据字典:连接身份(用于构造分页 query)──
// 注意:分页端点不需要 password,所以这里直接复用 form 的明文连接信息。
// 后端 cache key = (db_type, host, port, database, user),不含密码。
function buildDictConnParams() {
return {
db_type: form.db_type,
host: form.host,
port: form.port || 3306,
user: form.user,
database: form.database,
};
}
// ── 数据字典:分页拉取 ──
// replace = true 时清空已加载列表,只保留这一页(用于筛选重读、初始加载、刷新)
async function fetchTablesPage(page, replace = false) {
if (tableLoading.value && !replace) return;
const myGen = ++tablesFetchGen;
tableLoading.value = true;
try {
const params = new URLSearchParams({
...buildDictConnParams(),
page: String(page),
page_size: String(TABLE_PAGE_SIZE),
q: tableSearchCommitted.value,
});
const r = await fetch('/api/dict/tables?' + params.toString())
.then(r => r.json());
if (myGen !== tablesFetchGen) return; // stale,被新请求覆盖
if (r && r._warning) {
ElementPlus.ElMessage.warning(r._warning);
}
tableTotal.value = (r && r.total) || 0;
tableHasMore.value = !!(r && r.has_more);
tablePage.value = page;
const items = (r && r.tables) || [];
if (replace || page === 1) {
tableList.value = items;
} else {
tableList.value = tableList.value.concat(items);
}
await nextTick();
await restoreTableSelection();
await rebindDictScroll();
} catch (e) {
if (myGen === tablesFetchGen) {
ElementPlus.ElMessage.error('读取表列表失败: ' + e);
}
} finally {
if (myGen === tablesFetchGen) tableLoading.value = false;
}
}
async function fetchFieldsPage(page, replace = false) {
if (fieldLoading.value && !replace) return;
const myGen = ++fieldsFetchGen;
fieldLoading.value = true;
try {
const params = new URLSearchParams({
...buildDictConnParams(),
tables: (form.tables || []).join(','),
page: String(page),
page_size: String(FIELD_PAGE_SIZE),
q: fieldSearchCommitted.value,
});
const r = await fetch('/api/dict/fields?' + params.toString())
.then(r => r.json());
if (myGen !== fieldsFetchGen) return;
if (r && r._warning) {
ElementPlus.ElMessage.warning(r._warning);
}
fieldTotal.value = (r && r.total) || 0;
fieldHasMore.value = !!(r && r.has_more);
fieldPage.value = page;
const items = (r && r.fields) || [];
if (replace || page === 1) {
fieldList.value = items;
} else {
fieldList.value = fieldList.value.concat(items);
}
await nextTick();
await rebindDictScroll();
} catch (e) {
if (myGen === fieldsFetchGen) {
ElementPlus.ElMessage.error('读取字段列表失败: ' + e);
}
} finally {
if (myGen === fieldsFetchGen) fieldLoading.value = false;
}
}
// 滚动到底加载下一页
// 踩坑:EP 2.14.3 + height 设置时,内部用 <el-scrollbar> 包裹 body,
// 滚动事件不会冒泡到 el-table 根 → @scroll 不触发。
// 解法:直接在 .el-scrollbar__wrap(或 fallback .el-table__body-wrapper)
// 上挂原生 scroll 监听。@scroll 保留作为兜底,但实际靠原生监听。
function _onTablesScroll(e) {
const t = e && e.target;
if (!t) return;
if (tableLoading.value || !tableHasMore.value) return;
if (t.scrollTop + t.clientHeight >= t.scrollHeight - 60) {
fetchTablesPage(tablePage.value + 1, false);
}
}
function _onFieldsScroll(e) {
const t = e && e.target;
if (!t) return;
if (fieldLoading.value || !fieldHasMore.value) return;
if (t.scrollTop + t.clientHeight >= t.scrollHeight - 60) {
fetchFieldsPage(fieldPage.value + 1, false);
}
}
// 给一个 el-table 实例的内部 scrollable 容器挂原生 scroll 监听
function bindDictScroll(refEl, handler) {
if (!refEl || !refEl.$el) return;
const wrap = refEl.$el.querySelector(
'.el-table__body-wrapper .el-scrollbar__wrap'
) || refEl.$el.querySelector('.el-table__body-wrapper');
if (!wrap) return;
// 防重复挂(el-table 重新渲染 wrap 时会换 DOM 节点;用弱引用记一下)
if (wrap.__dictScrollHandler === handler) return;
if (wrap.__dictScrollHandler) {
wrap.removeEventListener('scroll', wrap.__dictScrollHandler);
}
wrap.__dictScrollHandler = handler;
wrap.addEventListener('scroll', handler, { passive: true });
}
// 每次 fetch 完成 + nextTick 后重新挂(防止 el-table 重建 wrap)
async function rebindDictScroll() {
await nextTick();
bindDictScroll(tablesTableRef.value, _onTablesScroll);
bindDictScroll(fieldsTableRef.value, _onFieldsScroll);
}
// Element Plus @scroll 事件兜底(高版本会触发;EP 2.14.3 不触发但留着无害)
function onTablesScroll(e) {
if (e && e.target && e.target.__dictScrollHandler === _onTablesScroll) return;
_onTablesScroll(e);
}
function onFieldsScroll(e) {
if (e && e.target && e.target.__dictScrollHandler === _onFieldsScroll) return;
_onFieldsScroll(e);
}
// 数据变化后把 el-table 的勾选状态与 form.tables 同步(reserve-selection 不够用)
async function restoreTableSelection() {
if (!tablesTableRef.value) return;
const selectedSet = new Set(form.tables || []);
const visibleNames = new Set(tableList.value.map(r => r.table_name));
const currentRows = tablesTableRef.value.getSelectionRows
? tablesTableRef.value.getSelectionRows()
: [];
const currentNames = new Set(currentRows.map(r => r.table_name));
// 取消 visible 但不应选中的行
for (const row of currentRows) {
if (!selectedSet.has(row.table_name)) {
// 调用 toggleRowSelection(row, false) 会触发 @selection-change,
// 但 onTableSelectionChange 用 set 合并,不会丢失不在 visible 的勾选
tablesTableRef.value.toggleRowSelection(row, false);
}
}
// 选中 visible 且应在 form.tables 但当前未选的行
for (const row of tableList.value) {
if (selectedSet.has(row.table_name) && !currentNames.has(row.table_name)) {
tablesTableRef.value.toggleRowSelection(row, true);
}
}
}
// ── 任务状态 ── // ── 任务状态 ──
const currentJob = ref(null); const currentJob = ref(null);
...@@ -1584,80 +1836,85 @@ ...@@ -1584,80 +1836,85 @@
// 注:旧 filteredViolations computed 已移除 —— 现在由 resolveRows() 在渲染时 // 注:旧 filteredViolations computed 已移除 —— 现在由 resolveRows() 在渲染时
// 根据 t.linkage 动态过滤(protocol-driven),单一实现多处复用。 // 根据 t.linkage 动态过滤(protocol-driven),单一实现多处复用。
// ── 数据字典:派生计算 ──
const tablesAll = computed(() => tableSummary.value);
const filteredTables = computed(() => {
const q = tableSearch.value.trim().toLowerCase();
if (!q) return tablesAll.value;
return tablesAll.value.filter(t =>
(t.table_name || '').toLowerCase().includes(q) ||
(t.table_comment || '').toLowerCase().includes(q)
);
});
const fieldCountByTable = computed(() => {
const m = {};
for (const r of dataDict.value) {
m[r.table_name] = (m[r.table_name] || 0) + 1;
}
return m;
});
const filteredFields = computed(() => {
const selected = new Set(form.tables);
const q = fieldSearch.value.trim().toLowerCase();
let rows = dataDict.value.filter(r => selected.has(r.table_name));
if (q) {
rows = rows.filter(r =>
(r.column_name || '').toLowerCase().includes(q) ||
(r.column_comment || '').toLowerCase().includes(q) ||
(r.table_name || '').toLowerCase().includes(q)
);
}
// 按 (table_name, ordinal_position) 排序
rows.sort((a, b) => {
const tn = (a.table_name || '').localeCompare(b.table_name || '');
if (tn !== 0) return tn;
return (a.ordinal_position || 0) - (b.ordinal_position || 0);
});
return rows;
});
// ── 数据字典:表格选择同步 ── // ── 数据字典:表格选择同步 ──
// 设计:form.tables 是 source of truth(set 语义),el-table 仅展示
// 当前 visible 的行 + 这些行的勾选状态。
// - 用户勾选/取消某行 → 更新 form.tables(不丢失不在 visible 的行)
// - 数据刷新 / 滚动翻页 → restoreTableSelection 把 el-table 的可见勾选
// 状态重对齐到 form.tables(reserve-selection 不够,因为 :data 引用变了)
function onTableSelectionChange(selectedRows) { function onTableSelectionChange(selectedRows) {
form.tables = selectedRows.map(r => r.table_name); const visibleNames = new Set((tableList.value || []).map(r => r.table_name));
const newSelectedNames = new Set((selectedRows || []).map(r => r.table_name));
const set = new Set(form.tables || []);
for (const tn of visibleNames) {
if (newSelectedNames.has(tn)) set.add(tn);
else set.delete(tn);
}
// 不在 visible 的 form.tables 项保持原样(保留跨页勾选)
form.tables = Array.from(set);
} }
function selectAllTables() { // 「全选」= 选中「当前筛选下」所有表(含未加载的)
if (!tablesTableRef.value) return; async function selectAllTables() {
tablesTableRef.value.clearSelection(); try {
for (const row of tablesAll.value) { const params = new URLSearchParams({
tablesTableRef.value.toggleRowSelection(row, true); ...buildDictConnParams(),
q: tableSearchCommitted.value,
});
const r = await fetch('/api/dict/all-table-names?' + params.toString())
.then(r => r.json());
if (r && r._warning) {
ElementPlus.ElMessage.warning(r._warning);
return;
}
form.tables = (r && r.names) || [];
// 把全量匹配拉回首屏,让用户能看到勾选生效
await fetchTablesPage(1, /* replace */ true);
} catch (e) {
ElementPlus.ElMessage.error('全选失败: ' + e);
} }
} }
function selectNoTables() { function selectNoTables() {
if (!tablesTableRef.value) return; form.tables = [];
tablesTableRef.value.clearSelection(); if (tablesTableRef.value) tablesTableRef.value.clearSelection();
} }
function selectTablesInvert() { // 「反选」= 把当前筛选下未选中的表加进 form.tables,把已选中的移出
if (!tablesTableRef.value) return; async function selectTablesInvert() {
const current = new Set(form.tables); try {
tablesTableRef.value.clearSelection(); const params = new URLSearchParams({
for (const row of tablesAll.value) { ...buildDictConnParams(),
if (!current.has(row.table_name)) { q: tableSearchCommitted.value,
tablesTableRef.value.toggleRowSelection(row, true); });
const r = await fetch('/api/dict/all-table-names?' + params.toString())
.then(r => r.json());
if (r && r._warning) {
ElementPlus.ElMessage.warning(r._warning);
return;
} }
const matched = (r && r.names) || [];
const current = new Set(form.tables);
form.tables = matched.filter(n => !current.has(n));
await fetchTablesPage(1, /* replace */ true);
} catch (e) {
ElementPlus.ElMessage.error('反选失败: ' + e);
} }
} }
// ── 断开重连:清空数据字典浏览器和已勾选的表 ── // ── 断开重连:清空数据字典浏览器和已勾选的表 ──
function disconnectDataDict() { function disconnectDataDict() {
connected.value = false; connected.value = false;
tableSummary.value = []; tableList.value = [];
dataDict.value = []; tablePage.value = 0;
tableHasMore.value = false;
tableTotal.value = 0;
tableLoading.value = false;
fieldList.value = [];
fieldPage.value = 0;
fieldHasMore.value = false;
fieldTotal.value = 0;
fieldLoading.value = false;
form.tables = []; form.tables = [];
jobResult.value = null; jobResult.value = null;
currentJob.value = null; currentJob.value = null;
...@@ -1669,7 +1926,6 @@ ...@@ -1669,7 +1926,6 @@
// ── 自定义规则 ── // ── 自定义规则 ──
// 点击字段表右侧 + 号:拷贝字段信息 + 字段 data_type(决定可用比较符) // 点击字段表右侧 + 号:拷贝字段信息 + 字段 data_type(决定可用比较符)
// 已存在的同一字段会被去重(防止用户重复点) // 已存在的同一字段会被去重(防止用户重复点)
// table_comment 兜底:字段行 row 可能不带,从 tableSummary 反查
// 默认名「自定义规则N」:扫描已用编号,找最小可用 N(用户手动改名后 N 仍递增) // 默认名「自定义规则N」:扫描已用编号,找最小可用 N(用户手动改名后 N 仍递增)
function nextDefaultRuleName() { function nextDefaultRuleName() {
const used = new Set( const used = new Set(
...@@ -1695,14 +1951,10 @@ ...@@ -1695,14 +1951,10 @@
); );
return; return;
} }
// 表注释兜底:字段行 row.table_comment 可能为空,从 tableSummary 反查 // 表注释兜底:2026-08-13 后端 /api/dict/fields 每行都附 table_comment
// (从 table_summary 查找,list_columns SQL 不带这列),
// 不再需要前端从 tableSummary 反查(tableSummary 已删除)
let tableComment = row.table_comment || ''; let tableComment = row.table_comment || '';
if (!tableComment) {
const t = (tableSummary.value || []).find(
x => x.table_name === row.table_name,
);
tableComment = (t && t.table_comment) || '';
}
const newRule = { const newRule = {
id: `rule_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, id: `rule_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
// 默认名「自定义规则N」:跳过当前已用的所有 N,找最小可用编号 // 默认名「自定义规则N」:跳过当前已用的所有 N,找最小可用编号
...@@ -1847,17 +2099,24 @@ ...@@ -1847,17 +2099,24 @@
testResult.value = { ok: r.ok, msg: r.message }; testResult.value = { ok: r.ok, msg: r.message };
connected.value = r.ok; connected.value = r.ok;
if (r.ok) { if (r.ok) {
tableSummary.value = r.table_summary || []; // 后端已经把全量字典写进 DataDictCache;前端按需分页拉取。
dataDict.value = r.data_dictionary || []; // r.table_summary / r.data_dictionary 仍在响应里给个 fallback
// (保持响应模型不变;前端不再使用)。
// 新数据库:清空勾选 // 新数据库:清空勾选
form.tables = []; form.tables = [];
// 等下一帧再清空 el-table 的内部选中状态 // 拉第一页:表列表 + 字段列表(fields 此时为空,因为 form.tables 是空的)
nextTick(() => { await Promise.all([
if (tablesTableRef.value) tablesTableRef.value.clearSelection(); fetchTablesPage(1, /* replace */ true),
}); fetchFieldsPage(1, /* replace */ true),
]);
await nextTick();
if (tablesTableRef.value) tablesTableRef.value.clearSelection();
} else { } else {
tableSummary.value = []; // 失败时清空本地状态
dataDict.value = []; tableList.value = [];
fieldList.value = [];
tableTotal.value = 0;
fieldTotal.value = 0;
} }
} catch (e) { } catch (e) {
testResult.value = { ok: false, msg: '请求失败: ' + e }; testResult.value = { ok: false, msg: '请求失败: ' + e };
...@@ -2296,9 +2555,15 @@ ...@@ -2296,9 +2555,15 @@
flatCheckRows, treeRowStyle, flatCheckRows, treeRowStyle,
toggleStep, toggleGroupCheck, toggleGroupExpand, toggleStep, toggleGroupCheck, toggleGroupExpand,
setAllChecks, setAllGroupsExpanded, setAllChecks, setAllGroupsExpanded,
tableSummary, dataDict, // 数据字典分页状态(2026-08-13 重构) ──
tableSearch, fieldSearch, tablesTableRef, tableList, tablePage, tableHasMore, tableTotal, tableLoading,
tablesAll, filteredTables, filteredFields, fieldCountByTable, fieldList, fieldPage, fieldHasMore, fieldTotal, fieldLoading,
TABLE_PAGE_SIZE, FIELD_PAGE_SIZE,
tableSearchDraft, tableSearchCommitted, applyTableSearch,
fieldSearchDraft, fieldSearchCommitted, applyFieldSearch,
tablesTableRef, fieldsTableRef,
fetchTablesPage, fetchFieldsPage,
onTablesScroll, onFieldsScroll,
onTableSelectionChange, selectAllTables, selectNoTables, selectTablesInvert, onTableSelectionChange, selectAllTables, selectNoTables, selectTablesInvert,
disconnectDataDict, disconnectDataDict,
currentJob, jobStatus, statusTagType, currentJob, jobStatus, statusTagType,
......
...@@ -3,6 +3,14 @@ ...@@ -3,6 +3,14 @@
display: flex; display: flex;
gap: 16px; gap: 16px;
} }
/* 2026-08-13:隐藏两侧表格的全选 checkbox(保留每行 checkbox)。
Element Plus 2.x 的 selection header 渲染在 .el-table__header 内 .el-checkbox
上;body 行每行也是 .el-checkbox → 只藏 thead 下的。 */
.dict-tables-table thead .el-checkbox,
.dict-fields-table thead .el-checkbox {
visibility: hidden;
pointer-events: none;
}
.dict-pane-left { .dict-pane-left {
flex: 0 0 560px; /* 4 列(表名 + 表注释 + 字段数 + selection)最小宽度约 520px,留出余量 */ flex: 0 0 560px; /* 4 列(表名 + 表注释 + 字段数 + selection)最小宽度约 520px,留出余量 */
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment