fix: 优化数据管理导出下拉样式

This commit is contained in:
2026-05-19 15:11:47 +08:00
parent b575298a3e
commit b809e8ee0a
5 changed files with 100 additions and 22 deletions
+7
View File
@@ -1,5 +1,12 @@
# 项目上下文记录 # 项目上下文记录
## 2026-05-19:优化数据管理导出入口和弹窗宽度
- `frontend/src/composables/pageHeader.ts` 和 `frontend/src/AppShell.vue` 为页面顶部动作支持 Naive UI 下拉菜单,按钮内显示下拉箭头。
- `frontend/src/components/DatabasePanel.vue` 将顶部“导出 CSV / 导出 XLSX”两个按钮合并为一个“导出”下拉按钮,点击后选择 CSV 或 XLSX 再进入对应表选择弹窗。
- CSV 和 XLSX 导出弹窗改为固定 420px 内的响应式宽度,避免在宽屏下铺满整页;表选择列表增加边框和背景,视觉上更集中。
- 已执行 `npm run build`,构建通过;构建只保留 Vite 原有大 chunk 提示。
## 2026-05-19:调整数据管理导出交互并支持多表 XLSX ## 2026-05-19:调整数据管理导出交互并支持多表 XLSX
- `frontend/src/components/DatabasePanel.vue` 将导出入口移到页面顶部,并放在“删除全部表”按钮左侧;内容区工具栏只保留刷新、清空、删除当前表。 - `frontend/src/components/DatabasePanel.vue` 将导出入口移到页面顶部,并放在“删除全部表”按钮左侧;内容区工具栏只保留刷新、清空、删除当前表。
+37 -1
View File
@@ -42,6 +42,31 @@
<span v-if="action.kind === 'text'" class="header-info-text"> <span v-if="action.kind === 'text'" class="header-info-text">
{{ actionLabel(action) }} {{ actionLabel(action) }}
</span> </span>
<n-dropdown
v-else-if="actionDropdownOptions(action).length"
trigger="click"
:options="actionDropdownOptions(action)"
:disabled="actionDisabled(action)"
@select="runHeaderActionSelect(action)"
>
<n-button
size="small"
:type="action.type === 'default' ? undefined : action.type"
:tertiary="action.variant !== 'solid'"
:text="action.variant === 'text'"
:loading="actionLoading(action)"
:disabled="actionDisabled(action)"
:title="actionTitle(action)"
>
<template v-if="actionIcon(action)" #icon>
<n-icon><component :is="actionIcon(action)" /></n-icon>
</template>
<span class="header-dropdown-label">
<span>{{ actionLabel(action) }}</span>
<n-icon class="header-dropdown-caret"><ChevronDownOutline /></n-icon>
</span>
</n-button>
</n-dropdown>
<n-button <n-button
v-else v-else
size="small" size="small"
@@ -97,8 +122,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, h, onBeforeUnmount, onMounted, ref, watch, type Component } from 'vue'; import { computed, h, onBeforeUnmount, onMounted, ref, watch, type Component } from 'vue';
import { RouterView, useRoute, useRouter } from 'vue-router'; import { RouterView, useRoute, useRouter } from 'vue-router';
import { useDialog, useMessage, type MenuOption, NIcon } from 'naive-ui'; import { useDialog, useMessage, type DropdownOption, type MenuOption, NIcon } from 'naive-ui';
import { import {
ChevronDownOutline,
CloudUploadOutline, CloudUploadOutline,
ConstructOutline, ConstructOutline,
FileTrayFullOutline, FileTrayFullOutline,
@@ -294,10 +320,20 @@ function actionDisabled(action: PageHeaderAction) {
return resolveHeaderValue(action.disabled, false); return resolveHeaderValue(action.disabled, false);
} }
function actionDropdownOptions(action: PageHeaderAction) {
return resolveHeaderValue(action.dropdownOptions, []);
}
function runHeaderAction(action: PageHeaderAction) { function runHeaderAction(action: PageHeaderAction) {
void action.onClick?.(); void action.onClick?.();
} }
function runHeaderActionSelect(action: PageHeaderAction) {
return (key: string | number, option: DropdownOption) => {
void action.onSelect?.(key, option);
};
}
function preventWindowFileDrop(event: DragEvent) { function preventWindowFileDrop(event: DragEvent) {
const types = Array.from(event.dataTransfer?.types || []); const types = Array.from(event.dataTransfer?.types || []);
if (!types.includes('Files')) { if (!types.includes('Files')) {
+43 -21
View File
@@ -146,7 +146,13 @@
</section> </section>
</div> </div>
<n-modal v-model:show="csvDialogVisible" preset="card" class="table-export-modal" title="导出 CSV"> <n-modal
v-model:show="csvDialogVisible"
preset="card"
class="table-export-modal"
title="导出 CSV"
style="width: min(420px, calc(100vw - 32px))"
>
<div class="export-dialog-body"> <div class="export-dialog-body">
<p class="export-dialog-hint">选择要导出的数据表。</p> <p class="export-dialog-hint">选择要导出的数据表。</p>
<n-scrollbar class="export-table-options"> <n-scrollbar class="export-table-options">
@@ -175,7 +181,13 @@
</template> </template>
</n-modal> </n-modal>
<n-modal v-model:show="xlsxDialogVisible" preset="card" class="table-export-modal" title="导出 XLSX"> <n-modal
v-model:show="xlsxDialogVisible"
preset="card"
class="table-export-modal"
title="导出 XLSX"
style="width: min(420px, calc(100vw - 32px))"
>
<div class="export-dialog-body"> <div class="export-dialog-body">
<p class="export-dialog-hint">选择一个或多个数据表,导出后会合并到同一个 XLSX 文件中。</p> <p class="export-dialog-hint">选择一个或多个数据表,导出后会合并到同一个 XLSX 文件中。</p>
<n-scrollbar class="export-table-options"> <n-scrollbar class="export-table-options">
@@ -209,7 +221,7 @@
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 { import {
ChevronForwardOutline, ChevronForwardOutline,
CloudDownloadOutline, CloudDownloadOutline,
@@ -248,9 +260,14 @@ 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 downloading = computed(() => Boolean(downloadingFormat.value));
const csvExportButtonText = computed(() => (downloadingFormat.value === 'csv' ? '导出 CSV...' : '导出 CSV')); const exportButtonText = computed(() => (
const xlsxExportButtonText = computed(() => (downloadingFormat.value === 'xlsx' ? '导出 XLSX...' : '导出 XLSX')); downloadingFormat.value ? `导出 ${downloadingFormat.value.toUpperCase()}...` : '导出'
));
const exportDisabled = computed(() => loadingTables.value || tables.value.length === 0 || downloading.value); const exportDisabled = computed(() => loadingTables.value || tables.value.length === 0 || downloading.value);
const exportOptions: 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 ? '可用' : '未启用';
@@ -281,24 +298,15 @@ onMounted(() => {
setPageHeader({ setPageHeader({
actions: [ actions: [
{ {
key: 'export-csv', key: 'export',
label: csvExportButtonText, label: exportButtonText,
icon: CloudDownloadOutline, icon: CloudDownloadOutline,
type: 'primary', type: 'primary',
variant: 'solid', variant: 'solid',
loading: computed(() => downloadingFormat.value === 'csv'), loading: downloading,
disabled: exportDisabled, disabled: exportDisabled,
onClick: showCsvExportDialog dropdownOptions: exportOptions,
}, onSelect: selectExportFormat
{
key: 'export-xlsx',
label: xlsxExportButtonText,
icon: CloudDownloadOutline,
type: 'primary',
variant: 'outline',
loading: computed(() => downloadingFormat.value === 'xlsx'),
disabled: exportDisabled,
onClick: showXlsxExportDialog
}, },
{ {
key: 'drop-all-tables', key: 'drop-all-tables',
@@ -434,6 +442,17 @@ function changePageSize(value: number) {
void loadTableData(); void loadTableData();
} }
function selectExportFormat(key: string | number) {
if (key === 'csv') {
void showCsvExportDialog();
return;
}
if (key === 'xlsx') {
void showXlsxExportDialog();
}
}
async function showCsvExportDialog() { async function showCsvExportDialog() {
await loadTables(); await loadTables();
if (tables.value.length === 0) { if (tables.value.length === 0) {
@@ -935,7 +954,7 @@ function formatCell(value: unknown): string {
} }
.table-export-modal { .table-export-modal {
width: min(520px, calc(100vw - 32px)); max-width: calc(100vw - 32px);
} }
.export-dialog-body { .export-dialog-body {
@@ -952,7 +971,10 @@ function formatCell(value: unknown): string {
.export-table-options { .export-table-options {
max-height: min(420px, 52vh); max-height: min(420px, 52vh);
padding: 6px 2px; padding: 8px 10px;
background: var(--td-bg-color-component);
border: 1px solid var(--td-border-color-light);
border-radius: var(--td-radius-default);
} }
.export-dialog-footer { .export-dialog-footer {
+3
View File
@@ -1,4 +1,5 @@
import { shallowReactive, unref, type Component, type Ref } from 'vue'; import { shallowReactive, unref, type Component, type Ref } from 'vue';
import type { DropdownOption } from 'naive-ui';
type HeaderValue<T> = T | Ref<T> | (() => T); type HeaderValue<T> = T | Ref<T> | (() => T);
@@ -12,7 +13,9 @@ export interface PageHeaderAction {
variant?: 'solid' | 'outline' | 'text'; variant?: 'solid' | 'outline' | 'text';
loading?: HeaderValue<boolean>; loading?: HeaderValue<boolean>;
disabled?: HeaderValue<boolean>; disabled?: HeaderValue<boolean>;
dropdownOptions?: HeaderValue<DropdownOption[]>;
onClick?: () => void | Promise<void>; onClick?: () => void | Promise<void>;
onSelect?: (key: string | number, option: DropdownOption) => void | Promise<void>;
} }
export const pageHeader = shallowReactive<{ export const pageHeader = shallowReactive<{
+10
View File
@@ -342,6 +342,16 @@ select {
white-space: nowrap; white-space: nowrap;
} }
.header-dropdown-label {
display: inline-flex;
align-items: center;
gap: 4px;
}
.header-dropdown-caret {
font-size: 13px;
}
.theme-toggle, .theme-toggle,
.restart-btn { .restart-btn {
display: inline-flex; display: inline-flex;