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

625 lines
25 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 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-if="filteredDatabases().length === 0" class="empty-state">
<i class="fa-solid fa-database empty-state-icon"></i>
<p>No databases found</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="p-4 border-b border-dark-600 bg-dark-900/50">
<div class="flex items-center justify-between mb-2">
<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="bg-dark-950 rounded-lg p-4 max-h-32 overflow-auto">
<pre class="text-xs text-primary-cyan font-mono whitespace-pre-wrap">{{ selectedTable.ddl }}</pre>
</div>
</div>
<!-- 字段映射表格 - 卡片式布局 -->
<div class="flex-1 overflow-auto p-4">
<div v-if="selectedTable" class="space-y-2">
<!-- 表头 -->
<div class="grid grid-cols-12 gap-4 px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="col-span-3">Field Name</div>
<div class="col-span-2">Type</div>
<div class="col-span-7">Comment / Mapping</div>
</div>
<!-- 字段行 -->
<div
v-for="col in selectedTable.columns"
:key="col.name"
class="grid grid-cols-12 gap-4 items-center px-4 py-3 bg-dark-800 rounded-lg border border-dark-600"
>
<!-- 字段名 -->
<div class="col-span-3 flex items-center gap-2">
<div class="w-6 h-6 rounded bg-primary-yellow/20 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-key text-xs text-primary-yellow"></i>
</div>
<span class="font-medium text-primary-yellow truncate" :title="col.name">{{ col.name }}</span>
</div>
<!-- 类型 -->
<div class="col-span-2">
<span class="inline-flex items-center px-2 py-1 rounded-md bg-dark-600 text-gray-400 text-xs font-mono">
{{ col.type }}
</span>
</div>
<!-- 注释输入 -->
<div class="col-span-7">
<input
v-model="col.mapped_name"
type="text"
:placeholder="col.comment || 'Enter comment...'"
class="w-full bg-dark-700 border border-dark-500 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary-cyan focus:ring-1 focus:ring-primary-cyan/30 transition-all"
>
</div>
</div>
</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>