feat: 添加数据表字段搜索
This commit is contained in:
@@ -84,7 +84,7 @@
|
|||||||
<div v-if="selectedTable" class="database-toolbar">
|
<div v-if="selectedTable" class="database-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<span class="current-table">{{ selectedTable }}</span>
|
<span class="current-table">{{ selectedTable }}</span>
|
||||||
<span class="row-count">共 {{ total }} 条</span>
|
<span class="row-count">{{ hasActiveSearch ? `匹配 ${total} 条` : `共 ${total} 条` }}</span>
|
||||||
</div>
|
</div>
|
||||||
<n-space size="small" class="toolbar-actions">
|
<n-space size="small" class="toolbar-actions">
|
||||||
<n-button size="small" tertiary :loading="loadingData" @click="reloadTable">刷新</n-button>
|
<n-button size="small" tertiary :loading="loadingData" @click="reloadTable">刷新</n-button>
|
||||||
@@ -95,6 +95,37 @@
|
|||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</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 }">
|
<div class="database-data-body" :class="{ empty: !selectedTable }">
|
||||||
<n-empty v-if="!selectedTable" description="请选择左侧的数据表" />
|
<n-empty v-if="!selectedTable" description="请选择左侧的数据表" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -148,7 +179,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedTable" class="database-pagination">
|
<div v-if="selectedTable" class="database-pagination">
|
||||||
<span class="pagination-total">共 {{ total }} 条</span>
|
<span class="pagination-total">{{ hasActiveSearch ? `匹配 ${total} 条` : `共 ${total} 条` }}</span>
|
||||||
<n-pagination
|
<n-pagination
|
||||||
:page="page"
|
:page="page"
|
||||||
:page-count="totalPages"
|
:page-count="totalPages"
|
||||||
@@ -312,6 +343,7 @@ import {
|
|||||||
ChevronForwardOutline,
|
ChevronForwardOutline,
|
||||||
CloudDownloadOutline,
|
CloudDownloadOutline,
|
||||||
RefreshOutline,
|
RefreshOutline,
|
||||||
|
SearchOutline,
|
||||||
ServerOutline,
|
ServerOutline,
|
||||||
TrashOutline
|
TrashOutline
|
||||||
} from '@vicons/ionicons5';
|
} from '@vicons/ionicons5';
|
||||||
@@ -349,6 +381,10 @@ const rows = ref<RowData[]>([]);
|
|||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const pageSize = ref(50);
|
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 loadingTables = ref(false);
|
||||||
const loadingData = ref(false);
|
const loadingData = ref(false);
|
||||||
const testing = ref(false);
|
const testing = ref(false);
|
||||||
@@ -371,6 +407,13 @@ const importInputRef = ref<HTMLInputElement | null>(null);
|
|||||||
let tableLoadToken = 0;
|
let tableLoadToken = 0;
|
||||||
|
|
||||||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)));
|
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(() => (
|
const selectedDatabaseSourceLabel = computed(() => (
|
||||||
databaseSourceOptions.value.find(option => option.value === selectedDatabaseSource.value)?.title || '主数据库'
|
databaseSourceOptions.value.find(option => option.value === selectedDatabaseSource.value)?.title || '主数据库'
|
||||||
));
|
));
|
||||||
@@ -547,6 +590,7 @@ function resetTableState() {
|
|||||||
rows.value = [];
|
rows.value = [];
|
||||||
total.value = 0;
|
total.value = 0;
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
|
resetSearchState();
|
||||||
columnSchemaExpanded.value = false;
|
columnSchemaExpanded.value = false;
|
||||||
loadingTables.value = false;
|
loadingTables.value = false;
|
||||||
loadingData.value = false;
|
loadingData.value = false;
|
||||||
@@ -555,6 +599,7 @@ function resetTableState() {
|
|||||||
async function loadTable(table: string) {
|
async function loadTable(table: string) {
|
||||||
selectedTable.value = table;
|
selectedTable.value = table;
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
|
resetSearchState();
|
||||||
columnSchemaExpanded.value = false;
|
columnSchemaExpanded.value = false;
|
||||||
await Promise.all([loadTableInfo(table), loadTableData()]);
|
await Promise.all([loadTableInfo(table), loadTableData()]);
|
||||||
}
|
}
|
||||||
@@ -570,6 +615,10 @@ async function loadTableInfo(table: string) {
|
|||||||
table_name: table,
|
table_name: table,
|
||||||
database_source: selectedDatabaseSource.value
|
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) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '加载表结构失败');
|
message.error(error instanceof Error ? error.message : '加载表结构失败');
|
||||||
}
|
}
|
||||||
@@ -579,11 +628,15 @@ async function loadTableData() {
|
|||||||
if (!selectedTable.value) return;
|
if (!selectedTable.value) return;
|
||||||
loadingData.value = true;
|
loadingData.value = true;
|
||||||
try {
|
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,
|
table_name: selectedTable.value,
|
||||||
database_source: selectedDatabaseSource.value,
|
database_source: selectedDatabaseSource.value,
|
||||||
page: page.value,
|
page: page.value,
|
||||||
page_size: pageSize.value
|
page_size: pageSize.value,
|
||||||
|
filters
|
||||||
});
|
});
|
||||||
rows.value = result.data;
|
rows.value = result.data;
|
||||||
total.value = result.total;
|
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() {
|
function toggleColumnSchema() {
|
||||||
columnSchemaExpanded.value = !columnSchemaExpanded.value;
|
columnSchemaExpanded.value = !columnSchemaExpanded.value;
|
||||||
}
|
}
|
||||||
@@ -1192,6 +1272,24 @@ async function doImport() {
|
|||||||
flex: 0 0 auto;
|
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 {
|
.database-data-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -1451,5 +1549,16 @@ async function doImport() {
|
|||||||
.toolbar-actions {
|
.toolbar-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.database-searchbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.database-search-field,
|
||||||
|
.database-search-keyword {
|
||||||
|
width: auto;
|
||||||
|
min-width: 180px;
|
||||||
|
flex: 1 1 220px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user