apilist.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. <template>
  2. <div class="page">
  3. <div class="apihub-container">
  4. <!-- 左侧分组树 -->
  5. <div class="apihub-sidebar">
  6. <el-card shadow="never" class="group-card">
  7. <template #header>
  8. <div class="card-header">
  9. <span>API分组</span>
  10. <div class="header-actions">
  11. <el-button type="primary" size="small" @click="addGroup" v-auth="'add_group'">
  12. <el-icon><ele-Plus /></el-icon>
  13. </el-button>
  14. <el-button type="primary" size="small" @click="refreshGroups">
  15. <el-icon><ele-Refresh /></el-icon>
  16. </el-button>
  17. </div>
  18. </div>
  19. </template>
  20. <div class="group-search">
  21. <el-input
  22. v-model="groupSearchKey"
  23. placeholder="搜索分组"
  24. clearable
  25. prefix-icon="ele-Search"
  26. @input="filterGroups"
  27. />
  28. </div>
  29. <div class="group-tree-container">
  30. <el-tree
  31. ref="groupTreeRef"
  32. :data="groupTreeData"
  33. :props="{ label: 'Name', children: 'Children' }"
  34. node-key="Id"
  35. highlight-current
  36. :expand-on-click-node="true"
  37. default-expand-all
  38. @node-click="handleGroupClick"
  39. >
  40. <template #default="{ node, data }">
  41. <div class="custom-tree-node">
  42. <span>{{ node.label }}</span>
  43. <span class="api-count" v-if="data.ApiCount">{{ data.ApiCount }}</span>
  44. <div class="node-actions">
  45. <el-dropdown @command="(command) => handleGroupCommand(command, data)" trigger="click">
  46. <el-icon><ele-More /></el-icon>
  47. <template #dropdown>
  48. <el-dropdown-menu>
  49. <el-dropdown-item command="edit" v-auth="'edit_group'">编辑</el-dropdown-item>
  50. <el-dropdown-item command="add_child" v-auth="'add_group'">添加子分组</el-dropdown-item>
  51. <el-dropdown-item command="delete" v-auth="'delete_group'">删除</el-dropdown-item>
  52. </el-dropdown-menu>
  53. </template>
  54. </el-dropdown>
  55. </div>
  56. </div>
  57. </template>
  58. </el-tree>
  59. </div>
  60. </el-card>
  61. </div>
  62. <!-- 右侧API列表 -->
  63. <div class="apihub-content">
  64. <el-card shadow="never">
  65. <div class="api-header">
  66. <div class="current-group" v-if="currentGroup.name">
  67. 当前分组: <span class="group-name">{{ currentGroup.name }}</span>
  68. </div>
  69. <div class="current-group" v-else>
  70. 全部API
  71. </div>
  72. </div>
  73. <el-form :model="params" inline ref="queryRef">
  74. <el-form-item label="API名称" prop="keyWord">
  75. <el-input v-model="params.keyWord" placeholder="请输入API名称" clearable style="width: 180px" @keyup.enter.native="getList(1)" />
  76. </el-form-item>
  77. <el-form-item label="数据源" prop="dataSourceId">
  78. <el-select v-model="params.dataSourceId" placeholder="请选择数据源" clearable style="width: 180px">
  79. <el-option v-for="item in dataSources" :key="item.id" :label="item.name" :value="item.id" />
  80. </el-select>
  81. </el-form-item>
  82. <el-form-item label="状态" prop="status">
  83. <el-select v-model="params.status" placeholder="请选择状态" clearable style="width: 120px">
  84. <el-option label="全部" value="" />
  85. <el-option label="草稿" value="Draft" />
  86. <el-option label="已发布" value="Published" />
  87. <el-option label="已废弃" value="Deprecated" />
  88. </el-select>
  89. </el-form-item>
  90. <el-form-item label="日期范围" prop="dateRange">
  91. <el-date-picker
  92. v-model="params.dateRange"
  93. type="daterange"
  94. range-separator="至"
  95. start-placeholder="开始日期"
  96. end-placeholder="结束日期"
  97. value-format="YYYY-MM-DD"
  98. style="width: 240px"
  99. />
  100. </el-form-item>
  101. <el-form-item>
  102. <el-button type="primary" class="ml10" @click="getList(1)">
  103. <el-icon>
  104. <ele-Search />
  105. </el-icon>
  106. 查询
  107. </el-button>
  108. <el-button @click="resetQuery()">
  109. <el-icon>
  110. <ele-Refresh />
  111. </el-icon>
  112. 重置
  113. </el-button>
  114. <el-button type="primary" @click="addOrEdit()" v-auth="'add'">
  115. <el-icon>
  116. <ele-FolderAdd />
  117. </el-icon>
  118. 新增API
  119. </el-button>
  120. </el-form-item>
  121. </el-form>
  122. <el-table :data="tableData" style="width: 100%" v-loading="loading" row-key="id">
  123. <el-table-column type="selection" width="55" align="center" />
  124. <el-table-column prop="id" label="ID" width="80" align="center" />
  125. <el-table-column prop="name" label="API名称" min-width="120" show-overflow-tooltip></el-table-column>
  126. <el-table-column prop="path" label="API路径" min-width="150" show-overflow-tooltip></el-table-column>
  127. <el-table-column prop="method" label="请求方法" width="100" align="center">
  128. <template #default="scope">
  129. <el-tag
  130. :type="getMethodTagType(scope.row.method)"
  131. size="small"
  132. >
  133. {{ scope.row.method }}
  134. </el-tag>
  135. </template>
  136. </el-table-column>
  137. <el-table-column prop="dataSourceName" label="数据源" width="120" show-overflow-tooltip></el-table-column>
  138. <el-table-column prop="sqlType" label="SQL类型" width="100" align="center">
  139. <template #default="scope">
  140. <el-tag size="small" type="info" v-if="scope.row.sqlType === 'query'">查询</el-tag>
  141. <el-tag size="small" type="warning" v-else-if="scope.row.sqlType === 'procedure'">存储过程</el-tag>
  142. <span v-else>{{ scope.row.sqlType }}</span>
  143. </template>
  144. </el-table-column>
  145. <el-table-column prop="version" label="版本" width="80" align="center"></el-table-column>
  146. <el-table-column prop="status" label="状态" width="100" align="center">
  147. <template #default="scope">
  148. <el-tag size="small" v-if="scope.row.status === 'Draft'">草稿</el-tag>
  149. <el-tag size="small" type="success" v-else-if="scope.row.status === 'Published'">已发布</el-tag>
  150. <el-tag size="small" type="info" v-else-if="scope.row.status === 'Deprecated'">已废弃</el-tag>
  151. <span v-else>{{ scope.row.status }}</span>
  152. </template>
  153. </el-table-column>
  154. <el-table-column prop="createdAt" label="创建时间" width="160" align="center"></el-table-column>
  155. <el-table-column label="操作" width="220" align="center">
  156. <template #default="scope">
  157. <el-button size="small" text type="primary" @click="viewDetail(scope.row)" v-auth="'view'">查看</el-button>
  158. <el-button size="small" text type="warning" @click="addOrEdit(scope.row)" v-auth="'edit'">编辑</el-button>
  159. <el-button size="small" text type="success" @click="testApi(scope.row)" v-auth="'test'">测试</el-button>
  160. <el-dropdown @command="(command) => handleCommand(command, scope.row)">
  161. <el-button size="small" text type="primary">
  162. 更多<el-icon class="el-icon--right"><arrow-down /></el-icon>
  163. </el-button>
  164. <template #dropdown>
  165. <el-dropdown-menu>
  166. <el-dropdown-item command="publish" v-if="scope.row.status === 'Draft'" v-auth="'publish'">发布</el-dropdown-item>
  167. <el-dropdown-item command="deprecate" v-if="scope.row.status === 'Published'" v-auth="'deprecate'">废弃</el-dropdown-item>
  168. <el-dropdown-item command="delete" v-auth="'delete'">删除</el-dropdown-item>
  169. </el-dropdown-menu>
  170. </template>
  171. </el-dropdown>
  172. </template>
  173. </el-table-column>
  174. </el-table>
  175. <pagination v-if="params.total" :total="params.total" v-model:page="params.pageNum" v-model:limit="params.pageSize" @pagination="getList()" />
  176. </el-card>
  177. </div>
  178. </div>
  179. <!-- 组件 -->
  180. <EditForm ref="editFormRef" @getList="getList(1)"></EditForm>
  181. <ViewDetail ref="viewDetailRef"></ViewDetail>
  182. <TestApi ref="testApiRef"></TestApi>
  183. <GroupForm ref="groupFormRef" @refresh="refreshGroups"></GroupForm>
  184. </div>
  185. </template>
  186. <script lang="ts" setup>
  187. import { ref, onMounted, reactive } from 'vue'
  188. import EditForm from './component/edit.vue'
  189. import ViewDetail from './component/view.vue'
  190. import TestApi from './component/test.vue'
  191. import GroupForm from './component/group.vue'
  192. import { useSearch } from '/@/hooks/useCommon'
  193. import { ElMessageBox, ElMessage } from 'element-plus'
  194. import { ArrowDown } from '@element-plus/icons-vue'
  195. import request from '/@/utils/request'
  196. // 定义API接口类型
  197. interface ApiDefinition {
  198. id?: number
  199. name: string
  200. path: string
  201. method: string
  202. dataSourceId: number
  203. dataSourceName?: string
  204. sqlType: string
  205. sqlContent: string
  206. parameters?: any[]
  207. returnFormat: string
  208. version: string
  209. status: string
  210. plugins?: any[]
  211. description?: string
  212. createdAt?: string
  213. updatedAt?: string
  214. }
  215. // 引用组件
  216. const editFormRef = ref()
  217. const viewDetailRef = ref()
  218. const testApiRef = ref()
  219. const queryRef = ref()
  220. const groupFormRef = ref()
  221. const groupTreeRef = ref()
  222. // 分组相关状态
  223. const groupSearchKey = ref('')
  224. const groupTreeData = ref([])
  225. const currentGroup = reactive({
  226. Id: undefined,
  227. GroupKey: '',
  228. Name: '',
  229. ParentId: 0,
  230. Description: ''
  231. })
  232. const originalGroupTree = ref([])
  233. // 数据源列表
  234. const dataSources = ref([])
  235. // API列表请求函数
  236. const apiRequest = (params: any) => {
  237. // 这里使用request函数发起请求
  238. // 实际使用时替换为真实API路径
  239. return request({
  240. url: '/api/list',
  241. method: 'get',
  242. params
  243. })
  244. }
  245. // 获取分组树形结构
  246. const getGroupTree = () => {
  247. return request({
  248. url: '/api_group/tree',
  249. method: 'get'
  250. })
  251. }
  252. // 删除分组
  253. const deleteGroup = (ids: number[]) => {
  254. return request({
  255. url: '/api_group/delete',
  256. method: 'post',
  257. data: { ids }
  258. })
  259. }
  260. // 使用通用搜索钩子
  261. const { params, tableData, getList, loading } = useSearch<ApiDefinition[]>(
  262. apiRequest,
  263. 'data',
  264. {
  265. keyWord: '',
  266. dataSourceId: '',
  267. status: '',
  268. dateRange: [],
  269. orderBy: '',
  270. pageNum: 1,
  271. pageSize: 10
  272. }
  273. )
  274. // 加载数据源列表
  275. const loadDataSources = () => {
  276. // 实际使用时替换为真实API调用
  277. // 模拟数据
  278. dataSources.value = [
  279. { id: 1, name: '主数据库' },
  280. { id: 2, name: '业务数据库' },
  281. { id: 3, name: '日志数据库' }
  282. ]
  283. }
  284. // 页面加载时获取列表数据
  285. onMounted(() => {
  286. // 获取API列表
  287. getList(1)
  288. // 加载数据源列表
  289. loadDataSources()
  290. // 加载分组树
  291. refreshGroups()
  292. })
  293. // 将扁平数组转换为树形结构
  294. const convertToTree = (flatData) => {
  295. // 创建一个映射表,用于快速查找节点
  296. const map = {}
  297. const result = []
  298. // 首先创建所有节点的映射
  299. flatData.forEach(item => {
  300. // 确保每个节点都有Children属性
  301. map[item.Id] = { ...item, Children: [] }
  302. })
  303. // 然后建立父子关系
  304. flatData.forEach(item => {
  305. const node = map[item.Id]
  306. if (item.ParentId === 0 || !map[item.ParentId]) {
  307. // 如果ParentId为0或者父节点不存在,则为顶级节点
  308. result.push(node)
  309. } else {
  310. // 否则将该节点添加到父节点的Children中
  311. map[item.ParentId].Children.push(node)
  312. }
  313. })
  314. // 清理空的Children数组
  315. flatData.forEach(item => {
  316. if (map[item.Id].Children.length === 0) {
  317. map[item.Id].Children = null
  318. }
  319. })
  320. return result
  321. }
  322. // 检查数据是否已经是树形结构
  323. const isTreeStructure = (data) => {
  324. // 检查数据中是否有包含非空的Children字段的项
  325. return data.some(item => item.Children && Array.isArray(item.Children) && item.Children.length > 0)
  326. }
  327. // 刷新分组树
  328. const refreshGroups = async () => {
  329. try {
  330. // 调用API获取分组树
  331. const res = await getGroupTree()
  332. console.log('获取到的API分组数据:', res)
  333. // 使用API返回的数据
  334. if (res.data && res.data.list) {
  335. // 获取原始数据
  336. const apiData = res.data.list || []
  337. console.log('原始数据:', apiData)
  338. // 检查数据是否已经是树形结构
  339. const hasTreeStructure = isTreeStructure(apiData)
  340. console.log('是否已经是树形结构:', hasTreeStructure)
  341. let treeData
  342. if (hasTreeStructure) {
  343. // 如果已经是树形结构,直接使用
  344. treeData = apiData
  345. console.log('使用原始树形结构')
  346. } else {
  347. // 如果是扁平结构,通过ParentId构建树形结构
  348. treeData = convertToTree(apiData)
  349. console.log('通过ParentId构建的树形结构:', treeData)
  350. }
  351. // 手动设置测试数据,确认组件是否正常工作
  352. const testData = [
  353. {
  354. "Id": 10,
  355. "GroupKey": "group_1746503398664_968",
  356. "Name": "巡检管理",
  357. "ParentId": 0,
  358. "Sort": 0,
  359. "Description": "",
  360. "Children": [
  361. {
  362. "Id": 11,
  363. "GroupKey": "group_1746515959076_509",
  364. "Name": "能耗分析",
  365. "ParentId": 10,
  366. "Sort": 0,
  367. "Description": "",
  368. "Children": null,
  369. "ApiCount": 0
  370. }
  371. ],
  372. "ApiCount": 0
  373. }
  374. ]
  375. console.log('测试数据:', testData)
  376. // 设置到组件中
  377. groupTreeData.value = testData
  378. originalGroupTree.value = JSON.parse(JSON.stringify(testData))
  379. console.log('设置后的分组数据:', groupTreeData.value)
  380. return
  381. }
  382. // 如果没有数据,初始化为空数组
  383. groupTreeData.value = []
  384. originalGroupTree.value = []
  385. } catch (error) {
  386. ElMessage.error('获取分组数据失败')
  387. }
  388. }
  389. // 搜索过滤分组
  390. const filterGroups = () => {
  391. if (!groupSearchKey.value) {
  392. // 如果搜索关键字为空,恢复原始数据
  393. groupTreeData.value = JSON.parse(JSON.stringify(originalGroupTree.value))
  394. return
  395. }
  396. // 递归搜索函数
  397. const searchTree = (nodes) => {
  398. return nodes.filter(node => {
  399. // 当前节点名称匹配
  400. const matchesName = node.Name.toLowerCase().includes(groupSearchKey.value.toLowerCase())
  401. // 递归搜索子节点
  402. if (node.Children && node.Children.length) {
  403. node.Children = searchTree(node.Children)
  404. // 如果子节点有匹配项,则保留父节点
  405. return matchesName || node.Children.length > 0
  406. }
  407. return matchesName
  408. })
  409. }
  410. groupTreeData.value = searchTree(JSON.parse(JSON.stringify(originalGroupTree.value)))
  411. }
  412. // 点击分组节点
  413. const handleGroupClick = (data) => {
  414. // 设置当前选中分组
  415. Object.assign(currentGroup, data)
  416. // 更新查询参数,加入分组条件
  417. params.groupKey = data.GroupKey
  418. // 重新获取列表
  419. getList(1)
  420. }
  421. // 添加分组
  422. const addGroup = () => {
  423. groupFormRef.value.open()
  424. }
  425. // 处理分组操作
  426. const handleGroupCommand = (command, data) => {
  427. switch (command) {
  428. case 'edit':
  429. groupFormRef.value.open(data)
  430. break
  431. case 'add_child':
  432. groupFormRef.value.open(null, data.GroupKey)
  433. break
  434. case 'delete':
  435. deleteGroupConfirm(data)
  436. break
  437. }
  438. }
  439. // 删除分组确认
  440. const deleteGroupConfirm = (data) => {
  441. ElMessageBox.confirm(`确定要删除分组「${data.Name}」吗?如果包含子分组或API,将一并删除。`, '警告', {
  442. confirmButtonText: '确定',
  443. cancelButtonText: '取消',
  444. type: 'warning'
  445. }).then(async () => {
  446. try {
  447. // 实际使用时调用API
  448. await deleteGroup([data.Id])
  449. ElMessage.success('删除成功')
  450. // 如果当前选中的是要删除的分组,则清空当前分组
  451. if (currentGroup.GroupKey === data.GroupKey) {
  452. Object.assign(currentGroup, {
  453. Id: undefined,
  454. GroupKey: '',
  455. Name: '',
  456. ParentId: 0,
  457. Description: ''
  458. })
  459. params.groupKey = ''
  460. getList(1)
  461. }
  462. // 刷新分组树
  463. refreshGroups()
  464. } catch (error) {
  465. ElMessage.error('删除失败')
  466. }
  467. }).catch(() => {})
  468. }
  469. // 根据请求方法返回不同的标签类型
  470. const getMethodTagType = (method: string) => {
  471. switch (method.toUpperCase()) {
  472. case 'GET':
  473. return 'success'
  474. case 'POST':
  475. return 'primary'
  476. case 'PUT':
  477. return 'warning'
  478. case 'DELETE':
  479. return 'danger'
  480. default:
  481. return 'info'
  482. }
  483. }
  484. // 重置查询表单
  485. const resetQuery = () => {
  486. queryRef.value.resetFields()
  487. getList(1)
  488. }
  489. // 新增或编辑API
  490. const addOrEdit = (row?: ApiDefinition) => {
  491. if (row) {
  492. // 编辑现有API
  493. // 实际使用时,可能需要先获取详情
  494. editFormRef.value.open(row)
  495. } else {
  496. // 新增API,如果有选中分组,则传递分组标识
  497. editFormRef.value.open(null, currentGroup.groupKey)
  498. }
  499. }
  500. // 查看API详情
  501. const viewDetail = (row: ApiDefinition) => {
  502. viewDetailRef.value.open(row)
  503. }
  504. // 测试API
  505. const testApi = (row: ApiDefinition) => {
  506. testApiRef.value.open(row)
  507. }
  508. // 处理下拉菜单命令
  509. const handleCommand = (command: string, row: ApiDefinition) => {
  510. switch (command) {
  511. case 'publish':
  512. publishApi(row)
  513. break
  514. case 'deprecate':
  515. deprecateApi(row)
  516. break
  517. case 'delete':
  518. deleteApi(row)
  519. break
  520. }
  521. }
  522. // 发布API
  523. const publishApi = (row: ApiDefinition) => {
  524. ElMessageBox.confirm(`确定要发布API「${row.name}」吗?`, '提示', {
  525. confirmButtonText: '确定',
  526. cancelButtonText: '取消',
  527. type: 'warning'
  528. }).then(async () => {
  529. // 实际使用时替换为真实API调用
  530. // await api.apihub.publish({ id: row.id })
  531. ElMessage.success('发布成功')
  532. getList()
  533. }).catch(() => {})
  534. }
  535. // 废弃API
  536. const deprecateApi = (row: ApiDefinition) => {
  537. ElMessageBox.confirm(`确定要废弃API「${row.name}」吗?`, '提示', {
  538. confirmButtonText: '确定',
  539. cancelButtonText: '取消',
  540. type: 'warning'
  541. }).then(async () => {
  542. // 实际使用时替换为真实API调用
  543. // await api.apihub.deprecate({ id: row.id })
  544. ElMessage.success('废弃成功')
  545. getList()
  546. }).catch(() => {})
  547. }
  548. // 删除API
  549. const deleteApi = (row: ApiDefinition) => {
  550. ElMessageBox.confirm(`确定要删除API「${row.name}」吗?此操作不可恢复!`, '警告', {
  551. confirmButtonText: '确定',
  552. cancelButtonText: '取消',
  553. type: 'error'
  554. }).then(async () => {
  555. // 实际使用时替换为真实API调用
  556. // await api.apihub.delete({ ids: [row.id] })
  557. ElMessage.success('删除成功')
  558. getList()
  559. }).catch(() => {})
  560. }
  561. </script>
  562. <style scoped>
  563. .ml10 {
  564. margin-left: 10px;
  565. }
  566. .apihub-container {
  567. display: flex;
  568. height: calc(100vh - 160px);
  569. }
  570. .apihub-sidebar {
  571. width: 280px;
  572. margin-right: 16px;
  573. overflow: hidden;
  574. }
  575. .apihub-content {
  576. flex: 1;
  577. overflow: hidden;
  578. }
  579. .group-card {
  580. height: 100%;
  581. display: flex;
  582. flex-direction: column;
  583. }
  584. .card-header {
  585. display: flex;
  586. justify-content: space-between;
  587. align-items: center;
  588. }
  589. .header-actions {
  590. display: flex;
  591. gap: 8px;
  592. }
  593. .group-search {
  594. margin-bottom: 12px;
  595. }
  596. .group-tree-container {
  597. overflow-y: auto;
  598. flex: 1;
  599. min-height: 200px; /* 确保容器有最小高度 */
  600. border: 1px solid #ebeef5; /* 添加边框以便于调试 */
  601. padding: 10px;
  602. }
  603. .custom-tree-node {
  604. flex: 1;
  605. display: flex;
  606. align-items: center;
  607. justify-content: space-between;
  608. font-size: 14px;
  609. padding-right: 8px;
  610. }
  611. .api-count {
  612. padding: 0 6px;
  613. background-color: #f0f0f0;
  614. border-radius: 10px;
  615. font-size: 12px;
  616. color: #606266;
  617. }
  618. .node-actions {
  619. margin-left: 8px;
  620. visibility: hidden;
  621. }
  622. .custom-tree-node:hover .node-actions {
  623. visibility: visible;
  624. }
  625. .api-header {
  626. margin-bottom: 16px;
  627. font-size: 16px;
  628. }
  629. .group-name {
  630. font-weight: bold;
  631. color: #409EFF;
  632. }
  633. </style>