Files
X-Agents/web/src/views/Database.vue
DESKTOP-72TV0V4\caoxiaozhu 945cf6c950 fix: 修复空状态显示问题,添加DDL编辑功能
- 修复无数据时重复显示空状态的问题
- 将DDL展示改为可编辑的textarea
- 优化空状态UI样式和提示文案

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:33:45 +08:00

591 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>
<!-- 步骤2DDL编辑 -->
<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>