Переглянути джерело

Merge branch 'professional2' of http://git.mydig.net/Sagoo-Cloud/sagoo-admin-ui into professional2

vera_min 5 місяців тому
батько
коміт
373a34bebf

Різницю між файлами не показано, бо вона завелика
+ 894 - 0
src/theme/form-create.scss


+ 1 - 0
src/theme/index.scss

@@ -7,6 +7,7 @@
 @import './waves.scss';
 @import './dark.scss';
 @import './fast.scss';
+@import './form-create.scss';
 
 
 // 自定义指令 v-auth 增加 v-disabled 的类

+ 699 - 0
src/views/apihub/apilist.vue

@@ -0,0 +1,699 @@
+<template>
+	<div class="page">
+		<div class="apihub-container">
+			<!-- 左侧分组树 -->
+			<div class="apihub-sidebar">
+				<el-card shadow="never" class="group-card">
+					<template #header>
+						<div class="card-header">
+							<span>API分组</span>
+							<div class="header-actions">
+								<el-button type="primary" size="small" @click="addGroup" v-auth="'add_group'">
+									<el-icon><ele-Plus /></el-icon>
+								</el-button>
+								<el-button type="primary" size="small" @click="refreshGroups">
+									<el-icon><ele-Refresh /></el-icon>
+								</el-button>
+							</div>
+						</div>
+					</template>
+					<div class="group-search">
+						<el-input
+							v-model="groupSearchKey"
+							placeholder="搜索分组"
+							clearable
+							prefix-icon="ele-Search"
+							@input="filterGroups"
+						/>
+					</div>
+					<div class="group-tree-container">
+						<el-tree
+							ref="groupTreeRef"
+							:data="groupTreeData"
+							:props="{ label: 'Name', children: 'Children' }"
+							node-key="Id"
+							highlight-current
+							:expand-on-click-node="true"
+							default-expand-all
+							@node-click="handleGroupClick"
+						>
+							<template #default="{ node, data }">
+								<div class="custom-tree-node">
+									<span>{{ node.label }}</span>
+									<span class="api-count" v-if="data.ApiCount">{{ data.ApiCount }}</span>
+									<div class="node-actions">
+										<el-dropdown @command="(command) => handleGroupCommand(command, data)" trigger="click">
+											<el-icon><ele-More /></el-icon>
+											<template #dropdown>
+												<el-dropdown-menu>
+													<el-dropdown-item command="edit" v-auth="'edit_group'">编辑</el-dropdown-item>
+													<el-dropdown-item command="add_child" v-auth="'add_group'">添加子分组</el-dropdown-item>
+													<el-dropdown-item command="delete" v-auth="'delete_group'">删除</el-dropdown-item>
+												</el-dropdown-menu>
+											</template>
+										</el-dropdown>
+									</div>
+								</div>
+							</template>
+						</el-tree>
+					</div>
+				</el-card>
+			</div>
+
+			<!-- 右侧API列表 -->
+			<div class="apihub-content">
+				<el-card shadow="never">
+					<div class="api-header">
+						<div class="current-group" v-if="currentGroup.name">
+							当前分组: <span class="group-name">{{ currentGroup.name }}</span>
+						</div>
+						<div class="current-group" v-else>
+							全部API
+						</div>
+					</div>
+					<el-form :model="params" inline ref="queryRef">
+				<el-form-item label="API名称" prop="keyWord">
+					<el-input v-model="params.keyWord" placeholder="请输入API名称" clearable style="width: 180px" @keyup.enter.native="getList(1)" />
+				</el-form-item>
+				<el-form-item label="数据源" prop="dataSourceId">
+					<el-select v-model="params.dataSourceId" placeholder="请选择数据源" clearable style="width: 180px">
+						<el-option v-for="item in dataSources" :key="item.id" :label="item.name" :value="item.id" />
+					</el-select>
+				</el-form-item>
+				<el-form-item label="状态" prop="status">
+					<el-select v-model="params.status" placeholder="请选择状态" clearable style="width: 120px">
+						<el-option label="全部" value="" />
+						<el-option label="草稿" value="Draft" />
+						<el-option label="已发布" value="Published" />
+						<el-option label="已废弃" value="Deprecated" />
+					</el-select>
+				</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" class="ml10" @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'">
+						<el-icon>
+							<ele-FolderAdd />
+						</el-icon>
+						新增API
+					</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="API名称" min-width="120" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="path" label="API路径" min-width="150" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="method" label="请求方法" width="100" align="center">
+					<template #default="scope">
+						<el-tag
+							:type="getMethodTagType(scope.row.method)"
+							size="small"
+						>
+							{{ scope.row.method }}
+						</el-tag>
+					</template>
+				</el-table-column>
+				<el-table-column prop="dataSourceName" label="数据源" width="120" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="sqlType" label="SQL类型" width="100" align="center">
+					<template #default="scope">
+						<el-tag size="small" type="info" v-if="scope.row.sqlType === 'query'">查询</el-tag>
+						<el-tag size="small" type="warning" v-else-if="scope.row.sqlType === 'procedure'">存储过程</el-tag>
+						<span v-else>{{ scope.row.sqlType }}</span>
+					</template>
+				</el-table-column>
+				<el-table-column prop="version" label="版本" width="80" align="center"></el-table-column>
+				<el-table-column prop="status" label="状态" width="100" align="center">
+					<template #default="scope">
+						<el-tag size="small" v-if="scope.row.status === 'Draft'">草稿</el-tag>
+						<el-tag size="small" type="success" v-else-if="scope.row.status === 'Published'">已发布</el-tag>
+						<el-tag size="small" type="info" v-else-if="scope.row.status === 'Deprecated'">已废弃</el-tag>
+						<span v-else>{{ scope.row.status }}</span>
+					</template>
+				</el-table-column>
+				<el-table-column prop="createdAt" label="创建时间" width="160" align="center"></el-table-column>
+				<el-table-column label="操作" width="220" align="center">
+					<template #default="scope">
+						<el-button size="small" text type="primary" @click="viewDetail(scope.row)" v-auth="'view'">查看</el-button>
+						<el-button size="small" text type="warning" @click="addOrEdit(scope.row)" v-auth="'edit'">编辑</el-button>
+						<el-button size="small" text type="success" @click="testApi(scope.row)" v-auth="'test'">测试</el-button>
+						<el-dropdown @command="(command) => handleCommand(command, scope.row)">
+							<el-button size="small" text type="primary">
+								更多<el-icon class="el-icon--right"><arrow-down /></el-icon>
+							</el-button>
+							<template #dropdown>
+								<el-dropdown-menu>
+									<el-dropdown-item command="publish" v-if="scope.row.status === 'Draft'" v-auth="'publish'">发布</el-dropdown-item>
+									<el-dropdown-item command="deprecate" v-if="scope.row.status === 'Published'" v-auth="'deprecate'">废弃</el-dropdown-item>
+									<el-dropdown-item command="delete" v-auth="'delete'">删除</el-dropdown-item>
+								</el-dropdown-menu>
+							</template>
+						</el-dropdown>
+					</template>
+				</el-table-column>
+			</el-table>
+					<pagination v-if="params.total" :total="params.total" v-model:page="params.pageNum" v-model:limit="params.pageSize" @pagination="getList()" />
+				</el-card>
+			</div>
+		</div>
+
+		<!-- 组件 -->
+		<EditForm ref="editFormRef" @getList="getList(1)"></EditForm>
+		<ViewDetail ref="viewDetailRef"></ViewDetail>
+		<TestApi ref="testApiRef"></TestApi>
+		<GroupForm ref="groupFormRef" @refresh="refreshGroups"></GroupForm>
+	</div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, reactive } from 'vue'
+import EditForm from './component/edit.vue'
+import ViewDetail from './component/view.vue'
+import TestApi from './component/test.vue'
+import GroupForm from './component/group.vue'
+import { useSearch } from '/@/hooks/useCommon'
+import { ElMessageBox, ElMessage } from 'element-plus'
+import { ArrowDown } from '@element-plus/icons-vue'
+import request from '/@/utils/request'
+
+// 定义API接口类型
+interface ApiDefinition {
+	id?: number
+	name: string
+	path: string
+	method: string
+	dataSourceId: number
+	dataSourceName?: string
+	sqlType: string
+	sqlContent: string
+	parameters?: any[]
+	returnFormat: string
+	version: string
+	status: string
+	plugins?: any[]
+	description?: string
+	createdAt?: string
+	updatedAt?: string
+}
+
+// 引用组件
+const editFormRef = ref()
+const viewDetailRef = ref()
+const testApiRef = ref()
+const queryRef = ref()
+const groupFormRef = ref()
+const groupTreeRef = ref()
+
+// 分组相关状态
+const groupSearchKey = ref('')
+const groupTreeData = ref([])
+const currentGroup = reactive({
+	Id: undefined,
+	GroupKey: '',
+	Name: '',
+	ParentId: 0,
+	Description: ''
+})
+const originalGroupTree = ref([])
+
+// 数据源列表
+const dataSources = ref([])
+
+// API列表请求函数
+const apiRequest = (params: any) => {
+	// 这里使用request函数发起请求
+	// 实际使用时替换为真实API路径
+	return request({
+		url: '/api/list',
+		method: 'get',
+		params
+	})
+}
+
+// 获取分组树形结构
+const getGroupTree = () => {
+	return request({
+		url: '/api_group/tree',
+		method: 'get'
+	})
+}
+
+// 删除分组
+const deleteGroup = (ids: number[]) => {
+	return request({
+		url: '/api_group/delete',
+		method: 'post',
+		data: { ids }
+	})
+}
+
+// 使用通用搜索钩子
+const { params, tableData, getList, loading } = useSearch<ApiDefinition[]>(
+	apiRequest,
+	'data',
+	{
+		keyWord: '',
+		dataSourceId: '',
+		status: '',
+		dateRange: [],
+		orderBy: '',
+		pageNum: 1,
+		pageSize: 10
+	}
+)
+
+// 加载数据源列表
+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 {
+		// 调用API获取分组树
+		const res = await getGroupTree()
+		console.log('获取到的API分组数据:', res)
+
+		// 使用API返回的数据
+		if (res.data && res.data.list) {
+			// 获取原始数据
+			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
+		}
+
+		// 如果没有数据,初始化为空数组
+		groupTreeData.value = []
+		originalGroupTree.value = []
+	} catch (error) {
+		ElMessage.error('获取分组数据失败')
+	}
+}
+
+// 搜索过滤分组
+const filterGroups = () => {
+	if (!groupSearchKey.value) {
+		// 如果搜索关键字为空,恢复原始数据
+		groupTreeData.value = JSON.parse(JSON.stringify(originalGroupTree.value))
+		return
+	}
+
+	// 递归搜索函数
+	const searchTree = (nodes) => {
+		return nodes.filter(node => {
+			// 当前节点名称匹配
+			const matchesName = node.Name.toLowerCase().includes(groupSearchKey.value.toLowerCase())
+
+			// 递归搜索子节点
+			if (node.Children && node.Children.length) {
+				node.Children = searchTree(node.Children)
+				// 如果子节点有匹配项,则保留父节点
+				return matchesName || node.Children.length > 0
+			}
+
+			return matchesName
+		})
+	}
+
+	groupTreeData.value = searchTree(JSON.parse(JSON.stringify(originalGroupTree.value)))
+}
+
+// 点击分组节点
+const handleGroupClick = (data) => {
+	// 设置当前选中分组
+	Object.assign(currentGroup, data)
+
+	// 更新查询参数,加入分组条件
+	params.groupKey = data.GroupKey
+
+	// 重新获取列表
+	getList(1)
+}
+
+// 添加分组
+const addGroup = () => {
+	groupFormRef.value.open()
+}
+
+// 处理分组操作
+const handleGroupCommand = (command, data) => {
+	switch (command) {
+		case 'edit':
+			groupFormRef.value.open(data)
+			break
+		case 'add_child':
+			groupFormRef.value.open(null, data.GroupKey)
+			break
+		case 'delete':
+			deleteGroupConfirm(data)
+			break
+	}
+}
+
+// 删除分组确认
+const deleteGroupConfirm = (data) => {
+	ElMessageBox.confirm(`确定要删除分组「${data.Name}」吗?如果包含子分组或API,将一并删除。`, '警告', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning'
+	}).then(async () => {
+		try {
+			// 实际使用时调用API
+			await deleteGroup([data.Id])
+			ElMessage.success('删除成功')
+
+			// 如果当前选中的是要删除的分组,则清空当前分组
+			if (currentGroup.GroupKey === data.GroupKey) {
+				Object.assign(currentGroup, {
+					Id: undefined,
+					GroupKey: '',
+					Name: '',
+					ParentId: 0,
+					Description: ''
+				})
+				params.groupKey = ''
+				getList(1)
+			}
+
+			// 刷新分组树
+			refreshGroups()
+		} catch (error) {
+			ElMessage.error('删除失败')
+		}
+	}).catch(() => {})
+}
+
+// 根据请求方法返回不同的标签类型
+const getMethodTagType = (method: string) => {
+	switch (method.toUpperCase()) {
+		case 'GET':
+			return 'success'
+		case 'POST':
+			return 'primary'
+		case 'PUT':
+			return 'warning'
+		case 'DELETE':
+			return 'danger'
+		default:
+			return 'info'
+	}
+}
+
+// 重置查询表单
+const resetQuery = () => {
+	queryRef.value.resetFields()
+	getList(1)
+}
+
+// 新增或编辑API
+const addOrEdit = (row?: ApiDefinition) => {
+	if (row) {
+		// 编辑现有API
+		// 实际使用时,可能需要先获取详情
+		editFormRef.value.open(row)
+	} else {
+		// 新增API,如果有选中分组,则传递分组标识
+		editFormRef.value.open(null, currentGroup.groupKey)
+	}
+}
+
+// 查看API详情
+const viewDetail = (row: ApiDefinition) => {
+	viewDetailRef.value.open(row)
+}
+
+// 测试API
+const testApi = (row: ApiDefinition) => {
+	testApiRef.value.open(row)
+}
+
+// 处理下拉菜单命令
+const handleCommand = (command: string, row: ApiDefinition) => {
+	switch (command) {
+		case 'publish':
+			publishApi(row)
+			break
+		case 'deprecate':
+			deprecateApi(row)
+			break
+		case 'delete':
+			deleteApi(row)
+			break
+	}
+}
+
+// 发布API
+const publishApi = (row: ApiDefinition) => {
+	ElMessageBox.confirm(`确定要发布API「${row.name}」吗?`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning'
+	}).then(async () => {
+		// 实际使用时替换为真实API调用
+		// await api.apihub.publish({ id: row.id })
+		ElMessage.success('发布成功')
+		getList()
+	}).catch(() => {})
+}
+
+// 废弃API
+const deprecateApi = (row: ApiDefinition) => {
+	ElMessageBox.confirm(`确定要废弃API「${row.name}」吗?`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning'
+	}).then(async () => {
+		// 实际使用时替换为真实API调用
+		// await api.apihub.deprecate({ id: row.id })
+		ElMessage.success('废弃成功')
+		getList()
+	}).catch(() => {})
+}
+
+// 删除API
+const deleteApi = (row: ApiDefinition) => {
+	ElMessageBox.confirm(`确定要删除API「${row.name}」吗?此操作不可恢复!`, '警告', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'error'
+	}).then(async () => {
+		// 实际使用时替换为真实API调用
+		// await api.apihub.delete({ ids: [row.id] })
+		ElMessage.success('删除成功')
+		getList()
+	}).catch(() => {})
+}
+</script>
+
+<style scoped>
+.ml10 {
+	margin-left: 10px;
+}
+
+.apihub-container {
+	display: flex;
+	height: calc(100vh - 160px);
+}
+
+.apihub-sidebar {
+	width: 280px;
+	margin-right: 16px;
+	overflow: hidden;
+}
+
+.apihub-content {
+	flex: 1;
+	overflow: hidden;
+}
+
+.group-card {
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+}
+
+.card-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
+
+.header-actions {
+	display: flex;
+	gap: 8px;
+}
+
+.group-search {
+	margin-bottom: 12px;
+}
+
+.group-tree-container {
+	overflow-y: auto;
+	flex: 1;
+	min-height: 200px; /* 确保容器有最小高度 */
+	border: 1px solid #ebeef5; /* 添加边框以便于调试 */
+	padding: 10px;
+}
+
+.custom-tree-node {
+	flex: 1;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	font-size: 14px;
+	padding-right: 8px;
+}
+
+.api-count {
+	padding: 0 6px;
+	background-color: #f0f0f0;
+	border-radius: 10px;
+	font-size: 12px;
+	color: #606266;
+}
+
+.node-actions {
+	margin-left: 8px;
+	visibility: hidden;
+}
+
+.custom-tree-node:hover .node-actions {
+	visibility: visible;
+}
+
+.api-header {
+	margin-bottom: 16px;
+	font-size: 16px;
+}
+
+.group-name {
+	font-weight: bold;
+	color: #409EFF;
+}
+</style>

+ 340 - 0
src/views/apihub/component/edit.vue

@@ -0,0 +1,340 @@
+<template>
+	<el-dialog
+		class="api-edit"
+		v-model="showDialog"
+		:title="`${formData.id ? '编辑API' : '新增API'}`"
+		width="800px"
+		:close-on-click-modal="false"
+		:close-on-press-escape="false"
+	>
+		<el-form ref="formRef" :model="formData" :rules="ruleForm" label-width="100px" @keyup.enter="onSubmit">
+			<el-form-item label="API名称" prop="name">
+				<el-input v-model="formData.name" placeholder="请输入API名称" />
+			</el-form-item>
+			<el-form-item label="API路径" prop="path">
+				<el-input v-model="formData.path" placeholder="请输入API路径,如/api/v1/users" />
+			</el-form-item>
+			<el-form-item label="请求方法" prop="method">
+				<el-select v-model="formData.method" placeholder="请选择请求方法">
+					<el-option label="GET" value="GET"></el-option>
+					<el-option label="POST" value="POST"></el-option>
+					<el-option label="PUT" value="PUT"></el-option>
+					<el-option label="DELETE" value="DELETE"></el-option>
+				</el-select>
+			</el-form-item>
+			<el-form-item label="数据源" prop="dataSourceId">
+				<el-select v-model="formData.dataSourceId" placeholder="请选择数据源" filterable>
+					<el-option v-for="item in dataSources" :key="item.id" :label="item.name" :value="item.id"></el-option>
+				</el-select>
+			</el-form-item>
+			<el-form-item label="SQL类型" prop="sqlType">
+				<el-radio-group v-model="formData.sqlType">
+					<el-radio label="query">查询</el-radio>
+					<el-radio label="procedure">存储过程</el-radio>
+				</el-radio-group>
+			</el-form-item>
+			<el-form-item label="所属分组" prop="groupKey">
+				<el-cascader
+					v-model="formData.groupKey"
+					:options="groupOptions"
+					:props="{ checkStrictly: true, emitPath: false, value: 'groupKey', label: 'name' }"
+					placeholder="请选择所属分组"
+					clearable
+					style="width: 100%"
+				/>
+			</el-form-item>
+			<el-form-item label="SQL内容" prop="sqlContent">
+				<el-input v-model="formData.sqlContent" type="textarea" :rows="4" placeholder="请输入SQL语句或存储过程名称" />
+			</el-form-item>
+			
+			<el-form-item label="参数定义">
+				<el-button type="primary" size="small" @click="addParameter">
+					<el-icon><ele-Plus /></el-icon>添加参数
+				</el-button>
+				<el-table :data="formData.parameters" style="width: 100%; margin-top: 10px;" border>
+					<el-table-column label="参数名" width="150">
+						<template #default="scope">
+							<el-input v-model="scope.row.name" placeholder="参数名"></el-input>
+						</template>
+					</el-table-column>
+					<el-table-column label="类型" width="120">
+						<template #default="scope">
+							<el-select v-model="scope.row.type" placeholder="类型">
+								<el-option label="string" value="string"></el-option>
+								<el-option label="int" value="int"></el-option>
+								<el-option label="float" value="float"></el-option>
+								<el-option label="bool" value="bool"></el-option>
+							</el-select>
+						</template>
+					</el-table-column>
+					<el-table-column label="必填" width="80">
+						<template #default="scope">
+							<el-checkbox v-model="scope.row.required"></el-checkbox>
+						</template>
+					</el-table-column>
+					<el-table-column label="默认值" width="120">
+						<template #default="scope">
+							<el-input v-model="scope.row.defaultValue" placeholder="默认值"></el-input>
+						</template>
+					</el-table-column>
+					<el-table-column label="描述">
+						<template #default="scope">
+							<el-input v-model="scope.row.description" placeholder="参数描述"></el-input>
+						</template>
+					</el-table-column>
+					<el-table-column label="操作" width="80">
+						<template #default="scope">
+							<el-button type="danger" size="small" @click="removeParameter(scope.$index)" text>
+								<el-icon><ele-Delete /></el-icon>
+							</el-button>
+						</template>
+					</el-table-column>
+				</el-table>
+			</el-form-item>
+			
+			<el-form-item label="返回格式" prop="returnFormat">
+				<el-select v-model="formData.returnFormat" placeholder="请选择返回格式">
+					<el-option label="JSON" value="JSON"></el-option>
+					<el-option label="XML" value="XML"></el-option>
+				</el-select>
+			</el-form-item>
+			<el-form-item label="版本" prop="version">
+				<el-input v-model="formData.version" placeholder="请输入版本号,如1.0" />
+			</el-form-item>
+			<el-form-item label="状态" prop="status">
+				<el-select v-model="formData.status" placeholder="请选择状态">
+					<el-option label="草稿" value="Draft"></el-option>
+					<el-option label="已发布" value="Published"></el-option>
+					<el-option label="已废弃" value="Deprecated"></el-option>
+				</el-select>
+			</el-form-item>
+			<el-form-item label="描述" prop="description">
+				<el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入API描述" />
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<div class="dialog-footer">
+				<el-button @click="cancel">取消</el-button>
+				<el-button type="primary" @click="onSubmit">确定</el-button>
+			</div>
+		</template>
+	</el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
+import { ruleRequired } from '/@/utils/validator'
+import request from '/@/utils/request'
+
+const emit = defineEmits(['getList'])
+
+const showDialog = ref(false)
+const formRef = ref()
+const dataSources = ref([
+	{ id: 1, name: '主数据库' },
+	{ id: 2, name: '业务数据库' },
+	{ id: 3, name: '日志数据库' }
+])
+
+// 分组选项
+const groupOptions = ref([])
+
+// 定义基础表单数据
+const baseForm = {
+	id: undefined,
+	name: '',
+	path: '',
+	method: 'GET',
+	dataSourceId: undefined,
+	sqlType: 'query',
+	sqlContent: '',
+	parameters: [],
+	returnFormat: 'JSON',
+	version: '1.0',
+	status: 'Draft',
+	plugins: [],
+	groupKey: '',
+	description: ''
+}
+
+const formData = reactive(JSON.parse(JSON.stringify(baseForm)))
+
+// 表单验证规则
+const ruleForm = {
+	name: [ruleRequired('API名称不能为空', 'change')],
+	path: [ruleRequired('API路径不能为空', 'change')],
+	method: [ruleRequired('请求方法不能为空', 'change')],
+	dataSourceId: [ruleRequired('数据源不能为空', 'change')],
+	sqlType: [ruleRequired('SQL类型不能为空', 'change')],
+	sqlContent: [ruleRequired('SQL内容不能为空', 'change')],
+	returnFormat: [ruleRequired('返回格式不能为空', 'change')],
+	version: [ruleRequired('版本不能为空', 'change')],
+	status: [ruleRequired('状态不能为空', 'change')]
+}
+
+// 添加参数
+const addParameter = () => {
+	if (!formData.parameters) {
+		formData.parameters = []
+	}
+	formData.parameters.push({
+		name: '',
+		type: 'string',
+		required: false,
+		defaultValue: '',
+		description: ''
+	})
+}
+
+// 移除参数
+const removeParameter = (index) => {
+	formData.parameters.splice(index, 1)
+}
+
+// 添加API
+const addApi = (data) => {
+	return request({
+		url: '/api/add',
+		method: 'post',
+		data
+	})
+}
+
+// 编辑API
+const editApi = (data) => {
+	return request({
+		url: '/api/edit',
+		method: 'put',
+		data
+	})
+}
+
+// 提交表单
+const onSubmit = async () => {
+	try {
+		// 表单验证
+		await formRef.value.validate()
+		
+		// 准备提交数据
+		const submitData = JSON.parse(JSON.stringify(formData))
+		
+		// 调用API
+		if (submitData.id) {
+			// 编辑模式
+			await editApi(submitData)
+		} else {
+			// 新增模式
+			await addApi(submitData)
+		}
+		
+		ElMessage.success('操作成功')
+		resetForm()
+		showDialog.value = false
+		emit('getList')
+	} catch (error) {
+		ElMessage.error(error.message || '操作失败')
+	}
+}
+
+// 重置表单
+const resetForm = () => {
+	Object.assign(formData, JSON.parse(JSON.stringify(baseForm)))
+	formRef.value && formRef.value.resetFields()
+}
+
+// 取消操作
+const cancel = () => {
+	resetForm()
+	showDialog.value = false
+}
+
+// 获取分组树形结构
+const getGroupTree = async () => {
+	try {
+		// 调用API获取分组树
+		const res = await request({
+			url: '/api_group/tree',
+			method: 'get'
+		})
+		return res.data?.list || []
+		
+		// 模拟数据
+		return [
+			{
+				id: 1,
+				groupKey: 'test_group',
+				name: '测试分组',
+				parentKey: '',
+				sort: 0,
+				description: '这是一个测试分组',
+				children: [
+					{
+						id: 3,
+						groupKey: 'test_subgroup',
+						name: '测试子分组',
+						parentKey: 'test_group',
+						sort: 0,
+						description: '这是一个测试子分组',
+						children: []
+					}
+				]
+			},
+			{
+				id: 2,
+				groupKey: 'user_api',
+				name: '用户API',
+				parentKey: '',
+				sort: 1,
+				description: '用户相关API',
+				children: []
+			}
+		]
+	} catch (error) {
+		ElMessage.error('获取分组树失败')
+		return []
+	}
+}
+
+// 加载分组选项
+const loadGroupOptions = async () => {
+	const treeData = await getGroupTree()
+	groupOptions.value = treeData
+}
+
+// 打开对话框
+const open = async (row?: any, defaultGroupKey?: string) => {
+	resetForm()
+	showDialog.value = true
+	
+	// 加载分组选项
+	await loadGroupOptions()
+	
+	if (row) {
+		// 编辑模式,填充表单数据
+		nextTick(() => {
+			// 如果是编辑模式,需要先获取详情
+			// 实际使用时,可能需要先调用API获取完整数据
+			// 这里模拟直接使用传入的行数据
+			Object.assign(formData, JSON.parse(JSON.stringify(row)))
+			
+			// 确保参数数组存在
+			if (!formData.parameters) {
+				formData.parameters = []
+			}
+		})
+	} else if (defaultGroupKey) {
+		// 新增模式,设置默认分组
+		formData.groupKey = defaultGroupKey
+	}
+}
+
+defineExpose({ open })
+</script>
+
+<style scoped>
+.dialog-footer {
+	display: flex;
+	justify-content: flex-end;
+}
+</style>

+ 205 - 0
src/views/apihub/component/group.vue

@@ -0,0 +1,205 @@
+<template>
+	<el-dialog
+		class="group-edit"
+		v-model="showDialog"
+		:title="`${formData.id ? '编辑分组' : '新增分组'}`"
+		width="500px"
+		:close-on-click-modal="false"
+		:close-on-press-escape="false"
+	>
+		<el-form ref="formRef" :model="formData" :rules="ruleForm" label-width="100px" @keyup.enter="onSubmit">
+			<el-form-item label="分组名称" prop="name">
+				<el-input v-model="formData.name" placeholder="请输入分组名称" />
+			</el-form-item>
+			<el-form-item label="分组标识" prop="groupKey">
+				<el-input v-model="formData.groupKey" placeholder="请输入分组唯一标识,留空则自动生成" />
+			</el-form-item>
+			<el-form-item label="父级分组" prop="parentKey">
+				<el-cascader
+					v-model="formData.parentKey"
+					:options="groupOptions"
+					:props="{ checkStrictly: true, emitPath: false, value: 'groupKey', label: 'name' }"
+					placeholder="请选择父级分组,不选择则为顶级分组"
+					clearable
+					style="width: 100%"
+				/>
+			</el-form-item>
+			<el-form-item label="排序号" prop="sort">
+				<el-input-number v-model="formData.sort" :min="0" :max="999" />
+			</el-form-item>
+			<el-form-item label="分组描述" prop="description">
+				<el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入分组描述" />
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<div class="dialog-footer">
+				<el-button @click="cancel">取消</el-button>
+				<el-button type="primary" @click="onSubmit">确定</el-button>
+			</div>
+		</template>
+	</el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
+import { ruleRequired } from '/@/utils/validator'
+import request from '/@/utils/request'
+
+const emit = defineEmits(['refresh'])
+
+const showDialog = ref(false)
+const formRef = ref()
+const groupOptions = ref([])
+
+// 定义基础表单数据
+const baseForm = {
+	id: undefined,
+	groupKey: '',
+	name: '',
+	parentKey: '',
+	sort: 0,
+	description: ''
+}
+
+const formData = reactive({...baseForm})
+
+// 表单验证规则
+const ruleForm = {
+	name: [ruleRequired('分组名称不能为空', 'change')]
+}
+
+// 获取分组树形结构
+const getGroupTree = async () => {
+	try {
+		// 调用API获取分组树
+		const res = await request({
+			url: '/api_group/tree',
+			method: 'get'
+		})
+		return res.data?.list || []
+	} catch (error) {
+		ElMessage.error('获取分组树失败')
+		return []
+	}
+}
+
+// 添加分组
+const addGroup = async (data) => {
+	return request({
+		url: '/api_group/add',
+		method: 'post',
+		data
+	})
+}
+
+// 编辑分组
+const editGroup = async (data) => {
+	return request({
+		url: '/api_group/edit',
+		method: 'post',
+		data
+	})
+}
+
+// 提交表单
+const onSubmit = async () => {
+	try {
+		// 表单验证
+		await formRef.value.validate()
+
+		// 准备提交数据
+		const submitData = JSON.parse(JSON.stringify(formData))
+
+		// 如果没有设置groupKey,生成一个基于名称的唯一标识
+		if (!submitData.groupKey) {
+			submitData.groupKey = 'group_' + Date.now() + '_' + Math.floor(Math.random() * 1000)
+		}
+
+		// 处理空的parent_key,将空字符串设置为null
+		if (submitData.parentKey === '') {
+			submitData.parentKey = null
+		}
+
+		// 调用API
+		if (submitData.id) {
+			// 编辑模式
+			await editGroup(submitData)
+		} else {
+			// 新增模式
+			await addGroup(submitData)
+		}
+
+		ElMessage.success('操作成功')
+		resetForm()
+		showDialog.value = false
+		emit('refresh')
+	} catch (error) {
+		ElMessage.error(error.message || '操作失败')
+	}
+}
+
+// 重置表单
+const resetForm = () => {
+	Object.assign(formData, {...baseForm})
+	formRef.value && formRef.value.resetFields()
+}
+
+// 取消操作
+const cancel = () => {
+	resetForm()
+	showDialog.value = false
+}
+
+// 加载分组选项
+const loadGroupOptions = async (excludeId) => {
+	const treeData = await getGroupTree()
+
+	// 如果是编辑模式,需要排除当前分组及其子分组
+	if (excludeId) {
+		const filterTree = (nodes) => {
+			return nodes.filter(node => {
+				if (node.id === excludeId) {
+					return false
+				}
+				if (node.children && node.children.length) {
+					node.children = filterTree(node.children)
+				}
+				return true
+			})
+		}
+
+		groupOptions.value = filterTree(treeData)
+	} else {
+		groupOptions.value = treeData
+	}
+}
+
+// 打开对话框
+const open = async (row?: any, parentKey?: string) => {
+	resetForm()
+	showDialog.value = true
+
+	// 加载分组选项
+	await loadGroupOptions(row?.id)
+
+	if (row) {
+		// 编辑模式,填充表单数据
+		nextTick(() => {
+			Object.assign(formData, JSON.parse(JSON.stringify(row)))
+		})
+	} else if (parentKey) {
+		// 新增子分组模式
+		formData.parentKey = parentKey
+	}
+}
+
+defineExpose({ open })
+</script>
+
+<style scoped>
+.dialog-footer {
+	display: flex;
+	justify-content: flex-end;
+}
+</style>

+ 318 - 0
src/views/apihub/component/test.vue

@@ -0,0 +1,318 @@
+<template>
+	<el-dialog
+		class="api-test"
+		v-model="showDialog"
+		title="测试API"
+		width="800px"
+		:close-on-click-modal="false"
+		:close-on-press-escape="false"
+	>
+		<el-descriptions :column="2" border>
+			<el-descriptions-item label="API名称" :span="2">{{ apiData.name }}</el-descriptions-item>
+			<el-descriptions-item label="API路径" :span="2">{{ apiData.path }}</el-descriptions-item>
+			<el-descriptions-item label="请求方法">
+				<el-tag :type="getMethodTagType(apiData.method)" size="small">{{ apiData.method }}</el-tag>
+			</el-descriptions-item>
+			<el-descriptions-item label="数据源">{{ apiData.dataSourceName }}</el-descriptions-item>
+		</el-descriptions>
+		
+		<div class="section-title">参数设置</div>
+		<el-form :model="testParams" label-width="120px" v-if="apiData.parameters && apiData.parameters.length">
+			<el-form-item 
+				v-for="param in apiData.parameters" 
+				:key="param.name" 
+				:label="param.name" 
+				:required="param.required"
+			>
+				<el-input 
+					v-if="param.type === 'string'" 
+					v-model="testParams[param.name]" 
+					:placeholder="getParamPlaceholder(param)"
+				></el-input>
+				<el-input-number 
+					v-else-if="param.type === 'int' || param.type === 'float'" 
+					v-model="testParams[param.name]" 
+					:placeholder="getParamPlaceholder(param)"
+				></el-input-number>
+				<el-switch 
+					v-else-if="param.type === 'bool'" 
+					v-model="testParams[param.name]" 
+					:active-value="true" 
+					:inactive-value="false"
+				></el-switch>
+				<el-input 
+					v-else 
+					v-model="testParams[param.name]" 
+					:placeholder="getParamPlaceholder(param)"
+				></el-input>
+				<div class="param-desc" v-if="param.description">{{ param.description }}</div>
+			</el-form-item>
+		</el-form>
+		<el-empty description="该API没有定义参数" v-else></el-empty>
+		
+		<div class="section-title">测试结果</div>
+		<div v-if="!testResult.success && !loading" class="test-placeholder">点击下方"执行测试"按钮开始测试</div>
+		<div v-else>
+			<el-alert
+				:title="testResult.message"
+				:type="testResult.success ? 'success' : 'error'"
+				:closable="false"
+				show-icon
+			></el-alert>
+			
+			<div v-if="testResult.success && testResult.data" class="result-container">
+				<el-tabs v-model="activeTab">
+					<el-tab-pane label="结果数据" name="data">
+						<pre class="result-json">{{ formatJson(testResult.data) }}</pre>
+					</el-tab-pane>
+					<el-tab-pane label="原始响应" name="raw">
+						<pre class="result-json">{{ formatJson(testResult) }}</pre>
+					</el-tab-pane>
+				</el-tabs>
+			</div>
+		</div>
+		
+		<template #footer>
+			<div class="dialog-footer">
+				<el-button @click="closeDialog">关闭</el-button>
+				<el-button type="primary" @click="runTest" :loading="loading">执行测试</el-button>
+			</div>
+		</template>
+	</el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, watch } from 'vue'
+
+const showDialog = ref(false)
+const loading = ref(false)
+const activeTab = ref('data')
+
+// API数据
+const apiData = reactive({
+	id: undefined,
+	name: '',
+	path: '',
+	method: '',
+	dataSourceId: undefined,
+	dataSourceName: '',
+	sqlType: '',
+	sqlContent: '',
+	parameters: [],
+	returnFormat: '',
+	version: '',
+	status: '',
+	plugins: [],
+	description: '',
+	createdAt: '',
+	updatedAt: ''
+})
+
+// 测试参数
+const testParams = reactive({})
+
+// 测试结果
+const testResult = reactive({
+	success: false,
+	message: '',
+	data: null
+})
+
+// 监听API数据变化,初始化测试参数
+watch(() => apiData.parameters, (newParams) => {
+	// 清空测试参数
+	Object.keys(testParams).forEach(key => {
+		delete testParams[key]
+	})
+	
+	// 根据参数定义初始化测试参数
+	if (newParams && newParams.length) {
+		newParams.forEach(param => {
+			// 如果有默认值,使用默认值
+			if (param.defaultValue !== undefined && param.defaultValue !== '') {
+				// 根据类型转换默认值
+				if (param.type === 'int') {
+					testParams[param.name] = parseInt(param.defaultValue)
+				} else if (param.type === 'float') {
+					testParams[param.name] = parseFloat(param.defaultValue)
+				} else if (param.type === 'bool') {
+					testParams[param.name] = param.defaultValue === 'true'
+				} else {
+					testParams[param.name] = param.defaultValue
+				}
+			} else {
+				// 否则设置为空值
+				if (param.type === 'int' || param.type === 'float') {
+					testParams[param.name] = undefined
+				} else if (param.type === 'bool') {
+					testParams[param.name] = false
+				} else {
+					testParams[param.name] = ''
+				}
+			}
+		})
+	}
+}, { deep: true })
+
+// 根据请求方法返回不同的标签类型
+const getMethodTagType = (method: string) => {
+	switch (method?.toUpperCase()) {
+		case 'GET':
+			return 'success'
+		case 'POST':
+			return 'primary'
+		case 'PUT':
+			return 'warning'
+		case 'DELETE':
+			return 'danger'
+		default:
+			return 'info'
+	}
+}
+
+// 获取参数占位符文本
+const getParamPlaceholder = (param) => {
+	if (param.required) {
+		return `请输入${param.description || param.name}(必填)`
+	}
+	return `请输入${param.description || param.name}(选填)`
+}
+
+// 格式化JSON
+const formatJson = (json) => {
+	try {
+		return JSON.stringify(json, null, 2)
+	} catch (e) {
+		return json
+	}
+}
+
+// 关闭对话框
+const closeDialog = () => {
+	showDialog.value = false
+}
+
+// 执行测试
+const runTest = async () => {
+	// 重置测试结果
+	testResult.success = false
+	testResult.message = ''
+	testResult.data = null
+	
+	loading.value = true
+	
+	try {
+		// 实际使用时,应该调用API进行测试
+		// const res = await api.apihub.test({ 
+		//   id: apiData.id,
+		//   parameters: testParams
+		// })
+		
+		// 模拟API调用
+		await new Promise(resolve => setTimeout(resolve, 1000))
+		
+		// 模拟测试结果
+		testResult.success = true
+		testResult.message = '测试成功'
+		
+		// 根据API类型模拟不同的测试数据
+		if (apiData.path.includes('users')) {
+			if (apiData.method === 'GET') {
+				testResult.data = {
+					id: 1,
+					username: 'admin',
+					email: 'admin@example.com',
+					created_at: '2025-05-01 10:00:00'
+				}
+			} else {
+				testResult.data = {
+					id: 10,
+					username: testParams.username || 'newuser',
+					email: testParams.email || 'newuser@example.com',
+					created_at: '2025-05-06 11:00:00'
+				}
+			}
+		} else {
+			testResult.data = {
+				result: '测试数据',
+				timestamp: new Date().toISOString()
+			}
+		}
+	} catch (error) {
+		testResult.success = false
+		testResult.message = error.message || '测试失败'
+	} finally {
+		loading.value = false
+	}
+}
+
+// 打开对话框
+const open = async (row: any) => {
+	// 清空数据
+	Object.keys(apiData).forEach(key => {
+		apiData[key] = undefined
+	})
+	
+	// 重置测试结果
+	testResult.success = false
+	testResult.message = ''
+	testResult.data = null
+	
+	showDialog.value = true
+	
+	// 实际使用时,应该调用API获取详细信息
+	// const res = await api.apihub.getDetail({ id: row.id })
+	// Object.assign(apiData, res)
+	
+	// 这里模拟直接使用传入的行数据
+	Object.assign(apiData, row)
+}
+
+defineExpose({ open })
+</script>
+
+<style scoped>
+.dialog-footer {
+	display: flex;
+	justify-content: flex-end;
+}
+
+.section-title {
+	font-weight: bold;
+	margin: 20px 0 10px;
+	font-size: 16px;
+	border-left: 4px solid #409EFF;
+	padding-left: 10px;
+}
+
+.param-desc {
+	font-size: 12px;
+	color: #909399;
+	margin-top: 5px;
+}
+
+.test-placeholder {
+	text-align: center;
+	color: #909399;
+	padding: 20px;
+	background-color: #f5f7fa;
+	border-radius: 4px;
+}
+
+.result-container {
+	margin-top: 15px;
+	border: 1px solid #e4e7ed;
+	border-radius: 4px;
+}
+
+.result-json {
+	background-color: #f5f7fa;
+	padding: 10px;
+	font-family: monospace;
+	white-space: pre-wrap;
+	word-break: break-all;
+	margin: 0;
+	max-height: 300px;
+	overflow: auto;
+}
+</style>

+ 162 - 0
src/views/apihub/component/view.vue

@@ -0,0 +1,162 @@
+<template>
+	<el-dialog
+		class="api-view"
+		v-model="showDialog"
+		title="API详情"
+		width="800px"
+		:close-on-click-modal="false"
+		:close-on-press-escape="false"
+	>
+		<el-descriptions :column="2" border>
+			<el-descriptions-item label="API名称" :span="2">{{ apiData.name }}</el-descriptions-item>
+			<el-descriptions-item label="API路径" :span="2">{{ apiData.path }}</el-descriptions-item>
+			<el-descriptions-item label="请求方法">
+				<el-tag :type="getMethodTagType(apiData.method)" size="small">{{ apiData.method }}</el-tag>
+			</el-descriptions-item>
+			<el-descriptions-item label="状态">
+				<el-tag size="small" v-if="apiData.status === 'Draft'">草稿</el-tag>
+				<el-tag size="small" type="success" v-else-if="apiData.status === 'Published'">已发布</el-tag>
+				<el-tag size="small" type="info" v-else-if="apiData.status === 'Deprecated'">已废弃</el-tag>
+				<span v-else>{{ apiData.status }}</span>
+			</el-descriptions-item>
+			<el-descriptions-item label="数据源">{{ apiData.dataSourceName }}</el-descriptions-item>
+			<el-descriptions-item label="版本">{{ apiData.version }}</el-descriptions-item>
+			<el-descriptions-item label="SQL类型">
+				<el-tag size="small" type="info" v-if="apiData.sqlType === 'query'">查询</el-tag>
+				<el-tag size="small" type="warning" v-else-if="apiData.sqlType === 'procedure'">存储过程</el-tag>
+				<span v-else>{{ apiData.sqlType }}</span>
+			</el-descriptions-item>
+			<el-descriptions-item label="返回格式">{{ apiData.returnFormat }}</el-descriptions-item>
+			<el-descriptions-item label="SQL内容" :span="2">
+				<div class="code-block">{{ apiData.sqlContent }}</div>
+			</el-descriptions-item>
+			<el-descriptions-item label="描述" :span="2">{{ apiData.description || '暂无描述' }}</el-descriptions-item>
+			<el-descriptions-item label="创建时间">{{ apiData.createdAt }}</el-descriptions-item>
+			<el-descriptions-item label="更新时间">{{ apiData.updatedAt }}</el-descriptions-item>
+		</el-descriptions>
+		
+		<div class="section-title">参数定义</div>
+		<el-table :data="apiData.parameters || []" style="width: 100%" border v-if="apiData.parameters && apiData.parameters.length">
+			<el-table-column prop="name" label="参数名" width="150"></el-table-column>
+			<el-table-column prop="type" label="类型" width="100"></el-table-column>
+			<el-table-column label="必填" width="80">
+				<template #default="scope">
+					<el-tag size="small" type="success" v-if="scope.row.required">是</el-tag>
+					<el-tag size="small" type="info" v-else>否</el-tag>
+				</template>
+			</el-table-column>
+			<el-table-column prop="defaultValue" label="默认值" width="120"></el-table-column>
+			<el-table-column prop="description" label="描述"></el-table-column>
+		</el-table>
+		<el-empty description="暂无参数" v-else></el-empty>
+		
+		<div class="section-title" v-if="apiData.plugins && apiData.plugins.length">关联插件</div>
+		<el-table :data="apiData.plugins || []" style="width: 100%" border v-if="apiData.plugins && apiData.plugins.length">
+			<el-table-column prop="pluginId" label="插件ID" width="100"></el-table-column>
+			<el-table-column prop="config" label="插件配置"></el-table-column>
+		</el-table>
+		
+		<template #footer>
+			<div class="dialog-footer">
+				<el-button @click="closeDialog">关闭</el-button>
+				<el-button type="primary" @click="testApi">测试API</el-button>
+			</div>
+		</template>
+	</el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue'
+
+const emit = defineEmits(['test'])
+const showDialog = ref(false)
+const apiData = reactive({
+	id: undefined,
+	name: '',
+	path: '',
+	method: '',
+	dataSourceId: undefined,
+	dataSourceName: '',
+	sqlType: '',
+	sqlContent: '',
+	parameters: [],
+	returnFormat: '',
+	version: '',
+	status: '',
+	plugins: [],
+	description: '',
+	createdAt: '',
+	updatedAt: ''
+})
+
+// 根据请求方法返回不同的标签类型
+const getMethodTagType = (method: string) => {
+	switch (method?.toUpperCase()) {
+		case 'GET':
+			return 'success'
+		case 'POST':
+			return 'primary'
+		case 'PUT':
+			return 'warning'
+		case 'DELETE':
+			return 'danger'
+		default:
+			return 'info'
+	}
+}
+
+// 关闭对话框
+const closeDialog = () => {
+	showDialog.value = false
+}
+
+// 测试API
+const testApi = () => {
+	closeDialog()
+	emit('test', apiData)
+}
+
+// 打开对话框
+const open = async (row: any) => {
+	// 清空数据
+	Object.keys(apiData).forEach(key => {
+		apiData[key] = undefined
+	})
+	
+	showDialog.value = true
+	
+	// 实际使用时,应该调用API获取详细信息
+	// const res = await api.apihub.getDetail({ id: row.id })
+	// Object.assign(apiData, res)
+	
+	// 这里模拟直接使用传入的行数据
+	Object.assign(apiData, row)
+}
+
+defineExpose({ open })
+</script>
+
+<style scoped>
+.dialog-footer {
+	display: flex;
+	justify-content: flex-end;
+}
+
+.section-title {
+	font-weight: bold;
+	margin: 20px 0 10px;
+	font-size: 16px;
+	border-left: 4px solid #409EFF;
+	padding-left: 10px;
+}
+
+.code-block {
+	background-color: #f5f7fa;
+	border: 1px solid #e4e7ed;
+	border-radius: 4px;
+	padding: 10px;
+	font-family: monospace;
+	white-space: pre-wrap;
+	word-break: break-all;
+}
+</style>

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

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
src/views/designer/form-create.es.js


+ 27 - 14
src/views/designer/view.vue

@@ -1,6 +1,6 @@
 <template>
 	<div class="page padding bg page-full">
-		<form-create v-model="formData" v-model:api="fapi" :rule="rule" :option="option" @submit="onSubmit"></form-create>
+		<form-create v-if="show" v-model="formData" v-model:api="fapi" :rule="rule" :option="option" @submit="onSubmit"></form-create>
 	</div>
 </template>
 
@@ -11,26 +11,33 @@ import formCreate from './form-create.es.js'
 import api from '/@/api/modules/designer'
 import { useRoute } from 'vue-router'
 import { ElMessage } from 'element-plus'
+import dayjs from 'dayjs'
 
 // 设计器中的拓展需要同步写在这里才能生效
 formCreate.extendApi((api) => {
-  /**
-   * 扩展一个 message 方法用于显示弹窗
-   * @param {string} msg - 要显示的消息内容
-   * @param {string} [type='info'] - 消息类型 ('success', 'warning', 'info', 'error')
-   */
-  api.message = (msg, type = 'info') => {
-    return ElMessage({
-      message: msg,
-      type: type, // 默认为 'info'
-    });
-  };
-});
+	/**
+	 * 扩展一个 message 方法用于显示弹窗
+	 * @param {string} msg - 要显示的消息内容
+	 * @param {string} [type='info'] - 消息类型 ('success', 'warning', 'info', 'error')
+	 */
+	api.message = (msg, type = 'info') => {
+		return ElMessage({
+			message: msg,
+			type: type, // 默认为 'info'
+		})
+	}
+
+	/**
+	 * 扩展一个 dayjs 方法用于日期格式化
+	 */
+	api.dayjs = dayjs
+})
 
 const route = useRoute()
 
 const code = route.path.replace('/designerPage/', '').replace('/designer/', '')
 
+const show = ref(false)
 const option = ref({})
 const rule = ref([])
 const fapi = ref(null)
@@ -41,10 +48,16 @@ api.get(code).then((res) => {
 		const data = JSON.parse(res.content)?.[0]
 		option.value = formCreate.parseJson(data?.options)
 		rule.value = formCreate.parseJson(data?.rule)
+		show.value = true
 	}
 })
 
-const onSubmit = (formData) => {
+const onSubmit = () => {
 	//todo 提交表单
 }
 </script>
+<style scoped>
+.from-create ::v-deep(.el-tree-node-disabled > .el-tree-node__content) {
+	color: var(--fc-text-color-3) !important;
+}
+</style>

+ 156 - 136
src/views/iot/dataAnalysis/IndicatorAggregation/index.vue

@@ -1,78 +1,89 @@
 <template>
-  <div class="page">
-    <el-card shadow="nover">
-      <el-form inline>
-        <el-form-item label="选择产品:" prop="productKey">
-          <el-select v-model="params.productKey" filterable placeholder="请选择产品" @change="productChange">
-            <el-option v-for="item in productList" :key="item.key" :label="item.name" :value="item.key">
-              <span style="float: left">{{ item.name }}</span>
-              <span style="float: right; font-size: 13px">{{ item.key }}</span>
-            </el-option>
-          </el-select>
-        </el-form-item>
-
-        <el-form-item label="选择设备:" prop="deviceKey">
-          <el-select v-model="params.deviceKey" filterable placeholder="请选择设备">
-            <el-option v-for="item in deviceList" :key="item.key" :label="item.name" :value="item.key">
-              <span style="float: left">{{ item.name }}</span>
-              <span style="float: right; font-size: 13px;margin-left: 12px;">{{ item.key }}</span>
-            </el-option>
-          </el-select>
-        </el-form-item>
-
-        <el-form-item label="选择属性:" prop="properties">
-          <el-select v-model="params.properties" filterable placeholder="请选择属性" @change="propertyChange">
-            <el-option v-for="item in propertyList" :key="item.key" :label="item.name" :value="item.key">
-              <span style="float: left">{{ item.name }}</span>
-              <span style="float: right; font-size: 13px;margin-left: 12px;">{{ item.key }}</span>
-            </el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="选择时间:" prop="dateRange">
-          <el-date-picker v-model="params.dateRange" style="width: 360px" value-format="YYYY-MM-DD HH:mm:ss" date-format="YYYY-MM-DD" time-format="HH:mm" format="YYYY-MM-DD HH:mm:ss" type="datetimerange" range-separator="-" start-placeholder="开始日期"
-            end-placeholder="结束日期" :clearable="false"></el-date-picker>
-        </el-form-item>
-        <el-form-item label="聚合粒度:" prop="dateType">
-          <el-radio-group v-model="params.dateType" @change="dateTypeChange">
-            <el-radio-button label="1">5分钟</el-radio-button>
-            <el-radio-button label="2">1小时</el-radio-button>
-            <el-radio-button label="3">1天</el-radio-button>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" class="ml10" @click="getData">
-            <el-icon>
-              <ele-Search />
-            </el-icon>
-            查询
-          </el-button>
-          <el-button type="primary" @click="handlePrintChart">
-            <el-icon>
-              <ele-Printer />
-            </el-icon>
-            打印图表
-          </el-button>
-        </el-form-item>
-      </el-form>
-      <div class="title">
-        <el-icon style="margin-right: 5px;">
-          <ele-Histogram />
-        </el-icon>
-        指标聚合统计图
-      </div>
-      <Chart class="flex1" height="12vw" ref="chart" :autoLoading="false" style="margin-top: 20px;" v-loading="loading"></Chart>
-    </el-card>
-  </div>
+	<div class="page">
+		<el-card shadow="nover">
+			<el-form inline>
+				<el-form-item label="选择产品:" prop="productKey">
+					<el-select v-model="params.productKey" filterable placeholder="请选择产品" @change="productChange">
+						<el-option v-for="item in productList" :key="item.key" :label="item.name" :value="item.key">
+							<span style="float: left">{{ item.name }}</span>
+							<span style="float: right; font-size: 13px">{{ item.key }}</span>
+						</el-option>
+					</el-select>
+				</el-form-item>
+
+				<el-form-item label="选择设备:" prop="deviceKey">
+					<el-select v-model="params.deviceKey" filterable placeholder="请选择设备">
+						<el-option v-for="item in deviceList" :key="item.key" :label="item.name" :value="item.key">
+							<span style="float: left">{{ item.name }}</span>
+							<span style="float: right; font-size: 13px; margin-left: 12px">{{ item.key }}</span>
+						</el-option>
+					</el-select>
+				</el-form-item>
+
+				<el-form-item label="选择属性:" prop="properties">
+					<el-select v-model="params.properties" filterable placeholder="请选择属性" @change="propertyChange">
+						<el-option v-for="item in propertyList" :key="item.key" :label="item.name" :value="item.key">
+							<span style="float: left">{{ item.name }}</span>
+							<span style="float: right; font-size: 13px; margin-left: 12px">{{ item.key }}</span>
+						</el-option>
+					</el-select>
+				</el-form-item>
+				<el-form-item label="选择时间:" prop="dateRange">
+					<el-date-picker
+						v-model="dateRange"
+						style="width: 360px"
+						value-format="YYYY-MM-DD HH:mm:ss"
+						date-format="YYYY-MM-DD"
+						time-format="HH:mm"
+						format="YYYY-MM-DD HH:mm:ss"
+						type="datetimerange"
+						range-separator="-"
+						start-placeholder="开始日期"
+						end-placeholder="结束日期"
+						:clearable="false"
+					></el-date-picker>
+				</el-form-item>
+				<el-form-item label="聚合粒度:" prop="dateType">
+					<el-radio-group v-model="params.dateType" @change="dateTypeChange">
+						<el-radio-button label="1">5分钟</el-radio-button>
+						<el-radio-button label="2">1小时</el-radio-button>
+						<el-radio-button label="3">1天</el-radio-button>
+					</el-radio-group>
+				</el-form-item>
+				<el-form-item>
+					<el-button type="primary" class="ml10" @click="getData">
+						<el-icon>
+							<ele-Search />
+						</el-icon>
+						查询
+					</el-button>
+					<el-button type="primary" @click="handlePrintChart">
+						<el-icon>
+							<ele-Printer />
+						</el-icon>
+						打印图表
+					</el-button>
+				</el-form-item>
+			</el-form>
+			<div class="title">
+				<el-icon style="margin-right: 5px">
+					<ele-Histogram />
+				</el-icon>
+				指标聚合统计图
+			</div>
+			<Chart class="flex1" height="12vw" ref="chart" :autoLoading="false" style="margin-top: 20px" v-loading="loading"></Chart>
+		</el-card>
+	</div>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue';
-import { ElMessage } from 'element-plus';
-import api from '/@/api/device';
-import dayjs from 'dayjs';
+import { ref, reactive, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import api from '/@/api/device'
+import dayjs from 'dayjs'
 import Chart from '/@/components/chart/index.vue'
 import { getLineOption } from '/@/components/chart/options'
-import { printChart } from '/@/utils/print';
+import { printChart } from '/@/utils/print'
 
 const productList = ref<any[]>([])
 const deviceList = ref<any[]>([])
@@ -82,94 +93,103 @@ const loading = ref(false)
 const propertyName = ref('')
 
 // 聚合力度可选值为"5分钟"、"1小时"和"1天",对应的最大时间范围为"一周"、"一个月"和"一年"。
+const dateRange = ref([dayjs().format('YYYY-MM-DD 00:00:00'), dayjs().format('YYYY-MM-DD HH:mm:00')])
 
 const params = reactive({
-  productKey: '',
-  deviceKey: '',
-  properties: '',
-  dateType: '1',
-  dateRange: [dayjs().format('YYYY-MM-DD 00:00:00'), dayjs().format('YYYY-MM-DD HH:mm:00')]
+	productKey: '',
+	deviceKey: '',
+	properties: '',
+	dateType: '1',
+	startDate: dateRange.value[0],
+	endDate: dateRange.value[1],
+})
+
+watch(dateRange, () => {
+	params.startDate = dateRange.value[0]
+	params.endDate = dateRange.value[1]
 })
 
 function dateTypeChange(dateType: string) {
-  switch (dateType) {
-    case '1':
-      params.dateRange = [dayjs().format('YYYY-MM-DD 00:00:00'), dayjs().format('YYYY-MM-DD HH:mm:00')]
-      break;
-    case '2':
-      params.dateRange = [dayjs().subtract(7, 'days').format('YYYY-MM-DD 00:00:00'), dayjs().format('YYYY-MM-DD HH:mm:00')]
-      break;
-    case '3':
-      params.dateRange = [dayjs().subtract(30, 'days').format('YYYY-MM-DD 00:00:00'), dayjs().format('YYYY-MM-DD HH:mm:00')]
-      break;
-
-    default:
-      break;
-  }
-
-  getData()
+	switch (dateType) {
+		case '1':
+			dateRange.value = [dayjs().format('YYYY-MM-DD 00:00:00'), dayjs().format('YYYY-MM-DD HH:mm:00')]
+			break
+		case '2':
+			dateRange.value = [dayjs().subtract(7, 'days').format('YYYY-MM-DD 00:00:00'), dayjs().format('YYYY-MM-DD HH:mm:00')]
+			break
+		case '3':
+			dateRange.value = [dayjs().subtract(30, 'days').format('YYYY-MM-DD 00:00:00'), dayjs().format('YYYY-MM-DD HH:mm:00')]
+			break
+		default:
+			break
+	}
+
+	getData()
 }
 
 api.product.getLists({ status: 1 }).then((res: any) => {
-  productList.value = res.product || [];
-});
+	productList.value = res.product || []
+})
 
 function getData() {
-  const diffDay = dayjs(params.dateRange[1], 'YYYY-MM-DD HH:mm:ss').diff(dayjs(params.dateRange[0], 'YYYY-MM-DD HH:mm:ss'), 'days')
-  if (params.dateType === '1' && diffDay > 7) return ElMessage('时间范围不能超过7天')
-  if (params.dateType === '2' && diffDay > 31) return ElMessage('时间范围不能超过1个月')
-  if (params.dateType === '3' && diffDay > 366) return ElMessage('时间范围不能超过一年')
-
-  if (!params.productKey) return ElMessage('请选选择产品')
-  if (!params.deviceKey) return ElMessage('请选选择设备')
-  if (!params.properties) return ElMessage('请选选择属性')
-
-  loading.value = true
-  api.analysis.deviceIndicatorPolymerize(params).then((data: any[]) => {
-    const res = data || []
-    chart.value.draw(
-      getLineOption({
-        datas: [res.map(item => item.dataAverageValue), res.map(item => item.dataMaxValue), res.map(item => item.dataMinValue)],
-        xAxis: res.map(item => item.date),
-        legend: [propertyName.value + '平均值', propertyName.value + '最大值', propertyName.value + '最小值']
-      })
-    )
-  }).finally(() => loading.value = false)
+	const diffDay = dayjs(dateRange.value[1], 'YYYY-MM-DD HH:mm:ss').diff(dayjs(dateRange.value[0], 'YYYY-MM-DD HH:mm:ss'), 'days')
+	if (params.dateType === '1' && diffDay > 7) return ElMessage('时间范围不能超过7天')
+	if (params.dateType === '2' && diffDay > 31) return ElMessage('时间范围不能超过1个月')
+	if (params.dateType === '3' && diffDay > 366) return ElMessage('时间范围不能超过一年')
+
+	if (!params.productKey) return ElMessage('请选选择产品')
+	if (!params.deviceKey) return ElMessage('请选选择设备')
+	if (!params.properties) return ElMessage('请选选择属性')
+
+	loading.value = true
+	api.analysis
+		.deviceIndicatorPolymerize(params)
+		.then((data: any[]) => {
+			const res = data || []
+			chart.value.draw(
+				getLineOption({
+					datas: [res.map((item) => item.dataAverageValue), res.map((item) => item.dataMaxValue), res.map((item) => item.dataMinValue)],
+					xAxis: res.map((item) => item.date),
+					legend: [propertyName.value + '平均值', propertyName.value + '最大值', propertyName.value + '最小值'],
+				})
+			)
+		})
+		.finally(() => (loading.value = false))
 }
 
 function productChange(productKey: string) {
-  params.deviceKey = ''
-  params.properties = ''
-  deviceList.value = []
-  propertyList.value = []
-  api.device.allList({ productKey }).then((res: any) => {
-    deviceList.value = res.device;
-  });
-  api.product.getpropertyList({ productKey }).then((res: any) => {
-    propertyList.value = res;
-  });
+	params.deviceKey = ''
+	params.properties = ''
+	deviceList.value = []
+	propertyList.value = []
+	api.device.allList({ productKey }).then((res: any) => {
+		deviceList.value = res.device
+	})
+	api.product.getpropertyList({ productKey }).then((res: any) => {
+		propertyList.value = res
+	})
 }
 
 function propertyChange(property: string) {
-  propertyName.value = propertyList.value.find(item => item.key === property)?.name
+	propertyName.value = propertyList.value.find((item) => item.key === property)?.name
 }
 
 // 打印图表
 const handlePrintChart = async () => {
-  if (!chart.value) {
-    ElMessage.warning('图表未加载');
-    return;
-  }
-  await printChart(chart.value.$el);
-};
+	if (!chart.value) {
+		ElMessage.warning('图表未加载')
+		return
+	}
+	await printChart(chart.value.$el)
+}
 </script>
 <style scoped>
 .title {
-  font-size: 14px;
-  font-weight: bold;
-  border-bottom: 1px solid #eee;
-  display: flex;
-  align-items: center;
-  padding-bottom: 6px;
+	font-size: 14px;
+	font-weight: bold;
+	border-bottom: 1px solid #eee;
+	display: flex;
+	align-items: center;
+	padding-bottom: 6px;
 }
-</style>
+</style>

+ 7 - 18
src/views/iot/property/dossier/edit.vue

@@ -42,10 +42,10 @@
 
       <el-row>
         <el-col :span="12">
-          <el-form-item label="所属区域" prop="area">
-            <el-cascader :options="orgData" :props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'name' }" placeholder="请选择区域" clearable class="w100" v-model="formData.area">
+          <el-form-item label="所属部门" prop="deptId">
+            <el-cascader :options="deptData" :props="{ checkStrictly: true, emitPath: false, value: 'deptId', label: 'deptName' }" placeholder="请选择所属部门" clearable class="w100" v-model="formData.deptId">
               <template #default="{ node, data }">
-                <span>{{ data.name }}</span>
+                <span>{{ data.deptName }}</span>
                 <span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
               </template>
             </el-cascader>
@@ -53,27 +53,16 @@
         </el-col>
 
         <el-col :span="12">
-          <el-form-item label="安装时间">
-            <el-date-picker v-model="formData.installTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择时间" class="w100" clearable />
+          <el-form-item label="设备类型">
+            <el-input v-model.trim="formData.deviceCategory" placeholder="请输入设备类型" />
           </el-form-item>
         </el-col>
       </el-row>
 
       <el-row>
         <el-col :span="12">
-          <el-form-item label="所属部门" prop="deptId">
-            <el-cascader :options="deptData" :props="{ checkStrictly: true, emitPath: false, value: 'deptId', label: 'deptName' }" placeholder="请选择所属部门" clearable class="w100" v-model="formData.deptId">
-              <template #default="{ node, data }">
-                <span>{{ data.deptName }}</span>
-                <span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
-              </template>
-            </el-cascader>
-          </el-form-item>
-        </el-col>
-
-        <el-col :span="12">
-          <el-form-item label="设备类型">
-            <el-input v-model.trim="formData.deviceCategory" placeholder="请输入设备类型" />
+          <el-form-item label="安装时间">
+            <el-date-picker v-model="formData.installTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择时间" class="w100" clearable />
           </el-form-item>
         </el-col>
       </el-row>

+ 13 - 4
src/views/system/datahub/modeling/component/editNode.vue

@@ -42,11 +42,11 @@
 					</el-select>
 				</el-form-item>
 
-				<!-- <el-form-item label="取值方式" prop="method" v-if="ruleForm.dataType=='int' || ruleForm.dataType=='bigint' || ruleForm.dataType=='float' || ruleForm.dataType=='double'">
+				<el-form-item label="取值方式" prop="method" v-if="ruleForm.from == 2 && (ruleForm.dataType=='int' || ruleForm.dataType=='bigint' || ruleForm.dataType=='float' || ruleForm.dataType=='double')">
 					<el-select v-model="ruleForm.method"  placeholder="请选择数据类型">
 						<el-option v-for="item in methodData" :key="item.value" :label="item.label" :value="item.value" />
 					</el-select>
-				</el-form-item> -->
+				</el-form-item>
 
 
 				<el-form-item label="默认值" prop="default" v-if="ruleForm.from == 1">
@@ -103,6 +103,7 @@ interface RuleFormState {
 	nodeId: number;
 	key: string;
 	dataType: string;
+	method: string;
 	default: string;
 	isSorting: number;
 	isDesc: number;
@@ -121,12 +122,14 @@ export default defineComponent({
 	components: { Delete, Minus, Right },
 
 	setup(prop, { emit }) {
-		const editDicRef = ref();
 		const formRef = ref<HTMLElement | null>(null);
 		const state = reactive<DicState>({
 			isShowDialog: false,
 			methodData: [
 				{
+					label: '默认',
+					value: '',
+				}, {
 					label: 'max',
 					value: 'max',
 				}, {
@@ -135,6 +138,9 @@ export default defineComponent({
 				}, {
 					label: 'avg',
 					value: 'avg',
+				}, {
+					label: 'sum',
+					value: 'sum',
 				}
 			],
 			tabData: [
@@ -176,6 +182,8 @@ export default defineComponent({
 				sourceId: 0,
 				nodeId: 0,
 				name: '',
+				dataType: '',
+				method: '',
 				key: '',
 				from: 1,
 				isSorting: 0,
@@ -189,7 +197,6 @@ export default defineComponent({
 				name: [{ required: true, message: '字段节点名称不能为空', trigger: 'blur' }],
 				dataType: [{ required: true, message: '数据类型不能为空', trigger: 'blur' }],
 				value: [{ required: true, message: '字段节点取值项不能为空', trigger: 'blur' }],
-				method: [{ required: true, message: '请选择取值方式', trigger: 'blur' }],
 				isSorting: [{ required: true, message: '请选择是否参与排序', trigger: 'blur' }],
 				isDesc: [{ required: true, message: '请选择排序方式', trigger: 'blur' }],
 			},
@@ -249,6 +256,8 @@ export default defineComponent({
 				sourceId: 0,
 				nodeId: 0,
 				key: '',
+				dataType: '',
+				method: '',
 				from: 1,
 				isSorting: 0,
 				isDesc: 0,

+ 125 - 110
src/views/system/datahub/source/component/edit.vue

@@ -1,5 +1,12 @@
 <template>
-	<el-dialog class="api-edit" v-model="showDialog" :title="`${formData.sourceId ? '编辑数据源' : '新增数据源'}`" width="800px" :close-on-click-modal="false" :close-on-press-escape="false">
+	<el-dialog
+		class="api-edit"
+		v-model="showDialog"
+		:title="`${formData.sourceId ? '编辑数据源' : '新增数据源'}`"
+		width="800px"
+		:close-on-click-modal="false"
+		:close-on-press-escape="false"
+	>
 		<el-form class="inline-form" ref="formRef" :model="formData" :rules="ruleForm" label-width="120px">
 			<el-form-item label="数据源标识" prop="key">
 				<el-input v-model="formData.key" placeholder="请输入数据源名称" :disabled="formData.sourceId" />
@@ -14,15 +21,13 @@
 			<el-form-item label="数据来源" prop="from" v-if="!formData.sourceId">
 				<el-radio-group v-model="formData.from">
 					<el-radio :label="1">api导入</el-radio>
-					<el-radio :label="4">设备</el-radio>
+					<el-radio :label="4">设备或产品</el-radio>
 					<el-radio :label="2">数据库</el-radio>
 				</el-radio-group>
 			</el-form-item>
 			<el-divider content-position="left">数据源配置</el-divider>
 
-
 			<div v-if="formData.from == 1">
-
 				<el-form-item label="请求方法" prop="config.method">
 					<el-select v-model="formData.config.method" :rules="ruleForm['config.method']" placeholder="请选择请求方法">
 						<el-option v-for="item in methodData" :key="item.value" :label="item.label" :value="item.value" />
@@ -34,16 +39,19 @@
 				</el-form-item>
 
 				<el-form-item label="定时请求" prop="config.cronExpression">
-					<div style="display:flex">
+					<div style="display: flex">
 						<el-input v-model="formData.config.cronExpression" placeholder="请输入cron表达式" :rules="ruleForm['config.cronExpression']" />
-						<el-button type="success" @click="showCron('config')" style="    margin-left: 5px;">设置</el-button>
-
+						<el-button type="success" @click="showCron('config')" style="margin-left: 5px">设置</el-button>
 					</div>
 				</el-form-item>
 
 				<div class="box-content">
 					<div>
-						<div v-for="(item, index) in requestParams" :key="index" style="padding: 10px; border: 1px solid #eee; margin-bottom: 10px; position: relative">
+						<div
+							v-for="(item, index) in requestParams"
+							:key="index"
+							style="padding: 10px; border: 1px solid #eee; margin-bottom: 10px; position: relative"
+						>
 							<div class="conicon" style="width: 100%; text-align: right; position: absolute; right: -8px; top: -8px; color: red">
 								<el-icon @click="delParams(index)">
 									<CircleClose />
@@ -74,9 +82,35 @@
 				<el-button type="success" class="addbutton" @click="addParamss">增加分组</el-button>
 			</div>
 			<div v-if="formData.from == 4">
-				<el-form-item label="选择设备" prop="devconfig.deviceKey">
-					<el-select v-model="formData.devconfig.deviceKey" :rules="ruleForm['devconfig.deviceKey']" filterable placeholder="请选择设备" @change="setNode">
-						<el-option v-for="item in sourceData" :key="item.key" :label="item.name" :value="item.id">
+				<el-form-item label="设备或产品">
+					<el-radio-group v-model="deviceOrProduct" @change=";(formData.devconfig.productKey = ''), (formData.devconfig.deviceKey = '')">
+						<el-radio label="device">设备</el-radio>
+						<el-radio label="product">产品</el-radio>
+					</el-radio-group>
+				</el-form-item>
+				<el-form-item label="选择设备" v-if="deviceOrProduct == 'device'" prop="devconfig.deviceKey">
+					<el-select
+						v-model="formData.devconfig.deviceKey"
+						:rules="ruleForm['devconfig.deviceKey']"
+						filterable
+						placeholder="请选择设备"
+						@change="formData.devconfig.productKey = ''"
+					>
+						<el-option v-for="item in sourceData" :key="item.key" :label="item.name" :value="item.key">
+							<span style="float: left">{{ item.name }}</span>
+							<span style="float: right; font-size: 13px">{{ item.key }}</span>
+						</el-option>
+					</el-select>
+				</el-form-item>
+				<el-form-item label="选择产品" v-if="deviceOrProduct == 'product'" prop="devconfig.productKey">
+					<el-select
+						v-model="formData.devconfig.productKey"
+						:rules="ruleForm['devconfig.productKey']"
+						filterable
+						placeholder="请选择产品"
+						@change="formData.devconfig.deviceKey = ''"
+					>
+						<el-option v-for="item in producList" :key="item.key" :label="item.name" :value="item.key">
 							<span style="float: left">{{ item.name }}</span>
 							<span style="float: right; font-size: 13px">{{ item.key }}</span>
 						</el-option>
@@ -84,15 +118,11 @@
 				</el-form-item>
 			</div>
 
-
-
 			<div v-if="formData.from == 2">
-
 				<el-form-item label="数据来源" prop="tabconfig.type">
 					<el-radio-group v-model="formData.tabconfig.type" :rules="ruleForm['tabconfig.type']">
 						<el-radio label="mysql">mysql</el-radio>
 						<el-radio label="mssql">mssql</el-radio>
-
 					</el-radio-group>
 				</el-form-item>
 
@@ -123,12 +153,15 @@
 					<el-radio-group v-model="formData.tabconfig.queryType" :rules="ruleForm['tabconfig.queryType']">
 						<el-radio label="tableName">数据表</el-radio>
 						<el-radio label="sql">Sql</el-radio>
-
 					</el-radio-group>
 				</el-form-item>
 
 				<el-form-item label="" prop="tabconfig.tableName">
-					<el-input v-model="formData.tabconfig.tableName" type="textarea" :placeholder="formData.tabconfig.queryType == 'sql' ? '请输入sql语句' : '请输入表名称'" />
+					<el-input
+						v-model="formData.tabconfig.tableName"
+						type="textarea"
+						:placeholder="formData.tabconfig.queryType == 'sql' ? '请输入sql语句' : '请输入表名称'"
+					/>
 				</el-form-item>
 
 				<el-form-item label="主键字段" prop="tabconfig.pk">
@@ -140,16 +173,12 @@
 				</el-form-item>
 
 				<el-form-item label="任务表达式" prop="tabconfig.cronExpression">
-
-					<div style="display:flex">
+					<div style="display: flex">
 						<el-input v-model="formData.tabconfig.cronExpression" placeholder="请输入cron任务表达式" :rules="ruleForm['tabconfig.cronExpression']" />
-						<el-button type="success" @click="showCron('tabconfig')" style="    margin-left: 5px;">设置</el-button>
-
+						<el-button type="success" @click="showCron('tabconfig')" style="margin-left: 5px">设置</el-button>
 					</div>
-
 				</el-form-item>
 			</div>
-
 		</el-form>
 
 		<template #footer>
@@ -178,24 +207,27 @@
 
 <script lang="ts" setup>
 import { ref, reactive, nextTick } from 'vue'
-import api from '/@/api/datahub';
+import api from '/@/api/datahub'
+import deviceApi from '/@/api/device'
 import { ruleRequired } from '/@/utils/validator'
-import 'vue3-json-viewer/dist/index.css';
-import vue3cron from '/@/components/vue3cron/vue3cron.vue';
+import 'vue3-json-viewer/dist/index.css'
+import vue3cron from '/@/components/vue3cron/vue3cron.vue'
 
 import { ElMessage } from 'element-plus'
-import { Delete, CircleClose, } from '@element-plus/icons-vue';
+import { Delete, CircleClose } from '@element-plus/icons-vue'
 
 const emit = defineEmits(['typeList'])
 
 const showDialog = ref(false)
+const deviceOrProduct = ref('device')
 const dialogVisible = ref(false)
 const cronShow = ref(false)
 const formRef = ref()
 const jsonData = ref()
 const crontype = ref()
-const sourceData = ref([]);
-const sourceId = ref();
+const sourceData = ref([])
+const producList = ref([])
+const sourceId = ref()
 const methodData = ref([
 	{
 		label: 'GET',
@@ -205,7 +237,7 @@ const methodData = ref([
 		label: 'POST',
 		value: 'post',
 	},
-]);
+])
 
 const paramData = ref([
 	{
@@ -220,7 +252,7 @@ const paramData = ref([
 		lable: 'param',
 		value: 'param',
 	},
-]);
+])
 
 const requestParams = ref([
 	[
@@ -231,17 +263,15 @@ const requestParams = ref([
 			value: '',
 		},
 	],
-]);
-const config = ref({
-	method: '',
-})
+])
+
 const delParams = (index: number) => {
-	requestParams.value.splice(index, 1);
-};
+	requestParams.value.splice(index, 1)
+}
 
 const delParamss = (index: number, bbb: number) => {
-	requestParams.value[index].splice(bbb, 1);
-};
+	requestParams.value[index].splice(bbb, 1)
+}
 
 const addParamss = () => {
 	requestParams.value.push([
@@ -251,53 +281,44 @@ const addParamss = () => {
 			name: '',
 			value: '',
 		},
-	]);
-};
+	])
+}
 const addParams = (index: number) => {
 	requestParams.value[index].push({
 		type: '',
 		key: '',
 		name: '',
 		value: '',
-	});
-};
+	})
+}
 const handlelisten = (e: any) => {
 	if (e.type == 'config') {
 		formData.config.cronExpression = e.cron
 	} else if (e.type == 'tabconfig') {
 		formData.tabconfig.cronExpression = e.cron
 	}
-};
+}
 const showCron = (type: string) => {
 	crontype.value = type
-	cronShow.value = true;
-
-};
+	cronShow.value = true
+}
 const cronclose = () => {
-	cronShow.value = false;
+	cronShow.value = false
 }
 const onTest = () => {
 	if (formData.from == 1) {
 		api.common.api(sourceId.value).then((res: any) => {
-			jsonData.value = JSON.parse(res.data);
-			dialogVisible.value = true;
-		});
+			jsonData.value = JSON.parse(res.data)
+			dialogVisible.value = true
+		})
 	} else if (formData.from == 4) {
 		api.common.devapi(sourceId.value).then((res: any) => {
-			jsonData.value = JSON.parse(res.data);
-			dialogVisible.value = true;
-		});
+			jsonData.value = JSON.parse(res.data)
+			dialogVisible.value = true
+		})
 	}
-};
+}
 
-const setNode = (event:any) => {
-	sourceData.value.forEach((item:any) => {
-		if (item.id == event) {
-			formData.devconfig.productKey = item.product.key;
-			formData.devconfig.deviceKey = item.key;
-		}
-	});
-};
 const baseForm = {
 	sourceId: undefined,
 	name: '',
@@ -312,7 +333,7 @@ const baseForm = {
 	},
 	devconfig: {
 		deviceKey: '',
-		productKey:'',
+		productKey: '',
 	},
 	tabconfig: {
 		type: 'mysql',
@@ -326,16 +347,13 @@ const baseForm = {
 		pk: '',
 		num: '',
 		cronExpression: '',
-	}
+	},
 }
-const onKeyclick = () => {
-
-};
+const onKeyclick = () => {}
 const formData = reactive({
 	...baseForm,
 })
 
-
 const ruleForm = {
 	key: [ruleRequired('数据源标识不能为空')],
 	name: [ruleRequired('数据源名称不能为空')],
@@ -346,17 +364,18 @@ const ruleForm = {
 		{
 			validator: (rule: any, value: string, callback: any) => {
 				if (formData.from === 1 && !formData.config.method) {
-					callback(new Error('请求方法不能为空'));
+					callback(new Error('请求方法不能为空'))
 				} else {
-					callback();
+					callback()
 				}
 			},
-			trigger: 'change'
-		}
+			trigger: 'change',
+		},
 	],
 	'config.url': [ruleRequired('请求地址不能为空')],
 	'config.cronExpression': [ruleRequired('定时请求不能为空')],
 	'devconfig.deviceKey': [ruleRequired('请选择设备')],
+	'devconfig.productKey': [ruleRequired('请选择产品')],
 	'tabconfig.type': [ruleRequired('请选择数据来源')],
 	'tabconfig.host': [ruleRequired('请输入主机地址')],
 	'tabconfig.port': [ruleRequired('请输入端口号')],
@@ -368,53 +387,52 @@ const ruleForm = {
 	'tabconfig.pk': [ruleRequired('请输入主键字段')],
 	'tabconfig.num': [ruleRequired('请输入每次获取数量')],
 	'tabconfig.cronExpression': [ruleRequired('请输入cron任务表达式')],
-
 }
 const getDevData = () => {
 	api.common.getdevList({}).then((res: any) => {
-		sourceData.value = res.device;
-	});
-};
+		sourceData.value = res.device
+	})
+	deviceApi.product.getLists().then((res: any) => {
+		producList.value = res.product
+	})
+}
 
 const onSubmit = async () => {
 	await formRef.value.validate()
-	
-
 
 	if (formData.from == 1) {
 		let form = {
-			sourceId: sourceId.value? sourceId.value:'',
+			sourceId: sourceId.value ? sourceId.value : '',
 			key: formData.key,
 			name: formData.name,
 			desc: formData.desc,
 			from: formData.from,
 			config: {
 				...formData.config,
-				requestParams: requestParams.value
+				requestParams: requestParams.value,
 			},
-		};
+		}
 		const theApi = sourceId.value ? api.common.edit : api.common.add
 		await theApi(form)
 		ElMessage.success('操作成功')
 		resetForm()
 		showDialog.value = false
 		emit('typeList')
-
 	} else if (formData.from == 4) {
 		let form = {
-						sourceId: formData.sourceId ? formData.sourceId : '',
-						key: formData.key,
-						name: formData.name,
-						desc: formData.desc,
-						from: formData.from,
-						config: formData.devconfig
-			};
-				const theApi = formData.sourceId ? api.common.devedit : api.common.devadd
-				await theApi(form)
-				ElMessage.success('操作成功')
-				resetForm()
-				showDialog.value = false
-				emit('typeList')
+			sourceId: formData.sourceId ? formData.sourceId : '',
+			key: formData.key,
+			name: formData.name,
+			desc: formData.desc,
+			from: formData.from,
+			config: formData.devconfig,
+		}
+		const theApi = formData.sourceId ? api.common.devedit : api.common.devadd
+		await theApi(form)
+		ElMessage.success('操作成功')
+		resetForm()
+		showDialog.value = false
+		emit('typeList')
 	} else if (formData.from == 2) {
 		let form = {
 			sourceId: formData.sourceId ? formData.sourceId : '',
@@ -422,8 +440,8 @@ const onSubmit = async () => {
 			name: formData.name,
 			desc: formData.desc,
 			from: formData.from,
-			config: formData.tabconfig
-		};
+			config: formData.tabconfig,
+		}
 		const theApi = formData.sourceId ? api.common.dbedit : api.common.dbadd
 		await theApi(form)
 		ElMessage.success('操作成功')
@@ -446,7 +464,6 @@ const resetForm = async () => {
 			},
 		],
 	]
-
 }
 
 const openDialog = async (row: any) => {
@@ -455,23 +472,21 @@ const openDialog = async (row: any) => {
 
 	nextTick(() => {
 		if (row) {
-
-			sourceId.value = row.sourceId;
+			sourceId.value = row.sourceId
 			api.common.detail(row.sourceId).then((res: any) => {
 				Object.assign(formData, { ...res.data })
 				if (res.data.from == 1) {
-					formData.config = res.data.apiConfig;
-					requestParams.value = res.data.apiConfig.requestParams;
+					formData.config = res.data.apiConfig
+					requestParams.value = res.data.apiConfig.requestParams
 				} else if (res.data.from == 4) {
-					formData.devconfig = res.data.deviceConfig;
+					deviceOrProduct.value = res.data.deviceConfig.deviceKey ? 'device' : 'product'
+					formData.devconfig = res.data.deviceConfig
 				} else if (res.data.from == 2) {
-					formData.tabconfig = res.data.dbConfig;
+					formData.tabconfig = res.data.dbConfig
 				}
-
-			});
+			})
 		}
-		getDevData();
-
+		getDevData()
 	})
 }
 
@@ -521,4 +536,4 @@ defineExpose({ openDialog })
 .jv-node {
 	margin-left: 25px;
 }
-</style>
+</style>

+ 8 - 8
yarn.lock

@@ -1096,7 +1096,7 @@ balanced-match@^1.0.0:
 
 base64-arraybuffer@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
+  resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
   integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
 
 batch-processor@1.0.0:
@@ -1347,7 +1347,7 @@ cross-spawn@^7.0.2:
 
 css-line-break@^2.1.0:
   version "2.1.0"
-  resolved "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
+  resolved "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
   integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
   dependencies:
     utrie "^1.0.2"
@@ -2274,7 +2274,7 @@ highlight.js@^11.8.0:
 
 html2canvas@^1.4.1:
   version "1.4.1"
-  resolved "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
+  resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
   integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
   dependencies:
     css-line-break "^2.1.0"
@@ -3214,7 +3214,7 @@ tape@^4.5.1:
 
 text-segmentation@^1.0.3:
   version "1.0.3"
-  resolved "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
+  resolved "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
   integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
   dependencies:
     utrie "^1.0.2"
@@ -3373,7 +3373,7 @@ uri-js@^4.2.2:
 
 utrie@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
+  resolved "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
   integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
   dependencies:
     base64-arraybuffer "^1.0.2"
@@ -3422,9 +3422,9 @@ vue-clipboard3@1.0.1:
     clipboard "^2.0.6"
 
 vue-data-ui@^2.4.17:
-  version "2.6.9"
-  resolved "https://registry.npmmirror.com/vue-data-ui/-/vue-data-ui-2.6.9.tgz#08c90093f98beb4a1f57a8336068f3d9a95e0cb5"
-  integrity sha512-q3lci39zyfKwpfK6ObnsZwTL0fu0hA0DrTVHZXhmwhS4b7K+cLYxIZuNyAp35o2zeVKhZ7hbW9g9P9jrSIqeZg==
+  version "2.6.2"
+  resolved "https://registry.npmjs.org/vue-data-ui/-/vue-data-ui-2.6.2.tgz#960569237e0ccf65e797e7517756bc735ec4a075"
+  integrity sha512-YQxX04a8raB/BVk95HeQegtvTXrvmDMbaIDE52s5lEPNUArGHVIF6G5EUPuNuPRgZDtJJW6csZ/0zOYAaCafAA==
 
 vue-demi@*:
   version "0.14.10"

Деякі файли не було показано, через те що забагато файлів було змінено