fix: 优化数据管理导出入口

This commit is contained in:
2026-05-18 17:23:23 +08:00
parent c96c6c806b
commit f58a323692
2 changed files with 85 additions and 4 deletions
+7
View File
@@ -1,5 +1,12 @@
# 项目上下文记录
## 2026-05-18:优化数据管理表列表和导出入口
- `frontend/src/components/DatabasePanel.vue` 的 MySQL 表列表顶部新增“表”标题和刷新图标按钮,刷新按钮复用 `loadTables()` 并在加载中显示 loading,避免重复刷新。
- 数据表导出从独立 CSV/XLSX 按钮改为 Naive UI 下拉按钮,用户先选择 CSV 或 XLSX 格式再下载。
- 导出请求期间按钮显示 loading 和当前格式文案,并禁用下拉入口,避免大数据表导出等待期间被重复点击。
- 已执行 `npm run build`,构建通过;未启动浏览器或 headless Chrome。
## 2026-05-18:修复数值字段转换失败
- `app/processor.py` 会从 `ReportScript.sql` 的 `ALTER TABLE ... MODIFY COLUMN ... float/int` 自动补全缺失的字段类型提示,配置中未标 `Type` 的数值字段在导入阶段也会按数值清洗和落库。
+78 -4
View File
@@ -2,6 +2,23 @@
<div class="database-workspace">
<aside class="database-sidebar-pane">
<section class="database-table-pane">
<div class="database-table-pane-header">
<h2>表</h2>
<n-button
quaternary
circle
size="small"
:loading="loadingTables"
:disabled="loadingTables"
aria-label="刷新表列表"
title="刷新表列表"
@click="loadTables"
>
<template #icon>
<n-icon><RefreshOutline /></n-icon>
</template>
</n-button>
</div>
<div class="database-table-list-wrap">
<n-empty v-if="tables.length === 0 && !loadingTables" description="暂无数据表" />
<n-spin v-else-if="loadingTables" class="database-list-loading" size="small" />
@@ -57,8 +74,22 @@
</div>
<n-space size="small" class="toolbar-actions">
<n-button size="small" tertiary :loading="loadingData" @click="reloadTable">刷新</n-button>
<n-button size="small" tertiary @click="downloadTable('csv')">CSV</n-button>
<n-button size="small" tertiary @click="downloadTable('xlsx')">XLSX</n-button>
<n-dropdown
trigger="click"
:options="downloadOptions"
:disabled="downloading"
@select="downloadTable"
>
<n-button size="small" tertiary :loading="downloading" :disabled="downloading">
<template #icon>
<n-icon><CloudDownloadOutline /></n-icon>
</template>
<span class="download-button-content">
<span>{{ downloadButtonText }}</span>
<n-icon class="download-caret"><ChevronDownOutline /></n-icon>
</span>
</n-button>
</n-dropdown>
<n-button size="small" tertiary type="warning" @click="confirmTruncate">清空</n-button>
<n-button size="small" tertiary type="error" @click="confirmDrop">删除</n-button>
</n-space>
@@ -127,8 +158,8 @@
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { useDialog, useMessage } from 'naive-ui';
import type { DataTableColumns } from 'naive-ui';
import { TrashOutline } from '@vicons/ionicons5';
import type { DataTableColumns, DropdownOption } from 'naive-ui';
import { ChevronDownOutline, CloudDownloadOutline, RefreshOutline, TrashOutline } from '@vicons/ionicons5';
import { apiGet, apiPost, download } from '../api/client';
import type { ApiMessage, DatabaseInfo, TableData, TableInfo } from '../types';
@@ -150,9 +181,18 @@ const pageSize = ref(50);
const loadingTables = ref(false);
const loadingData = ref(false);
const testing = ref(false);
const downloadingFormat = ref<DownloadFormat | null>(null);
let tableLoadToken = 0;
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)));
const downloading = computed(() => Boolean(downloadingFormat.value));
const downloadButtonText = computed(() => (
downloadingFormat.value ? `导出 ${downloadingFormat.value.toUpperCase()}...` : '导出'
));
const downloadOptions: DropdownOption[] = [
{ label: 'CSV', key: 'csv' },
{ label: 'XLSX', key: 'xlsx' }
];
const loadDataInfileText = computed(() => {
if (testing.value || !databaseInfo.value) return '检测中';
return databaseInfo.value.load_data_infile ? '可用' : '未启用';
@@ -221,6 +261,8 @@ async function testConnection() {
}
async function loadTables() {
if (loadingTables.value) return;
const currentToken = ++tableLoadToken;
loadingTables.value = true;
try {
@@ -305,10 +347,15 @@ function changePageSize(value: number) {
async function downloadTable(format: DownloadFormat) {
if (!selectedTable.value) return;
if (downloadingFormat.value) return;
downloadingFormat.value = format;
try {
await download('/api/download', { table_name: selectedTable.value, format }, `${selectedTable.value}.${format}`);
} catch (error) {
message.error(error instanceof Error ? error.message : '导出失败');
} finally {
downloadingFormat.value = null;
}
}
@@ -425,6 +472,23 @@ function formatCell(value: unknown): string {
flex-direction: column;
}
.database-table-pane-header {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 44px;
padding: 10px 12px 10px 16px;
border-bottom: 1px solid var(--td-border-color-light);
}
.database-table-pane-header h2 {
margin: 0;
color: var(--td-text-color-primary);
font-size: 15px;
font-weight: 600;
line-height: 22px;
}
.database-table-list-wrap {
display: flex;
flex: 1;
@@ -615,6 +679,16 @@ function formatCell(value: unknown): string {
flex: 0 0 auto;
}
.download-button-content {
display: inline-flex;
align-items: center;
gap: 4px;
}
.download-caret {
font-size: 13px;
}
.database-data-body {
display: flex;
flex: 1;