- 修复无数据时重复显示空状态的问题 - 将DDL展示改为可编辑的textarea - 优化空状态UI样式和提示文案 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
591 lines
23 KiB
Vue
591 lines
23 KiB
Vue
<script setup lang="ts">
|
||
import { useDatabase } from './database/useDatabase'
|
||
import './database/database.css'
|
||
|
||
const {
|
||
// State
|
||
databases,
|
||
loading,
|
||
editingDb,
|
||
isEditing,
|
||
isCreating,
|
||
searchQuery,
|
||
isMapping,
|
||
mappingDb,
|
||
tables,
|
||
tableLoading,
|
||
tableSearchQuery,
|
||
selectedTables,
|
||
tablePage,
|
||
tablePageSize,
|
||
currentTableIndex,
|
||
mappingStep,
|
||
newDbForm,
|
||
editForm,
|
||
dbTypes,
|
||
// Computed
|
||
selectedTable,
|
||
filteredDatabases,
|
||
paginatedTables,
|
||
totalFilteredTables,
|
||
// Methods
|
||
isTableSelected,
|
||
toggleTableSelection,
|
||
selectAllTables,
|
||
clearAllTables,
|
||
prevTable,
|
||
nextTable,
|
||
fetchDatabases,
|
||
openCreate,
|
||
closeCreate,
|
||
closeMapping,
|
||
testConnect,
|
||
openMapping,
|
||
goToStep2,
|
||
goToStep1,
|
||
saveMapping,
|
||
openEdit,
|
||
saveEdit,
|
||
cancelEdit,
|
||
deleteDb,
|
||
} = useDatabase()
|
||
</script>
|
||
|
||
<template>
|
||
<!-- 主内容区域 -->
|
||
<div class="p-6 min-h-screen">
|
||
<!-- 顶部导航 -->
|
||
<div class="flex justify-between items-center mb-6">
|
||
<div class="flex items-center gap-2">
|
||
<i class="fa-solid fa-database text-gray-400"></i>
|
||
<span class="font-medium">Database</span>
|
||
</div>
|
||
<button @click="openCreate" class="btn-primary">
|
||
<i class="fa-solid fa-plus"></i>
|
||
New Connection
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 搜索 -->
|
||
<div class="flex gap-4 mb-6">
|
||
<div class="flex-1 relative">
|
||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||
<input
|
||
v-model="searchQuery"
|
||
type="text"
|
||
placeholder="Search databases by name, type or host..."
|
||
class="search-input w-full"
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Database 列表 -->
|
||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||
<div v-if="loading" class="py-12 text-center text-gray-400">
|
||
<i class="fa-solid fa-circle-notch fa-spin text-2xl mb-2"></i>
|
||
<p>Loading...</p>
|
||
</div>
|
||
<table v-else-if="filteredDatabases().length > 0" class="w-full">
|
||
<thead class="bg-dark-600">
|
||
<tr>
|
||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Database Name</th>
|
||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Type</th>
|
||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Host</th>
|
||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Tables</th>
|
||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Created</th>
|
||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="db in filteredDatabases()" :key="db.id" class="table-row">
|
||
<td class="px-5 py-4">
|
||
<div class="font-medium">{{ db.name }}</div>
|
||
<div class="text-sm text-gray-500">{{ db.description }}</div>
|
||
</td>
|
||
<td class="px-5 py-4 text-center">
|
||
<span class="bg-dark-500 px-2 py-1 rounded text-sm">{{ db.db_type }}</span>
|
||
</td>
|
||
<td class="px-5 py-4 text-center text-gray-400 text-sm">
|
||
{{ db.host }}:{{ db.port }}
|
||
</td>
|
||
<td class="px-5 py-4 text-center">
|
||
<span class="text-primary-cyan">{{ db.table_count }}</span>
|
||
</td>
|
||
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ db.created_at?.split('T')[0] }}</td>
|
||
<td class="px-5 py-4">
|
||
<div class="flex items-center justify-center gap-2">
|
||
<button
|
||
@click="openMapping(db)"
|
||
class="btn-icon"
|
||
title="Map Tables"
|
||
>
|
||
<i class="fa-solid fa-link text-gray-400"></i>
|
||
</button>
|
||
<button
|
||
@click="openEdit(db)"
|
||
class="btn-icon"
|
||
title="Edit"
|
||
>
|
||
<i class="fa-solid fa-pen text-gray-400"></i>
|
||
</button>
|
||
<button
|
||
@click="deleteDb(db.id)"
|
||
class="btn-icon"
|
||
title="Delete"
|
||
>
|
||
<i class="fa-solid fa-trash text-gray-400 hover:text-primary-danger"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-else class="empty-box">
|
||
<div class="empty-icon">
|
||
<i class="fa-solid fa-database"></i>
|
||
</div>
|
||
<p class="empty-text">No databases found</p>
|
||
<p class="empty-tip">Click "New Connection" to add a database</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑弹窗 -->
|
||
<Teleport to="body">
|
||
<div v-if="isEditing" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50 modal-overlay">
|
||
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl modal-content">
|
||
<!-- 弹窗头部 -->
|
||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||
<h3 class="text-lg font-semibold">Edit Database</h3>
|
||
<button @click="cancelEdit" class="btn-icon">
|
||
<i class="fa-solid fa-xmark text-xl"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 弹窗内容 -->
|
||
<div class="p-5 space-y-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Database Name</label>
|
||
<input
|
||
v-model="editForm.name"
|
||
type="text"
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||
<textarea
|
||
v-model="editForm.description"
|
||
rows="2"
|
||
class="input-field resize-none"
|
||
></textarea>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Type</label>
|
||
<el-select v-model="editForm.db_type" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||
<el-option v-for="type in dbTypes" :key="type" :label="type" :value="type" />
|
||
</el-select>
|
||
</div>
|
||
|
||
<!-- 连接凭据 -->
|
||
<div class="space-y-4 pt-2">
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Host</label>
|
||
<input
|
||
v-model="editForm.host"
|
||
type="text"
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Port</label>
|
||
<input
|
||
v-model="editForm.port"
|
||
type="number"
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Database</label>
|
||
<input
|
||
v-model="editForm.database"
|
||
type="text"
|
||
placeholder="Database name..."
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">User</label>
|
||
<input
|
||
v-model="editForm.username"
|
||
type="text"
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Password</label>
|
||
<input
|
||
v-model="editForm.password"
|
||
type="password"
|
||
placeholder="Leave blank to keep current"
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 弹窗底部 -->
|
||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||
<button
|
||
@click="cancelEdit"
|
||
class="btn-secondary"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
@click="saveEdit"
|
||
class="btn-primary"
|
||
>
|
||
Save Changes
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
|
||
<!-- 新建 Database 弹窗 -->
|
||
<Teleport to="body">
|
||
<div v-if="isCreating" class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4 modal-overlay">
|
||
<div class="bg-dark-800 rounded-2xl w-full max-w-lg border border-dark-600 shadow-2xl overflow-hidden modal-content">
|
||
<!-- 弹窗头部 -->
|
||
<div class="flex items-center justify-between p-5 border-b border-dark-600 bg-dark-700/50">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center">
|
||
<i class="fa-solid fa-database text-white"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-xl font-semibold text-white">Create New Connection</h3>
|
||
<p class="text-sm text-gray-400">Configure your database connection</p>
|
||
</div>
|
||
</div>
|
||
<button @click="closeCreate" class="btn-icon">
|
||
<i class="fa-solid fa-xmark text-xl"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 弹窗内容 -->
|
||
<div class="p-5 space-y-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Database Name</label>
|
||
<input
|
||
v-model="newDbForm.name"
|
||
type="text"
|
||
placeholder="Enter database name..."
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||
<textarea
|
||
v-model="newDbForm.description"
|
||
rows="2"
|
||
placeholder="Describe this database..."
|
||
class="input-field resize-none"
|
||
></textarea>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Type</label>
|
||
<el-select v-model="newDbForm.db_type" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||
<el-option v-for="type in dbTypes" :key="type" :label="type" :value="type" />
|
||
</el-select>
|
||
</div>
|
||
|
||
<!-- 连接凭据 - 当选择MySQL时显示 -->
|
||
<div v-if="newDbForm.db_type === 'MySQL'" class="space-y-4 pt-2">
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Host</label>
|
||
<input
|
||
v-model="newDbForm.host"
|
||
type="text"
|
||
placeholder="localhost"
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Port</label>
|
||
<input
|
||
v-model="newDbForm.port"
|
||
type="number"
|
||
placeholder="3306"
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Database</label>
|
||
<input
|
||
v-model="newDbForm.database"
|
||
type="text"
|
||
placeholder="Database name..."
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">User</label>
|
||
<input
|
||
v-model="newDbForm.username"
|
||
type="text"
|
||
placeholder="root"
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Password</label>
|
||
<input
|
||
v-model="newDbForm.password"
|
||
type="password"
|
||
placeholder="Enter password..."
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 连接凭据 - 当选择Neo4j时显示 -->
|
||
<div v-if="newDbForm.db_type === 'Neo4j'" class="space-y-4 pt-2">
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Host</label>
|
||
<input
|
||
v-model="newDbForm.host"
|
||
type="text"
|
||
placeholder="localhost"
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Port</label>
|
||
<input
|
||
v-model="newDbForm.port"
|
||
type="number"
|
||
placeholder="7687"
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Database</label>
|
||
<input
|
||
v-model="newDbForm.database"
|
||
type="text"
|
||
placeholder="neo4j (default)"
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">User</label>
|
||
<input
|
||
v-model="newDbForm.username"
|
||
type="text"
|
||
placeholder="neo4j"
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Password</label>
|
||
<input
|
||
v-model="newDbForm.password"
|
||
type="password"
|
||
placeholder="Enter password..."
|
||
class="input-field"
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部操作栏 -->
|
||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-600 bg-dark-700/50">
|
||
<button
|
||
@click="closeCreate"
|
||
class="btn-secondary"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
@click="testConnect"
|
||
class="btn-primary"
|
||
>
|
||
<i class="fa-solid fa-plug"></i>
|
||
Connect
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
|
||
<!-- 表映射弹窗 -->
|
||
<Teleport to="body">
|
||
<div v-if="isMapping" class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4 modal-overlay">
|
||
<div class="bg-dark-800 rounded-2xl w-full max-w-6xl h-[85vh] border border-dark-600 shadow-2xl overflow-hidden flex flex-col modal-content">
|
||
<!-- 弹窗头部 -->
|
||
<div class="flex items-center justify-between p-5 border-b border-dark-600 bg-dark-700/50">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-cyan to-blue-500 flex items-center justify-center">
|
||
<i class="fa-solid fa-table text-white"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-xl font-semibold text-white">Table Mapping</h3>
|
||
<p class="text-sm text-gray-400">
|
||
<span v-if="mappingStep === 1">Step 1: Select tables to map</span>
|
||
<span v-else>Step 2: Edit DDL and field mapping</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button @click="closeMapping" class="btn-icon">
|
||
<i class="fa-solid fa-xmark text-xl"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 步骤指示器 -->
|
||
<div class="flex items-center justify-center gap-4 py-4 border-b border-dark-600">
|
||
<div class="flex items-center gap-2">
|
||
<div :class="['w-8 h-8 rounded-full flex items-center justify-center text-sm', mappingStep >= 1 ? 'bg-primary-cyan text-white' : 'bg-dark-600 text-gray-400']">1</div>
|
||
<span :class="mappingStep >= 1 ? 'text-white' : 'text-gray-400'">Select Tables</span>
|
||
</div>
|
||
<div class="w-16 h-0.5" :class="mappingStep >= 2 ? 'bg-primary-cyan' : 'bg-dark-600'"></div>
|
||
<div class="flex items-center gap-2">
|
||
<div :class="['w-8 h-8 rounded-full flex items-center justify-center text-sm', mappingStep >= 2 ? 'bg-primary-cyan text-white' : 'bg-dark-600 text-gray-400']">2</div>
|
||
<span :class="mappingStep >= 2 ? 'text-white' : 'text-gray-400'">Edit Mapping</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 弹窗内容 -->
|
||
<div class="flex-1 flex flex-col overflow-hidden">
|
||
<!-- 步骤1:选择表 -->
|
||
<template v-if="mappingStep === 1">
|
||
<div class="p-4 flex-1 overflow-auto">
|
||
<div class="mb-3">
|
||
<span class="text-sm font-medium text-gray-300">Select Tables ({{ selectedTables.length }} selected):</span>
|
||
</div>
|
||
<div class="relative mb-3">
|
||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||
<input
|
||
v-model="tableSearchQuery"
|
||
type="text"
|
||
placeholder="Filter tables by name..."
|
||
class="search-input w-full"
|
||
>
|
||
</div>
|
||
<!-- 表选择列表 -->
|
||
<div class="bg-dark-900 rounded-lg p-2 max-h-80 overflow-auto">
|
||
<div
|
||
v-for="table in paginatedTables"
|
||
:key="table.name"
|
||
class="flex items-center gap-3 p-3 rounded-lg cursor-pointer"
|
||
@click="toggleTableSelection(table)"
|
||
>
|
||
<el-checkbox
|
||
:model-value="isTableSelected(table.name)"
|
||
@click.stop
|
||
@change="toggleTableSelection(table)"
|
||
class="table-checkbox"
|
||
/>
|
||
<span class="font-medium text-white">{{ table.name }}</span>
|
||
<span class="text-xs text-gray-500">{{ table.columns.length }} columns</span>
|
||
</div>
|
||
<div v-if="paginatedTables.length === 0" class="py-8 text-center text-gray-500">
|
||
No tables found
|
||
</div>
|
||
</div>
|
||
<!-- 分页 -->
|
||
<div class="flex justify-center mt-3">
|
||
<el-pagination
|
||
v-if="totalFilteredTables > tablePageSize"
|
||
v-model:current-page="tablePage"
|
||
:page-size="tablePageSize"
|
||
:total="totalFilteredTables"
|
||
layout="prev, pager, next"
|
||
background
|
||
small
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 步骤2:DDL编辑 -->
|
||
<template v-if="mappingStep === 2">
|
||
<!-- DDL 编辑区域 -->
|
||
<div class="flex-1 p-4 overflow-hidden">
|
||
<div class="flex items-center justify-between mb-3">
|
||
<span class="text-sm font-medium text-gray-300">DDL - {{ selectedTable?.name }}</span>
|
||
<div class="flex items-center gap-2">
|
||
<button
|
||
v-if="selectedTables.length > 1"
|
||
@click="prevTable"
|
||
class="p-1 hover:bg-dark-700 rounded"
|
||
:disabled="currentTableIndex <= 0"
|
||
>
|
||
<i class="fa-solid fa-chevron-left text-gray-400"></i>
|
||
</button>
|
||
<span class="text-xs text-gray-500">{{ currentTableIndex + 1 }} / {{ selectedTables.length }}</span>
|
||
<button
|
||
v-if="selectedTables.length > 1"
|
||
@click="nextTable"
|
||
class="p-1 hover:bg-dark-700 rounded"
|
||
:disabled="currentTableIndex >= selectedTables.length - 1"
|
||
>
|
||
<i class="fa-solid fa-chevron-right text-gray-400"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div v-if="selectedTable" class="h-[calc(100%-40px)]">
|
||
<textarea
|
||
v-model="selectedTable.ddl"
|
||
class="w-full h-full bg-dark-950 border border-dark-500 rounded-lg p-4 text-xs text-primary-cyan font-mono whitespace-pre-wrap resize-none focus:outline-none focus:border-primary-cyan focus:ring-1 focus:ring-primary-cyan/30 overflow-auto"
|
||
placeholder="Enter DDL..."
|
||
></textarea>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 底部操作栏 -->
|
||
<div class="flex items-center justify-between p-5 border-t border-dark-600 bg-dark-700/50">
|
||
<div class="text-sm text-gray-400">
|
||
<span v-if="mappingStep === 1">{{ selectedTables.length }} tables selected</span>
|
||
<span v-else>Editing: {{ selectedTable?.name }}</span>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<template v-if="mappingStep === 1">
|
||
<button @click="closeMapping" class="btn-secondary">Skip</button>
|
||
<button
|
||
@click="goToStep2"
|
||
:disabled="selectedTables.length === 0"
|
||
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-cyan to-blue-500 text-white hover:from-cyan-500 hover:to-blue-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||
>
|
||
Next <i class="fa-solid fa-arrow-right"></i>
|
||
</button>
|
||
</template>
|
||
<template v-else>
|
||
<button @click="goToStep1" class="btn-secondary">
|
||
<i class="fa-solid fa-arrow-left"></i> Back
|
||
</button>
|
||
<button @click="saveMapping" class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-cyan to-blue-500 text-white hover:from-cyan-500 hover:to-blue-600 transition-all flex items-center gap-2">
|
||
<i class="fa-solid fa-save"></i> Save Mapping
|
||
</button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
</div>
|
||
</template>
|