Files
X-Agents/web/src/views/Database.vue

591 lines
23 KiB
Vue
Raw Normal View History

<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>