Compare commits

...

3 Commits

Author SHA1 Message Date
b6791cb18d chore: 添加数据库资源和需求文档
- 新增数据库连接配置 (db_info)
- 添加 DDL 编辑需求文档
- 添加 TODO 待办事项

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:34:16 +08:00
945cf6c950 fix: 修复空状态显示问题,添加DDL编辑功能
- 修复无数据时重复显示空状态的问题
- 将DDL展示改为可编辑的textarea
- 优化空状态UI样式和提示文案

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:33:45 +08:00
2eddc53249 feat: 更新数据库时支持同步保存子表配置和DDL
- UpdateDatabaseRequest 添加 SubTables 字段
- 数据库更新时同步创建或更新子表记录
- 支持子表 DDL 的保存

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:33:30 +08:00
11 changed files with 261 additions and 75 deletions

View File

@@ -54,21 +54,22 @@ type CreateDatabaseRequest struct {
// UpdateRequest 更新数据库信息请求 // UpdateRequest 更新数据库信息请求
type UpdateDatabaseRequest struct { type UpdateDatabaseRequest struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
DBType string `json:"db_type"` DBType string `json:"db_type"`
Host string `json:"host"` Host string `json:"host"`
Port int `json:"port"` Port int `json:"port"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
Database string `json:"database"` Database string `json:"database"`
TableCount int `json:"table_count"` TableCount int `json:"table_count"`
Charset string `json:"charset"` Charset string `json:"charset"`
SSLMode string `json:"ssl_mode"` SSLMode string `json:"ssl_mode"`
URI string `json:"uri"` // Neo4j 连接地址 URI string `json:"uri"` // Neo4j 连接地址
GraphLabels string `json:"graph_labels"` // Neo4j 标签列表 (JSON) GraphLabels string `json:"graph_labels"` // Neo4j 标签列表 (JSON)
GraphRelationship string `json:"graph_relationship"` // Neo4j 关系类型列表 (JSON) GraphRelationship string `json:"graph_relationship"` // Neo4j 关系类型列表 (JSON)
SelectedLabel string `json:"selected_label"` // 当前选中的标签 SelectedLabel string `json:"selected_label"` // 当前选中的标签
SubTables []CreateSubTableRequest `json:"sub_tables"` // 子表配置
} }
// SaveGraphRequest 保存图谱信息请求 // SaveGraphRequest 保存图谱信息请求

View File

@@ -95,6 +95,7 @@ type CreateSubTableRequest struct {
RelationField string `json:"relation_field"` RelationField string `json:"relation_field"`
RelationType string `json:"relation_type"` RelationType string `json:"relation_type"`
Fields []FieldMapping `json:"fields"` // 字段映射列表 Fields []FieldMapping `json:"fields"` // 字段映射列表
DDL string `json:"ddl"` // 建表DDL
} }
// UpdateSubTableRequest 更新子表请求 // UpdateSubTableRequest 更新子表请求
@@ -105,6 +106,7 @@ type UpdateSubTableRequest struct {
MappingType string `json:"mapping_type"` MappingType string `json:"mapping_type"`
RelationField string `json:"relation_field"` RelationField string `json:"relation_field"`
RelationType string `json:"relation_type"` RelationType string `json:"relation_type"`
DDL string `json:"ddl"`
} }
// SubTableMapping 完整的子表映射配置(存储到文件的格式) // SubTableMapping 完整的子表映射配置(存储到文件的格式)

View File

@@ -696,12 +696,66 @@ func (s *DatabaseService) Update(id string, req model.UpdateDatabaseRequest) (*m
updates["ssl_mode"] = req.SSLMode updates["ssl_mode"] = req.SSLMode
} }
// 更新数据库基本信息
info := &model.DatabaseInfo{} info := &model.DatabaseInfo{}
if err := s.repo.Update(id, info); err != nil { if err := s.repo.Update(id, info); err != nil {
log.Printf("[Update] 更新失败: %v", err) log.Printf("[Update] 更新失败: %v", err)
return nil, err return nil, err
} }
// 处理 SubTables - 创建或更新子表记录(包括 DDL
if len(req.SubTables) > 0 {
log.Printf("[Update] 处理 %d 个子表配置", len(req.SubTables))
for _, subTableReq := range req.SubTables {
subTableReq.DatabaseID = id
// 检查是否已存在(根据 parent_table 查找)
existingTables, err := s.subTableRepo.FindByDatabaseID(id)
if err != nil {
log.Printf("[Update] 查询子表失败: %v", err)
continue
}
found := false
for _, existing := range existingTables {
if existing.ParentTable == subTableReq.ParentTable {
// 存在则更新
log.Printf("[Update] 更新子表: %s", existing.ID)
err := s.subTableRepo.Update(existing.ID, &model.SubTableInfo{
ParentTable: subTableReq.ParentTable,
SubTableName: subTableReq.SubTableName,
SubTableComment: subTableReq.SubTableComment,
MappingType: subTableReq.MappingType,
RelationField: subTableReq.RelationField,
RelationType: subTableReq.RelationType,
DDL: subTableReq.DDL,
})
if err != nil {
log.Printf("[Update] 更新子表失败: %v", err)
}
found = true
break
}
}
if !found {
// 不存在则创建
log.Printf("[Update] 创建子表: %s", subTableReq.ParentTable)
err := s.subTableRepo.Create(&model.SubTableInfo{
ID: uuid.New().String(),
DatabaseID: id,
ParentTable: subTableReq.ParentTable,
SubTableName: subTableReq.SubTableName,
SubTableComment: subTableReq.SubTableComment,
MappingType: subTableReq.MappingType,
RelationField: subTableReq.RelationField,
RelationType: subTableReq.RelationType,
DDL: subTableReq.DDL,
})
if err != nil {
log.Printf("[Update] 创建子表失败: %v", err)
}
}
}
}
return s.repo.FindByID(id) return s.repo.FindByID(id)
} }

View File

@@ -119,6 +119,7 @@ func (s *SubTableService) Create(req model.CreateSubTableRequest) (*model.SubTab
MappingType: req.MappingType, MappingType: req.MappingType,
RelationField: req.RelationField, RelationField: req.RelationField,
RelationType: req.RelationType, RelationType: req.RelationType,
DDL: req.DDL,
} }
if err := s.repo.Create(info); err != nil { if err := s.repo.Create(info); err != nil {
@@ -196,6 +197,8 @@ func (s *SubTableService) Update(id string, req model.UpdateSubTableRequest) (*m
if req.RelationType != "" { if req.RelationType != "" {
info.RelationType = req.RelationType info.RelationType = req.RelationType
} }
// 更新 DDL
info.DDL = req.DDL
if err := s.repo.Update(id, info); err != nil { if err := s.repo.Update(id, info); err != nil {
log.Printf("[SubTable Update] 更新失败: %v", err) log.Printf("[SubTable Update] 更新失败: %v", err)

View File

@@ -0,0 +1,22 @@
{
"database_id": "44cf2f22-fccb-4dd0-92fb-02d613ce2624",
"database_name": "1231",
"db_type": "mysql",
"tables": [
{
"id": "042fb35c-9023-40ed-8d3a-cec24f0709a2",
"database_id": "44cf2f22-fccb-4dd0-92fb-02d613ce2624",
"parent_table": "scores",
"sub_table_name": "scores",
"sub_table_comment": "",
"mapping_type": "",
"relation_field": "",
"relation_type": "",
"fields": null,
"ddl": "CREATE TABLE `scores` (\n `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n `student_id` int(10) unsigned NOT NULL,\n `subject` varchar(50) NOT NULL COMMENT '科目',\n `score` double DEFAULT NULL COMMENT '分数',\n `teacher_id` int(10) unsigned DEFAULT NULL,\n `exam_date` date DEFAULT NULL COMMENT '考试日期',\n `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4",
"created_at": "2026-03-07T09:59:48.436+08:00",
"updated_at": "2026-03-07T09:59:48.436+08:00"
}
],
"updated_at": "2026-03-07T09:59:48.4882963+08:00"
}

View File

@@ -0,0 +1,66 @@
# 后端需求 - DDL 编辑功能
## 问题描述
前端 Database 页面的 Table Mapping 功能已从"字段映射"改为"DDL 编辑"模式。后端需要支持保存和读取 DDL 数据。
## 需求
### 1. 保存 DDL
前端保存时发送 `ddl` 字段而非 `fields` 字段。
请求结构:
```json
{
"name": "数据库名",
"sub_tables": [
{
"parent_table": "users",
"sub_table_name": "用户表",
"sub_table_comment": "用户表",
"ddl": "CREATE TABLE users (...)"
}
]
}
```
### 2. 后端处理
- `CreateSubTableRequest` 已有 `DDL string` 字段(已添加)
- `UpdateSubTableRequest` 已有 `DDL string` 字段(已添加)
- `UpdateDatabaseRequest` 已有 `SubTables []CreateSubTableRequest` 字段(已添加)
### 3. Service 层修改
**sub_table_service.go**
1. `Create` 函数 - 添加 DDL 字段赋值:
```go
info := &model.SubTableInfo{
// ... 现有字段
DDL: req.DDL,
}
```
2. `Update` 函数 - 添加 DDL 更新逻辑:
```go
// 更新 DDL
info.DDL = req.DDL
```
**database_service.go 或 handler**
`Update` 方法中处理 `SubTables` 字段:
- 当前端传入 `sub_tables` 时,需要创建或更新对应的子表记录(包括 DDL
- 遍历 `sub_tables`,调用 `SubTableService.Create``SubTableService.Update`
## 影响范围
- `server/internal/service/sub_table_service.go` - Create/Update 方法
- `server/internal/service/database_service.go` 或 handler - Update 方法处理 SubTables
## 状态
- [x] 前端修改完成
- [x] 后端修改已完成

View File

@@ -0,0 +1,13 @@
# Web 前端需求 TODO
## 2026年3月
### 2026-03-07
- [x] **DDL 编辑功能** - 后端已完成 ✔
- 前端只发送 ddl 字段,不再发送 fields 字段
- 详细需求:[ddl-edit.md](./ddl-edit.md)
---
> 需求完成后请完成者打 ✔

View File

@@ -286,8 +286,14 @@ html.dark .el-select-dropdown__item.is-selected {
/* el-checkbox */ /* el-checkbox */
html.dark .el-checkbox .el-checkbox__input.is-checked .el-checkbox__inner { html.dark .el-checkbox .el-checkbox__input.is-checked .el-checkbox__inner {
background-color: #ffb700; background-color: transparent;
border-color: #ffb700; border-color: #ff9500;
}
html.dark .el-checkbox .el-checkbox__input.is-checked .el-checkbox__inner::after {
border-color: #ff9500 !important;
width: 4px;
height: 8px;
} }
html.dark .el-checkbox .el-checkbox__label { html.dark .el-checkbox .el-checkbox__label {

View File

@@ -85,7 +85,7 @@ const {
<i class="fa-solid fa-circle-notch fa-spin text-2xl mb-2"></i> <i class="fa-solid fa-circle-notch fa-spin text-2xl mb-2"></i>
<p>Loading...</p> <p>Loading...</p>
</div> </div>
<table v-else class="w-full"> <table v-else-if="filteredDatabases().length > 0" class="w-full">
<thead class="bg-dark-600"> <thead class="bg-dark-600">
<tr> <tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Database Name</th> <th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Database Name</th>
@@ -142,9 +142,12 @@ const {
</table> </table>
<!-- 空状态 --> <!-- 空状态 -->
<div v-if="filteredDatabases().length === 0" class="empty-state"> <div v-else class="empty-box">
<i class="fa-solid fa-database empty-state-icon"></i> <div class="empty-icon">
<p>No databases found</p> <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>
</div> </div>
@@ -516,11 +519,11 @@ const {
</div> </div>
</template> </template>
<!-- 步骤2DDL和字段映射 --> <!-- 步骤2DDL编辑 -->
<template v-if="mappingStep === 2"> <template v-if="mappingStep === 2">
<!-- DDL 展示 --> <!-- DDL 编辑区域 -->
<div class="p-4 border-b border-dark-600 bg-dark-900/50"> <div class="flex-1 p-4 overflow-hidden">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-3">
<span class="text-sm font-medium text-gray-300">DDL - {{ selectedTable?.name }}</span> <span class="text-sm font-medium text-gray-300">DDL - {{ selectedTable?.name }}</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
@@ -542,49 +545,12 @@ const {
</button> </button>
</div> </div>
</div> </div>
<div v-if="selectedTable" class="bg-dark-950 rounded-lg p-4 max-h-32 overflow-auto"> <div v-if="selectedTable" class="h-[calc(100%-40px)]">
<pre class="text-xs text-primary-cyan font-mono whitespace-pre-wrap">{{ selectedTable.ddl }}</pre> <textarea
</div> v-model="selectedTable.ddl"
</div> 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 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>
</div> </div>
</template> </template>

View File

@@ -30,13 +30,69 @@
@apply border-b border-dark-600 hover:bg-dark-600/50 transition-colors; @apply border-b border-dark-600 hover:bg-dark-600/50 transition-colors;
} }
/* 空状态 */ /* 空状态(已不使用) */
.empty-state { .empty-state {
@apply py-16 text-center text-gray-500; @apply py-16 text-center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* 自定义空状态盒子 */
.empty-box {
min-height: 340px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.empty-icon {
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1f2937, #111827);
border-radius: 24px;
margin-bottom: 20px;
}
.empty-icon i {
font-size: 40px;
color: #6b7280;
}
.empty-text {
color: #d1d5db;
font-size: 1.25rem;
font-weight: 500;
margin-bottom: 8px;
}
.empty-tip {
color: #6b7280;
font-size: 0.875rem;
} }
.empty-state-icon { .empty-state-icon {
@apply text-4xl mb-4 block; font-size: 3rem;
margin-bottom: 1rem;
display: block;
color: #4b5563;
}
.empty-state-title {
color: #d1d5db;
font-size: 1.25rem;
font-weight: 500;
}
.empty-state-hint {
color: #6b7280;
font-size: 0.875rem;
margin-top: 0.5rem;
} }
/* 表格复选框 */ /* 表格复选框 */

View File

@@ -482,10 +482,7 @@ export function useDatabase() {
parent_table: table.name, parent_table: table.name,
sub_table_name: table.table_comment || table.name, sub_table_name: table.table_comment || table.name,
sub_table_comment: table.table_comment || '', sub_table_comment: table.table_comment || '',
fields: (table.columns || []).map(col => ({ ddl: table.ddl || '',
column_name: col.name,
mapped_name: col.mapped_name || '',
})),
})) }))
try { try {