fix: 优化数据管理导出入口
This commit is contained in:
@@ -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:修复数值字段转换失败
|
## 2026-05-18:修复数值字段转换失败
|
||||||
|
|
||||||
- `app/processor.py` 会从 `ReportScript.sql` 的 `ALTER TABLE ... MODIFY COLUMN ... float/int` 自动补全缺失的字段类型提示,配置中未标 `Type` 的数值字段在导入阶段也会按数值清洗和落库。
|
- `app/processor.py` 会从 `ReportScript.sql` 的 `ALTER TABLE ... MODIFY COLUMN ... float/int` 自动补全缺失的字段类型提示,配置中未标 `Type` 的数值字段在导入阶段也会按数值清洗和落库。
|
||||||
|
|||||||
@@ -2,6 +2,23 @@
|
|||||||
<div class="database-workspace">
|
<div class="database-workspace">
|
||||||
<aside class="database-sidebar-pane">
|
<aside class="database-sidebar-pane">
|
||||||
<section class="database-table-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">
|
<div class="database-table-list-wrap">
|
||||||
<n-empty v-if="tables.length === 0 && !loadingTables" description="暂无数据表" />
|
<n-empty v-if="tables.length === 0 && !loadingTables" description="暂无数据表" />
|
||||||
<n-spin v-else-if="loadingTables" class="database-list-loading" size="small" />
|
<n-spin v-else-if="loadingTables" class="database-list-loading" size="small" />
|
||||||
@@ -57,8 +74,22 @@
|
|||||||
</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>
|
||||||
<n-button size="small" tertiary @click="downloadTable('csv')">CSV</n-button>
|
<n-dropdown
|
||||||
<n-button size="small" tertiary @click="downloadTable('xlsx')">XLSX</n-button>
|
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="warning" @click="confirmTruncate">清空</n-button>
|
||||||
<n-button size="small" tertiary type="error" @click="confirmDrop">删除</n-button>
|
<n-button size="small" tertiary type="error" @click="confirmDrop">删除</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
@@ -127,8 +158,8 @@
|
|||||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue';
|
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { onBeforeRouteLeave } from 'vue-router';
|
import { onBeforeRouteLeave } from 'vue-router';
|
||||||
import { useDialog, useMessage } from 'naive-ui';
|
import { useDialog, useMessage } from 'naive-ui';
|
||||||
import type { DataTableColumns } from 'naive-ui';
|
import type { DataTableColumns, DropdownOption } from 'naive-ui';
|
||||||
import { TrashOutline } from '@vicons/ionicons5';
|
import { ChevronDownOutline, CloudDownloadOutline, RefreshOutline, TrashOutline } from '@vicons/ionicons5';
|
||||||
|
|
||||||
import { apiGet, apiPost, download } from '../api/client';
|
import { apiGet, apiPost, download } from '../api/client';
|
||||||
import type { ApiMessage, DatabaseInfo, TableData, TableInfo } from '../types';
|
import type { ApiMessage, DatabaseInfo, TableData, TableInfo } from '../types';
|
||||||
@@ -150,9 +181,18 @@ const pageSize = ref(50);
|
|||||||
const loadingTables = ref(false);
|
const loadingTables = ref(false);
|
||||||
const loadingData = ref(false);
|
const loadingData = ref(false);
|
||||||
const testing = ref(false);
|
const testing = ref(false);
|
||||||
|
const downloadingFormat = ref<DownloadFormat | 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 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(() => {
|
const loadDataInfileText = computed(() => {
|
||||||
if (testing.value || !databaseInfo.value) return '检测中';
|
if (testing.value || !databaseInfo.value) return '检测中';
|
||||||
return databaseInfo.value.load_data_infile ? '可用' : '未启用';
|
return databaseInfo.value.load_data_infile ? '可用' : '未启用';
|
||||||
@@ -221,6 +261,8 @@ async function testConnection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadTables() {
|
async function loadTables() {
|
||||||
|
if (loadingTables.value) return;
|
||||||
|
|
||||||
const currentToken = ++tableLoadToken;
|
const currentToken = ++tableLoadToken;
|
||||||
loadingTables.value = true;
|
loadingTables.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -305,10 +347,15 @@ function changePageSize(value: number) {
|
|||||||
|
|
||||||
async function downloadTable(format: DownloadFormat) {
|
async function downloadTable(format: DownloadFormat) {
|
||||||
if (!selectedTable.value) return;
|
if (!selectedTable.value) return;
|
||||||
|
if (downloadingFormat.value) return;
|
||||||
|
|
||||||
|
downloadingFormat.value = format;
|
||||||
try {
|
try {
|
||||||
await download('/api/download', { table_name: selectedTable.value, format }, `${selectedTable.value}.${format}`);
|
await download('/api/download', { table_name: selectedTable.value, format }, `${selectedTable.value}.${format}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '导出失败');
|
message.error(error instanceof Error ? error.message : '导出失败');
|
||||||
|
} finally {
|
||||||
|
downloadingFormat.value = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,6 +472,23 @@ function formatCell(value: unknown): string {
|
|||||||
flex-direction: column;
|
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 {
|
.database-table-list-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -615,6 +679,16 @@ function formatCell(value: unknown): string {
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.download-button-content {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-caret {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.database-data-body {
|
.database-data-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user