feat: 添加数据表字段搜索

This commit is contained in:
2026-08-26 16:15:55 +08:00
parent 4fc9a65676
commit e007776093
+113 -4
View File
@@ -84,7 +84,7 @@
<div v-if="selectedTable" class="database-toolbar">
<div class="toolbar-left">
<span class="current-table">{{ selectedTable }}</span>
<span class="row-count">共 {{ total }} 条</span>
<span class="row-count">{{ hasActiveSearch ? `匹配 ${total} 条` : `共 ${total} 条` }}</span>
</div>
<n-space size="small" class="toolbar-actions">
<n-button size="small" tertiary :loading="loadingData" @click="reloadTable">刷新</n-button>
@@ -95,6 +95,37 @@
</n-space>
</div>
<div v-if="selectedTable" class="database-searchbar">
<n-select
v-model:value="searchField"
class="database-search-field"
size="small"
filterable
:options="searchFieldOptions"
:consistent-menu-width="false"
placeholder="选择字段"
/>
<n-input
v-model:value="searchKeyword"
class="database-search-keyword"
size="small"
clearable
placeholder="输入关键字"
@keyup.enter="applySearch"
@clear="clearSearch"
/>
<n-button
size="small"
type="primary"
:disabled="!searchField || !searchKeyword.trim()"
@click="applySearch"
>
<template #icon><n-icon><SearchOutline /></n-icon></template>
搜索
</n-button>
<n-button v-if="hasActiveSearch" size="small" tertiary @click="clearSearch">清除</n-button>
</div>
<div class="database-data-body" :class="{ empty: !selectedTable }">
<n-empty v-if="!selectedTable" description="请选择左侧的数据表" />
<template v-else>
@@ -148,7 +179,7 @@
</div>
<div v-if="selectedTable" class="database-pagination">
<span class="pagination-total">共 {{ total }} 条</span>
<span class="pagination-total">{{ hasActiveSearch ? `匹配 ${total} 条` : `共 ${total} 条` }}</span>
<n-pagination
:page="page"
:page-count="totalPages"
@@ -312,6 +343,7 @@ import {
ChevronForwardOutline,
CloudDownloadOutline,
RefreshOutline,
SearchOutline,
ServerOutline,
TrashOutline
} from '@vicons/ionicons5';
@@ -349,6 +381,10 @@ const rows = ref<RowData[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(50);
const searchField = ref<string | null>(null);
const searchKeyword = ref('');
const activeSearchField = ref<string | null>(null);
const activeSearchKeyword = ref('');
const loadingTables = ref(false);
const loadingData = ref(false);
const testing = ref(false);
@@ -371,6 +407,13 @@ const importInputRef = ref<HTMLInputElement | null>(null);
let tableLoadToken = 0;
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)));
const hasActiveSearch = computed(() => Boolean(activeSearchField.value && activeSearchKeyword.value));
const searchFieldOptions = computed(() => (
tableInfo.value?.columns
.map(column => String(column.Field || ''))
.filter(Boolean)
.map(field => ({ label: field, value: field })) || []
));
const selectedDatabaseSourceLabel = computed(() => (
databaseSourceOptions.value.find(option => option.value === selectedDatabaseSource.value)?.title || '主数据库'
));
@@ -547,6 +590,7 @@ function resetTableState() {
rows.value = [];
total.value = 0;
page.value = 1;
resetSearchState();
columnSchemaExpanded.value = false;
loadingTables.value = false;
loadingData.value = false;
@@ -555,6 +599,7 @@ function resetTableState() {
async function loadTable(table: string) {
selectedTable.value = table;
page.value = 1;
resetSearchState();
columnSchemaExpanded.value = false;
await Promise.all([loadTableInfo(table), loadTableData()]);
}
@@ -570,6 +615,10 @@ async function loadTableInfo(table: string) {
table_name: table,
database_source: selectedDatabaseSource.value
});
const fields = tableInfo.value.columns.map(column => String(column.Field || '')).filter(Boolean);
if (!searchField.value || !fields.includes(searchField.value)) {
searchField.value = fields[0] || null;
}
} catch (error) {
message.error(error instanceof Error ? error.message : '加载表结构失败');
}
@@ -579,11 +628,15 @@ async function loadTableData() {
if (!selectedTable.value) return;
loadingData.value = true;
try {
const result = await apiPost<TableData>('/api/database/table/data', {
const filters = hasActiveSearch.value && activeSearchField.value
? { [activeSearchField.value]: activeSearchKeyword.value }
: {};
const result = await apiPost<TableData>('/api/database/table/query', {
table_name: selectedTable.value,
database_source: selectedDatabaseSource.value,
page: page.value,
page_size: pageSize.value
page_size: pageSize.value,
filters
});
rows.value = result.data;
total.value = result.total;
@@ -594,6 +647,33 @@ async function loadTableData() {
}
}
function resetSearchState() {
searchField.value = null;
searchKeyword.value = '';
activeSearchField.value = null;
activeSearchKeyword.value = '';
}
function applySearch() {
const keyword = searchKeyword.value.trim();
if (!searchField.value || !keyword) return;
activeSearchField.value = searchField.value;
activeSearchKeyword.value = keyword;
page.value = 1;
void loadTableData();
}
function clearSearch() {
const wasActive = hasActiveSearch.value;
searchKeyword.value = '';
activeSearchField.value = null;
activeSearchKeyword.value = '';
if (wasActive) {
page.value = 1;
void loadTableData();
}
}
function toggleColumnSchema() {
columnSchemaExpanded.value = !columnSchemaExpanded.value;
}
@@ -1192,6 +1272,24 @@ async function doImport() {
flex: 0 0 auto;
}
.database-searchbar {
display: flex;
min-height: 48px;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: var(--td-bg-color-container);
border-bottom: 1px solid var(--td-border-color-light);
}
.database-search-field {
width: min(220px, 28vw);
}
.database-search-keyword {
width: min(360px, 38vw);
}
.database-data-body {
display: flex;
flex: 1;
@@ -1451,5 +1549,16 @@ async function doImport() {
.toolbar-actions {
width: 100%;
}
.database-searchbar {
flex-wrap: wrap;
}
.database-search-field,
.database-search-keyword {
width: auto;
min-width: 180px;
flex: 1 1 220px;
}
}
</style>