fix: 恢复数据库页左右布局
This commit is contained in:
@@ -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:修复旧版前端未登录闪烁
|
||||
|
||||
- `frontend_old/js/app.js` 现在会在首页初始化前检查登录 token;未登录时直接跳到旧版登录页,避免首页继续初始化并反复请求 API 造成未授权提示闪烁。
|
||||
|
||||
@@ -1,96 +1,86 @@
|
||||
<template>
|
||||
<n-grid :cols="12" :x-gap="16" :y-gap="16" responsive="screen">
|
||||
<n-gi :span="12" :m="4">
|
||||
<n-space vertical size="large">
|
||||
<n-card title="数据库连接" size="small">
|
||||
<template #header-extra>
|
||||
<n-button size="small" tertiary :loading="testing" @click="testConnection">测试</n-button>
|
||||
</template>
|
||||
<n-space vertical>
|
||||
<n-alert v-if="databaseInfo" :type="databaseInfo.success ? 'success' : 'error'" :bordered="false">
|
||||
MySQL {{ databaseInfo.version || '-' }} / LOAD DATA:
|
||||
{{ databaseInfo.load_data_infile ? '可用' : '不可用' }}
|
||||
</n-alert>
|
||||
<n-button block :loading="loadingTables" @click="loadTables">刷新数据表</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
<div class="database-workspace">
|
||||
<aside class="database-sidebar-pane">
|
||||
<section class="database-table-pane">
|
||||
<div class="database-table-actions">
|
||||
<n-button size="small" tertiary :loading="loadingTables" @click="loadTables">刷新数据表</n-button>
|
||||
<n-button size="small" tertiary type="error" :disabled="tables.length === 0" @click="confirmDropAll">
|
||||
删除全部
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<n-card title="数据表" size="small">
|
||||
<template #header-extra>
|
||||
<n-button size="small" tertiary type="error" :disabled="tables.length === 0" @click="confirmDropAll">
|
||||
删除全部
|
||||
</n-button>
|
||||
</template>
|
||||
<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" />
|
||||
<n-scrollbar v-else class="database-table-list">
|
||||
<button
|
||||
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>
|
||||
<n-select
|
||||
v-model:value="selectedTable"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="选择数据表"
|
||||
:options="tableOptions"
|
||||
@update:value="handleSelectTable"
|
||||
/>
|
||||
<n-empty v-if="tables.length === 0 && !loadingTables" description="暂无数据表" />
|
||||
<n-scrollbar v-else class="table-list">
|
||||
<n-list bordered>
|
||||
<n-list-item v-for="table in tables" :key="table">
|
||||
<n-button text @click="loadTable(table)">{{ table }}</n-button>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-scrollbar>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-gi>
|
||||
<section class="database-server-info">
|
||||
<div class="server-info-item">
|
||||
<span class="info-label">版本:</span>
|
||||
<span class="info-value">
|
||||
<span class="status-badge" :class="databaseInfo?.success ? 'success' : 'unknown'">
|
||||
{{ databaseInfo?.version || '检测中' }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="server-info-item">
|
||||
<span class="info-label">快速导入:</span>
|
||||
<span class="info-value">
|
||||
<span class="status-badge" :class="databaseInfo?.load_data_infile ? 'success' : 'warning'">
|
||||
{{ databaseInfo ? (databaseInfo.load_data_infile ? '可用' : '未启用') : '检测中' }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<n-button size="tiny" text :loading="testing" class="connection-test" @click="testConnection">
|
||||
重新检测
|
||||
</n-button>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<n-gi :span="12" :m="8">
|
||||
<n-card title="表数据" size="small">
|
||||
<template #header-extra>
|
||||
<n-space>
|
||||
<n-button size="small" tertiary :disabled="!selectedTable" @click="reloadTable">刷新</n-button>
|
||||
<n-button size="small" tertiary :disabled="!selectedTable" @click="downloadTable('csv')">CSV</n-button>
|
||||
<n-button size="small" tertiary :disabled="!selectedTable" @click="downloadTable('xlsx')">XLSX</n-button>
|
||||
<n-button size="small" tertiary type="warning" :disabled="!selectedTable" @click="confirmTruncate">
|
||||
清空
|
||||
</n-button>
|
||||
<n-button size="small" tertiary type="error" :disabled="!selectedTable" @click="confirmDrop">
|
||||
删除
|
||||
</n-button>
|
||||
</n-space>
|
||||
</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>
|
||||
<section class="database-content-pane">
|
||||
<div v-if="selectedTable" class="database-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<span class="current-table">{{ selectedTable }}</span>
|
||||
<span class="row-count">共 {{ total }} 条</span>
|
||||
</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-button size="small" tertiary type="warning" @click="confirmTruncate">清空</n-button>
|
||||
<n-button size="small" tertiary type="error" @click="confirmDrop">删除</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<div class="database-data-body" :class="{ empty: !selectedTable }">
|
||||
<n-empty v-if="!selectedTable" description="请选择左侧的数据表" />
|
||||
<template v-else>
|
||||
<n-data-table
|
||||
size="small"
|
||||
class="database-data-table"
|
||||
:columns="columns"
|
||||
:data="rows"
|
||||
:loading="loadingData"
|
||||
:bordered="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-table size="small" :bordered="false">
|
||||
<thead>
|
||||
@@ -116,10 +106,23 @@
|
||||
</n-table>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -147,7 +150,6 @@ const loadingTables = ref(false);
|
||||
const loadingData = 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 columns = computed<DataTableColumns<RowData>>(() => {
|
||||
const keys = rows.value.length
|
||||
@@ -190,6 +192,7 @@ async function loadTables() {
|
||||
selectedTable.value = null;
|
||||
tableInfo.value = null;
|
||||
rows.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
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) {
|
||||
selectedTable.value = table;
|
||||
page.value = 1;
|
||||
@@ -310,9 +307,10 @@ async function dropTable() {
|
||||
}
|
||||
|
||||
function confirmDropAll() {
|
||||
if (tables.value.length === 0) return;
|
||||
dialog.error({
|
||||
title: '删除全部数据表',
|
||||
content: '确认删除当前数据库中的全部数据表?',
|
||||
content: `确认删除当前数据库中的全部 ${tables.value.length} 张数据表?`,
|
||||
positiveText: '删除全部',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: dropAllTables
|
||||
@@ -339,3 +337,305 @@ function formatCell(value: unknown): string {
|
||||
return String(value);
|
||||
}
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user