fix: 恢复数据库页左右布局

This commit is contained in:
2026-05-15 15:00:22 +08:00
parent 03718e6a62
commit da5c92733d
2 changed files with 398 additions and 91 deletions
+7
View File
@@ -1,5 +1,12 @@
# 项目上下文记录 # 项目上下文记录
## 2026-05-15:恢复新版数据库页左右工作区布局
- `frontend/src/components/DatabasePanel.vue` 已从纵向卡片堆叠改回旧版主体布局:左侧固定 240px 数据表列表,右侧为表数据面板,左下角显示数据库版本和快速导入状态。
- 数据库连接状态不再占用页面顶部;“重新检测”保留在左下状态块中,表列表刷新和删除全部放在左侧列表顶部。
- 右侧表数据区在未选择表时显示“请选择左侧的数据表”,选择表后显示表名、总行数、刷新、CSV、XLSX、清空、删除、数据表格、字段结构和分页。
- 已执行 `npm run build`;并用 headless Chrome 对照旧版 `http://127.0.0.1:9082/` 与新版 `http://127.0.0.1:9081/database`,确认两边数据库主体均为横向 flex,左栏宽 240px,右侧内容区占用剩余宽度。
## 2026-05-15:修复旧版前端未登录闪烁 ## 2026-05-15:修复旧版前端未登录闪烁
- `frontend_old/js/app.js` 现在会在首页初始化前检查登录 token;未登录时直接跳到旧版登录页,避免首页继续初始化并反复请求 API 造成未授权提示闪烁。 - `frontend_old/js/app.js` 现在会在首页初始化前检查登录 token;未登录时直接跳到旧版登录页,避免首页继续初始化并反复请求 API 造成未授权提示闪烁。
+391 -91
View File
@@ -1,96 +1,86 @@
<template> <template>
<n-grid :cols="12" :x-gap="16" :y-gap="16" responsive="screen"> <div class="database-workspace">
<n-gi :span="12" :m="4"> <aside class="database-sidebar-pane">
<n-space vertical size="large"> <section class="database-table-pane">
<n-card title="数据库连接" size="small"> <div class="database-table-actions">
<template #header-extra> <n-button size="small" tertiary :loading="loadingTables" @click="loadTables">刷新数据表</n-button>
<n-button size="small" tertiary :loading="testing" @click="testConnection">测试</n-button> <n-button size="small" tertiary type="error" :disabled="tables.length === 0" @click="confirmDropAll">
</template> 删除全部
<n-space vertical> </n-button>
<n-alert v-if="databaseInfo" :type="databaseInfo.success ? 'success' : 'error'" :bordered="false"> </div>
MySQL {{ databaseInfo.version || '-' }} / LOAD DATA:
{{ databaseInfo.load_data_infile ? '可用' : '不可用' }}
</n-alert>
<n-button block :loading="loadingTables" @click="loadTables">刷新数据表</n-button>
</n-space>
</n-card>
<n-card title="数据表" size="small"> <div class="database-table-list-wrap">
<template #header-extra> <n-empty v-if="tables.length === 0 && !loadingTables" description="暂无数据表" />
<n-button size="small" tertiary type="error" :disabled="tables.length === 0" @click="confirmDropAll"> <n-spin v-else-if="loadingTables" class="database-list-loading" size="small" />
删除全部 <n-scrollbar v-else class="database-table-list">
</n-button> <button
</template> v-for="table in tables"
:key="table"
type="button"
class="database-table-item"
:class="{ active: selectedTable === table }"
@click="loadTable(table)"
>
<span class="table-dot"></span>
<span class="table-name">{{ table }}</span>
</button>
</n-scrollbar>
</div>
</section>
<n-space vertical> <section class="database-server-info">
<n-select <div class="server-info-item">
v-model:value="selectedTable" <span class="info-label">版本:</span>
filterable <span class="info-value">
clearable <span class="status-badge" :class="databaseInfo?.success ? 'success' : 'unknown'">
placeholder="选择数据表" {{ databaseInfo?.version || '检测中' }}
:options="tableOptions" </span>
@update:value="handleSelectTable" </span>
/> </div>
<n-empty v-if="tables.length === 0 && !loadingTables" description="暂无数据表" /> <div class="server-info-item">
<n-scrollbar v-else class="table-list"> <span class="info-label">快速导入:</span>
<n-list bordered> <span class="info-value">
<n-list-item v-for="table in tables" :key="table"> <span class="status-badge" :class="databaseInfo?.load_data_infile ? 'success' : 'warning'">
<n-button text @click="loadTable(table)">{{ table }}</n-button> {{ databaseInfo ? (databaseInfo.load_data_infile ? '可用' : '未启用') : '检测中' }}
</n-list-item> </span>
</n-list> </span>
</n-scrollbar> </div>
</n-space> <n-button size="tiny" text :loading="testing" class="connection-test" @click="testConnection">
</n-card> 重新检测
</n-space> </n-button>
</n-gi> </section>
</aside>
<n-gi :span="12" :m="8"> <section class="database-content-pane">
<n-card title="表数据" size="small"> <div v-if="selectedTable" class="database-toolbar">
<template #header-extra> <div class="toolbar-left">
<n-space> <span class="current-table">{{ selectedTable }}</span>
<n-button size="small" tertiary :disabled="!selectedTable" @click="reloadTable">刷新</n-button> <span class="row-count">共 {{ total }} 条</span>
<n-button size="small" tertiary :disabled="!selectedTable" @click="downloadTable('csv')">CSV</n-button> </div>
<n-button size="small" tertiary :disabled="!selectedTable" @click="downloadTable('xlsx')">XLSX</n-button> <n-space size="small" class="toolbar-actions">
<n-button size="small" tertiary type="warning" :disabled="!selectedTable" @click="confirmTruncate"> <n-button size="small" tertiary :loading="loadingData" @click="reloadTable">刷新</n-button>
清空 <n-button size="small" tertiary @click="downloadTable('csv')">CSV</n-button>
</n-button> <n-button size="small" tertiary @click="downloadTable('xlsx')">XLSX</n-button>
<n-button size="small" tertiary type="error" :disabled="!selectedTable" @click="confirmDrop"> <n-button size="small" tertiary type="warning" @click="confirmTruncate">清空</n-button>
删除 <n-button size="small" tertiary type="error" @click="confirmDrop">删除</n-button>
</n-button> </n-space>
</n-space> </div>
</template>
<n-empty v-if="!selectedTable" description="请选择数据表" />
<n-space v-else vertical size="large">
<n-descriptions v-if="tableInfo" bordered size="small" :column="3">
<n-descriptions-item label="表名">{{ tableInfo.name }}</n-descriptions-item>
<n-descriptions-item label="行数">{{ tableInfo.row_count }}</n-descriptions-item>
<n-descriptions-item label="字段数">{{ tableInfo.columns.length }}</n-descriptions-item>
</n-descriptions>
<div class="database-data-body" :class="{ empty: !selectedTable }">
<n-empty v-if="!selectedTable" description="请选择左侧的数据表" />
<template v-else>
<n-data-table <n-data-table
size="small" size="small"
class="database-data-table"
:columns="columns" :columns="columns"
:data="rows" :data="rows"
:loading="loadingData" :loading="loadingData"
:bordered="false" :bordered="false"
:single-line="false" :single-line="false"
max-height="420" flex-height
/> />
<div class="table-footer">
<span>共 {{ total }} 行</span>
<n-pagination
:page="page"
:page-count="totalPages"
:page-size="pageSize"
show-size-picker
:page-sizes="[20, 50, 100, 200]"
@update:page="changePage"
@update:page-size="changePageSize"
/>
</div>
<n-collapse v-if="tableInfo"> <n-collapse v-if="tableInfo" class="columns-collapse">
<n-collapse-item title="字段结构" name="columns"> <n-collapse-item title="字段结构" name="columns">
<n-table size="small" :bordered="false"> <n-table size="small" :bordered="false">
<thead> <thead>
@@ -116,10 +106,23 @@
</n-table> </n-table>
</n-collapse-item> </n-collapse-item>
</n-collapse> </n-collapse>
</n-space> </template>
</n-card> </div>
</n-gi>
</n-grid> <div v-if="selectedTable" class="database-pagination">
<span class="pagination-total">共 {{ total }} 条</span>
<n-pagination
:page="page"
:page-count="totalPages"
:page-size="pageSize"
show-size-picker
:page-sizes="[20, 50, 100, 200]"
@update:page="changePage"
@update:page-size="changePageSize"
/>
</div>
</section>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -147,7 +150,6 @@ const loadingTables = ref(false);
const loadingData = ref(false); const loadingData = ref(false);
const testing = ref(false); const testing = ref(false);
const tableOptions = computed(() => tables.value.map(table => ({ label: table, value: table })));
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 columns = computed<DataTableColumns<RowData>>(() => { const columns = computed<DataTableColumns<RowData>>(() => {
const keys = rows.value.length const keys = rows.value.length
@@ -190,6 +192,7 @@ async function loadTables() {
selectedTable.value = null; selectedTable.value = null;
tableInfo.value = null; tableInfo.value = null;
rows.value = []; rows.value = [];
total.value = 0;
} }
} catch (error) { } catch (error) {
message.error(error instanceof Error ? error.message : '加载数据表失败'); message.error(error instanceof Error ? error.message : '加载数据表失败');
@@ -198,12 +201,6 @@ async function loadTables() {
} }
} }
function handleSelectTable(value: string | null) {
if (value) {
void loadTable(value);
}
}
async function loadTable(table: string) { async function loadTable(table: string) {
selectedTable.value = table; selectedTable.value = table;
page.value = 1; page.value = 1;
@@ -310,9 +307,10 @@ async function dropTable() {
} }
function confirmDropAll() { function confirmDropAll() {
if (tables.value.length === 0) return;
dialog.error({ dialog.error({
title: '删除全部数据表', title: '删除全部数据表',
content: '确认删除当前数据库中的全部数据表?', content: `确认删除当前数据库中的全部 ${tables.value.length} 张数据表?`,
positiveText: '删除全部', positiveText: '删除全部',
negativeText: '取消', negativeText: '取消',
onPositiveClick: dropAllTables onPositiveClick: dropAllTables
@@ -339,3 +337,305 @@ function formatCell(value: unknown): string {
return String(value); return String(value);
} }
</script> </script>
<style scoped>
.database-workspace {
display: flex;
gap: 20px;
height: calc(100vh - 92px);
min-height: 560px;
min-width: 0;
}
.database-sidebar-pane {
display: flex;
flex: 0 0 240px;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.database-table-pane,
.database-content-pane,
.database-server-info {
background: #fff;
border: 1px solid #dfe4ec;
border-radius: 6px;
}
.database-table-pane {
display: flex;
flex: 1;
min-height: 0;
flex-direction: column;
}
.database-table-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 46px;
padding: 8px 12px;
border-bottom: 1px solid #e6eaf0;
}
.database-table-list-wrap {
display: flex;
flex: 1;
min-height: 0;
align-items: stretch;
justify-content: center;
}
.database-list-loading {
align-self: center;
}
.database-table-list {
width: 100%;
height: 100%;
}
.database-table-item {
display: flex;
width: 100%;
min-height: 38px;
align-items: center;
gap: 10px;
padding: 8px 14px;
color: #1f2933;
font: inherit;
text-align: left;
background: transparent;
border: 0;
border-bottom: 1px solid #e8ecf2;
cursor: pointer;
}
.database-table-item:hover {
background: #f4f7fb;
}
.database-table-item.active {
color: #0052d9;
font-weight: 600;
background: #ecf2fe;
}
.table-dot {
width: 7px;
height: 7px;
flex: 0 0 auto;
background: #9aa4b2;
border-radius: 50%;
}
.database-table-item.active .table-dot {
background: #0052d9;
}
.table-name {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.database-server-info {
position: relative;
flex-shrink: 0;
padding: 10px 12px 28px;
font-size: 12px;
background: #f8fafc;
}
.server-info-item {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 28px;
gap: 10px;
}
.server-info-item:not(:last-of-type) {
border-bottom: 1px dashed #dfe4ec;
}
.info-label {
color: #4f5b6b;
}
.info-value {
min-width: 84px;
text-align: right;
}
.status-badge {
display: inline-block;
max-width: 112px;
overflow: hidden;
padding: 2px 8px;
font-size: 11px;
font-weight: 500;
white-space: nowrap;
text-overflow: ellipsis;
border-radius: 10px;
}
.status-badge.success {
color: #00a870;
background: #dff7ed;
}
.status-badge.warning {
color: #ed7b2f;
background: #fff1e6;
}
.status-badge.unknown {
color: #8b95a4;
background: #edf1f6;
}
.connection-test {
position: absolute;
right: 10px;
bottom: 6px;
}
.database-content-pane {
display: flex;
flex: 1;
min-width: 0;
overflow: hidden;
flex-direction: column;
}
.database-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 48px;
padding: 8px 14px;
background: #f8fafc;
border-bottom: 1px solid #e6eaf0;
}
.toolbar-left {
display: flex;
align-items: center;
min-width: 0;
gap: 10px;
}
.current-table {
overflow: hidden;
max-width: 42vw;
color: #1f2933;
font-weight: 700;
white-space: nowrap;
text-overflow: ellipsis;
}
.row-count {
flex: 0 0 auto;
padding: 2px 8px;
color: #667386;
font-size: 12px;
background: #edf1f6;
border-radius: 10px;
}
.toolbar-actions {
flex: 0 0 auto;
}
.database-data-body {
display: flex;
flex: 1;
min-height: 0;
overflow: auto;
flex-direction: column;
}
.database-data-body.empty {
align-items: center;
justify-content: center;
}
.database-data-table {
flex: 1;
min-height: 0;
}
.columns-collapse {
flex: 0 0 auto;
padding: 0 14px 12px;
border-top: 1px solid #e6eaf0;
}
.database-pagination {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 48px;
padding: 8px 14px;
background: #f8fafc;
border-top: 1px solid #e6eaf0;
}
.pagination-total {
flex: 0 0 auto;
color: #667386;
font-size: 12px;
}
@media (max-width: 1180px) {
.database-workspace {
flex-direction: column;
height: auto;
min-height: 0;
}
.database-sidebar-pane {
flex: none;
width: 100%;
flex-direction: row;
}
.database-table-pane {
min-height: 240px;
}
.database-server-info {
flex: 0 0 240px;
}
.database-content-pane {
min-height: 560px;
}
}
@media (max-width: 720px) {
.database-sidebar-pane {
flex-direction: column;
}
.database-server-info {
flex: none;
}
.database-toolbar,
.database-pagination {
align-items: flex-start;
flex-direction: column;
}
.toolbar-actions {
width: 100%;
}
}
</style>