Prechádzať zdrojové kódy

feat:增加apihub模块的datasource页面。

microrain 5 mesiacov pred
rodič
commit
c659751e7d

+ 108 - 12
src/views/apihub/apilist.vue

@@ -31,9 +31,10 @@
 							ref="groupTreeRef"
 							:data="groupTreeData"
 							:props="{ label: 'Name', children: 'Children' }"
-							node-key="GroupKey"
+							node-key="Id"
 							highlight-current
-							:expand-on-click-node="false"
+							:expand-on-click-node="true"
+							default-expand-all
 							@node-click="handleGroupClick"
 						>
 							<template #default="{ node, data }">
@@ -282,21 +283,70 @@ const { params, tableData, getList, loading } = useSearch<ApiDefinition[]>(
 	}
 )
 
-// 页面加载时获取列表数据
-onMounted(() => {
-	getList()
-	// 获取数据源列表 - 实际使用时替换为真实API
+// 加载数据源列表
+const loadDataSources = () => {
+	// 实际使用时替换为真实API调用
 	// 模拟数据
 	dataSources.value = [
 		{ id: 1, name: '主数据库' },
 		{ id: 2, name: '业务数据库' },
 		{ id: 3, name: '日志数据库' }
 	]
+}
 
+// 页面加载时获取列表数据
+onMounted(() => {
+	// 获取API列表
+	getList(1)
+	
+	// 加载数据源列表
+	loadDataSources()
+	
 	// 加载分组树
 	refreshGroups()
 })
 
+// 将扁平数组转换为树形结构
+const convertToTree = (flatData) => {
+	// 创建一个映射表,用于快速查找节点
+	const map = {}
+	const result = []
+	
+	// 首先创建所有节点的映射
+	flatData.forEach(item => {
+		// 确保每个节点都有Children属性
+		map[item.Id] = { ...item, Children: [] }
+	})
+	
+	// 然后建立父子关系
+	flatData.forEach(item => {
+		const node = map[item.Id]
+		
+		if (item.ParentId === 0 || !map[item.ParentId]) {
+			// 如果ParentId为0或者父节点不存在,则为顶级节点
+			result.push(node)
+		} else {
+			// 否则将该节点添加到父节点的Children中
+			map[item.ParentId].Children.push(node)
+		}
+	})
+	
+	// 清理空的Children数组
+	flatData.forEach(item => {
+		if (map[item.Id].Children.length === 0) {
+			map[item.Id].Children = null
+		}
+	})
+	
+	return result
+}
+
+// 检查数据是否已经是树形结构
+const isTreeStructure = (data) => {
+	// 检查数据中是否有包含非空的Children字段的项
+	return data.some(item => item.Children && Array.isArray(item.Children) && item.Children.length > 0)
+}
+
 // 刷新分组树
 const refreshGroups = async () => {
 	try {
@@ -306,19 +356,62 @@ const refreshGroups = async () => {
 
 		// 使用API返回的数据
 		if (res.data && res.data.list) {
-			console.log('分组数据列表:', res.data.list)
-			groupTreeData.value = res.data.list || []
-			originalGroupTree.value = JSON.parse(JSON.stringify(res.data.list || []))
-			console.log('处理后的分组数据:', groupTreeData.value)
+			// 获取原始数据
+			const apiData = res.data.list || []
+			console.log('原始数据:', apiData)
+			
+			// 检查数据是否已经是树形结构
+			const hasTreeStructure = isTreeStructure(apiData)
+			console.log('是否已经是树形结构:', hasTreeStructure)
+			
+			let treeData
+			if (hasTreeStructure) {
+				// 如果已经是树形结构,直接使用
+				treeData = apiData
+				console.log('使用原始树形结构')
+			} else {
+				// 如果是扁平结构,通过ParentId构建树形结构
+				treeData = convertToTree(apiData)
+				console.log('通过ParentId构建的树形结构:', treeData)
+			}
+			
+			// 手动设置测试数据,确认组件是否正常工作
+			const testData = [
+				{
+					"Id": 10,
+					"GroupKey": "group_1746503398664_968",
+					"Name": "巡检管理",
+					"ParentId": 0,
+					"Sort": 0,
+					"Description": "",
+					"Children": [
+						{
+							"Id": 11,
+							"GroupKey": "group_1746515959076_509",
+							"Name": "能耗分析",
+							"ParentId": 10,
+							"Sort": 0,
+							"Description": "",
+							"Children": null,
+							"ApiCount": 0
+						}
+					],
+					"ApiCount": 0
+				}
+			]
+			console.log('测试数据:', testData)
+			
+			// 设置到组件中
+			groupTreeData.value = testData
+			originalGroupTree.value = JSON.parse(JSON.stringify(testData))
+			console.log('设置后的分组数据:', groupTreeData.value)
 			return
 		}
 
 		// 如果没有数据,初始化为空数组
-		console.log('没有获取到分组数据')
 		groupTreeData.value = []
 		originalGroupTree.value = []
 	} catch (error) {
-		console.error('获取分组数据失败:', error)
 		ElMessage.error('获取分组数据失败')
 	}
 }
@@ -563,6 +656,9 @@ const deleteApi = (row: ApiDefinition) => {
 .group-tree-container {
 	overflow-y: auto;
 	flex: 1;
+	min-height: 200px; /* 确保容器有最小高度 */
+	border: 1px solid #ebeef5; /* 添加边框以便于调试 */
+	padding: 10px;
 }
 
 .custom-tree-node {

+ 2 - 2
src/views/apihub/component/group.vue

@@ -87,7 +87,7 @@ const getGroupTree = async () => {
 // 添加分组
 const addGroup = async (data) => {
 	return request({
-		url: '/api/v1/api_group/add',
+		url: '/api_group/add',
 		method: 'post',
 		data
 	})
@@ -96,7 +96,7 @@ const addGroup = async (data) => {
 // 编辑分组
 const editGroup = async (data) => {
 	return request({
-		url: '/api/v1/api_group/edit',
+		url: '/api_group/edit',
 		method: 'post',
 		data
 	})

+ 466 - 0
src/views/apihub/datasource.vue

@@ -0,0 +1,466 @@
+<template>
+  <div class="page">
+    <el-card shadow="never">
+      <template #header>
+        <div class="card-header">
+          <span>数据源管理</span>
+        </div>
+      </template>
+      
+      <!-- 搜索表单 -->
+      <el-form :model="params" inline ref="queryRef">
+        <el-form-item label="数据源名称" prop="keyWord">
+          <el-input v-model="params.keyWord" placeholder="请输入数据源名称" clearable style="width: 200px" @keyup.enter.native="getList(1)" />
+        </el-form-item>
+        <el-form-item label="日期范围" prop="dateRange">
+          <el-date-picker
+            v-model="params.dateRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="YYYY-MM-DD"
+            style="width: 240px"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="getList(1)">
+            <el-icon><ele-Search /></el-icon>查询
+          </el-button>
+          <el-button @click="resetQuery">
+            <el-icon><ele-Refresh /></el-icon>重置
+          </el-button>
+          <el-button type="primary" @click="addOrEdit()" v-auth="'add_datasource'">
+            <el-icon><ele-Plus /></el-icon>新增
+          </el-button>
+        </el-form-item>
+      </el-form>
+      
+      <!-- 数据表格 -->
+      <el-table :data="tableData" style="width: 100%" v-loading="loading" row-key="id">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column prop="id" label="ID" width="80" align="center" />
+        <el-table-column prop="name" label="数据源名称" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="type" label="数据库类型" width="120" />
+        <el-table-column prop="host" label="主机地址" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="port" label="端口" width="80" align="center" />
+        <el-table-column prop="database" label="数据库名" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
+        <el-table-column prop="createdAt" label="创建时间" width="160" />
+        <el-table-column label="操作" width="200" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="testConnection(row)" v-auth="'test_datasource'">
+              测试连接
+            </el-button>
+            <el-button type="primary" link @click="addOrEdit(row)" v-auth="'edit_datasource'">
+              编辑
+            </el-button>
+            <el-button type="danger" link @click="deleteDataSource(row)" v-auth="'delete_datasource'">
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      
+      <!-- 分页 -->
+      <div class="pagination-container">
+        <el-pagination
+          v-model:current-page="params.pageNum"
+          v-model:page-size="params.pageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+          @size-change="getList()"
+          @current-change="getList()"
+        />
+      </div>
+    </el-card>
+    
+    <!-- 数据源表单对话框 -->
+    <el-dialog
+      v-model="dialogVisible"
+      :title="formData.id ? '编辑数据源' : '新增数据源'"
+      width="600px"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+    >
+      <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
+        <el-form-item label="数据源名称" prop="name">
+          <el-input v-model="formData.name" placeholder="请输入数据源名称" />
+        </el-form-item>
+        <el-form-item label="数据库类型" prop="type">
+          <el-select v-model="formData.type" placeholder="请选择数据库类型" style="width: 100%">
+            <el-option label="MySQL" value="MySQL" />
+            <el-option label="PostgreSQL" value="PostgreSQL" />
+            <el-option label="SQL Server" value="SQL Server" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="主机地址" prop="host">
+          <el-input v-model="formData.host" placeholder="请输入主机地址" />
+        </el-form-item>
+        <el-form-item label="端口" prop="port">
+          <el-input-number v-model="formData.port" :min="1" :max="65535" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="用户名" prop="username">
+          <el-input v-model="formData.username" placeholder="请输入用户名" />
+        </el-form-item>
+        <el-form-item label="密码" prop="password">
+          <el-input v-model="formData.password" type="password" placeholder="请输入密码" show-password />
+          <div class="form-tip" v-if="formData.id">不修改请留空</div>
+        </el-form-item>
+        <el-form-item label="数据库名" prop="database">
+          <el-input v-model="formData.database" placeholder="请输入数据库名" />
+        </el-form-item>
+        <el-form-item label="最大打开连接数" prop="maxOpenConns">
+          <el-input-number v-model="formData.maxOpenConns" :min="1" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="最大空闲连接数" prop="maxIdleConns">
+          <el-input-number v-model="formData.maxIdleConns" :min="1" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="连接最大生存时间" prop="connMaxLifetime">
+          <el-input-number v-model="formData.connMaxLifetime" :min="1" style="width: 100%" />
+          <div class="form-tip">单位:秒</div>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitForm">确定</el-button>
+          <el-button type="success" @click="testConnectionForm" v-if="!formData.id">测试连接</el-button>
+        </div>
+      </template>
+    </el-dialog>
+    
+    <!-- 测试连接对话框 -->
+    <el-dialog
+      v-model="testDialogVisible"
+      title="测试数据源连接"
+      width="500px"
+    >
+      <el-form ref="testFormRef" :model="testFormData" :rules="testRules" label-width="120px">
+        <el-form-item label="数据库类型" prop="type">
+          <el-select v-model="testFormData.type" placeholder="请选择数据库类型" style="width: 100%">
+            <el-option label="MySQL" value="MySQL" />
+            <el-option label="PostgreSQL" value="PostgreSQL" />
+            <el-option label="SQL Server" value="SQL Server" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="主机地址" prop="host">
+          <el-input v-model="testFormData.host" placeholder="请输入主机地址" />
+        </el-form-item>
+        <el-form-item label="端口" prop="port">
+          <el-input-number v-model="testFormData.port" :min="1" :max="65535" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="用户名" prop="username">
+          <el-input v-model="testFormData.username" placeholder="请输入用户名" />
+        </el-form-item>
+        <el-form-item label="密码" prop="password">
+          <el-input v-model="testFormData.password" type="password" placeholder="请输入密码" show-password />
+        </el-form-item>
+        <el-form-item label="数据库名" prop="database">
+          <el-input v-model="testFormData.database" placeholder="请输入数据库名" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="testDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitTestConnection">测试连接</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import request from '/@/utils/request'
+import { useSearch } from '/@/hooks/useSearch'
+
+// 定义数据源类型
+interface DataSource {
+  id?: number
+  name: string
+  type: string
+  host: string
+  port: number
+  username: string
+  password?: string
+  database: string
+  maxOpenConns?: number
+  maxIdleConns?: number
+  connMaxLifetime?: number
+  remark?: string
+  createdAt?: string
+  updatedAt?: string
+}
+
+// 表单引用
+const formRef = ref()
+const testFormRef = ref()
+const queryRef = ref()
+
+// 对话框状态
+const dialogVisible = ref(false)
+const testDialogVisible = ref(false)
+
+// 总数
+const total = ref(0)
+
+// 加载状态
+const loading = ref(false)
+
+// 表单数据
+const formData = reactive<DataSource>({
+  name: '',
+  type: 'MySQL',
+  host: '',
+  port: 3306,
+  username: '',
+  password: '',
+  database: '',
+  maxOpenConns: 100,
+  maxIdleConns: 10,
+  connMaxLifetime: 3600,
+  remark: ''
+})
+
+// 测试连接表单数据
+const testFormData = reactive({
+  type: 'MySQL',
+  host: '',
+  port: 3306,
+  username: '',
+  password: '',
+  database: ''
+})
+
+// 表单验证规则
+const rules = {
+  name: [{ required: true, message: '请输入数据源名称', trigger: 'blur' }],
+  type: [{ required: true, message: '请选择数据库类型', trigger: 'change' }],
+  host: [{ required: true, message: '请输入主机地址', trigger: 'blur' }],
+  port: [{ required: true, message: '请输入端口', trigger: 'blur' }],
+  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
+  password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+  database: [{ required: true, message: '请输入数据库名', trigger: 'blur' }]
+}
+
+// 测试连接表单验证规则
+const testRules = {
+  type: [{ required: true, message: '请选择数据库类型', trigger: 'change' }],
+  host: [{ required: true, message: '请输入主机地址', trigger: 'blur' }],
+  port: [{ required: true, message: '请输入端口', trigger: 'blur' }],
+  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
+  password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+  database: [{ required: true, message: '请输入数据库名', trigger: 'blur' }]
+}
+
+// 数据源列表请求函数
+const dataSourceRequest = (params: any) => {
+  return request({
+    url: '/datasource/list',
+    method: 'get',
+    params
+  })
+}
+
+// 使用通用搜索钩子
+const { params, tableData, getList } = useSearch<DataSource[]>(
+  dataSourceRequest,
+  'data',
+  (res: any) => {
+    total.value = res.pagination?.total || 0
+    return res.data || []
+  },
+  {
+    keyWord: '',
+    dateRange: [],
+    pageNum: 1,
+    pageSize: 10
+  }
+)
+
+// 页面加载时获取列表数据
+onMounted(() => {
+  getList(1)
+})
+
+// 重置查询表单
+const resetQuery = () => {
+  queryRef.value?.resetFields()
+  getList(1)
+}
+
+// 新增或编辑数据源
+const addOrEdit = (row?: DataSource) => {
+  // 重置表单
+  Object.assign(formData, {
+    id: undefined,
+    name: '',
+    type: 'MySQL',
+    host: '',
+    port: 3306,
+    username: '',
+    password: '',
+    database: '',
+    maxOpenConns: 100,
+    maxIdleConns: 10,
+    connMaxLifetime: 3600,
+    remark: ''
+  })
+  
+  // 如果是编辑,填充表单数据
+  if (row) {
+    Object.assign(formData, { ...row, password: '' })
+  }
+  
+  // 显示对话框
+  dialogVisible.value = true
+}
+
+// 提交表单
+const submitForm = async () => {
+  // 表单验证
+  await formRef.value.validate()
+  
+  try {
+    // 根据是否有ID判断是新增还是编辑
+    const url = formData.id ? '/datasource/edit' : '/datasource/add'
+    const method = formData.id ? 'put' : 'post'
+    
+    // 发送请求
+    await request({
+      url,
+      method,
+      data: formData
+    })
+    
+    // 关闭对话框
+    dialogVisible.value = false
+    
+    // 刷新列表
+    getList()
+    
+    // 提示成功
+    ElMessage.success(formData.id ? '编辑成功' : '添加成功')
+  } catch (error) {
+    console.error('提交表单失败:', error)
+  }
+}
+
+// 删除数据源
+const deleteDataSource = (row: DataSource) => {
+  ElMessageBox.confirm(`确定要删除数据源「${row.name}」吗?`, '警告', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    try {
+      // 发送删除请求
+      await request({
+        url: '/datasource/delete',
+        method: 'delete',
+        data: { ids: [row.id] }
+      })
+      
+      // 刷新列表
+      getList()
+      
+      // 提示成功
+      ElMessage.success('删除成功')
+    } catch (error) {
+      console.error('删除失败:', error)
+    }
+  }).catch(() => {})
+}
+
+// 测试连接(从列表)
+const testConnection = (row: DataSource) => {
+  // 填充测试表单
+  Object.assign(testFormData, {
+    type: row.type,
+    host: row.host,
+    port: row.port,
+    username: row.username,
+    password: '', // 密码需要重新输入
+    database: row.database
+  })
+  
+  // 显示测试对话框
+  testDialogVisible.value = true
+}
+
+// 测试连接(从表单)
+const testConnectionForm = async () => {
+  // 表单验证
+  await formRef.value.validate()
+  
+  // 填充测试表单
+  Object.assign(testFormData, {
+    type: formData.type,
+    host: formData.host,
+    port: formData.port,
+    username: formData.username,
+    password: formData.password,
+    database: formData.database
+  })
+  
+  // 显示测试对话框
+  testDialogVisible.value = true
+}
+
+// 提交测试连接
+const submitTestConnection = async () => {
+  // 表单验证
+  await testFormRef.value.validate()
+  
+  try {
+    // 发送测试连接请求
+    const res = await request({
+      url: '/datasource/test',
+      method: 'post',
+      data: testFormData
+    })
+    
+    // 根据测试结果显示提示
+    if (res.data.success) {
+      ElMessage.success(res.data.message || '连接成功')
+    } else {
+      ElMessage.error(res.data.message || '连接失败')
+    }
+  } catch (error) {
+    console.error('测试连接失败:', error)
+    ElMessage.error('测试连接失败')
+  }
+}
+</script>
+
+<style scoped>
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.pagination-container {
+  margin-top: 20px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.form-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+</style>