Quellcode durchsuchen

wip: ice协议开发中

vera_min vor 1 Jahr
Ursprung
Commit
c57faf4bcb

+ 3 - 1
.env

@@ -23,4 +23,6 @@ VITE_SCREEN_URL = '/plugin/screen/'
 # 组态图前端
 VITE_TOPO_URL = '/plugin/topo/'
 # modbus服务
-VITE_MODBUS_API = '/base-api/modbus'
+VITE_MODBUS_API = '/base-api/modbus'
+# ice104协议网关服务
+VITE_ICE104_API = '/base-api/ice104'

+ 4 - 2
.env.development

@@ -1,2 +1,4 @@
-VITE_SERVER_PROTOCOL = 'http:'
-VITE_SERVER_HOSTNAME = '127.0.0.1:8200'
+# VITE_SERVER_PROTOCOL = 'http:'
+# VITE_SERVER_HOSTNAME = '127.0.0.1:8200'
+VITE_SERVER_PROTOCOL = 'https:'
+VITE_SERVER_HOSTNAME = 'zhgy.sagoo.cn'

+ 39 - 0
src/api/ice104/index.ts

@@ -0,0 +1,39 @@
+/*
+ * @Author: vera_min vera_min@163.com
+ * @Date: 2023-10-23 22:45:52
+ * @LastEditors: vera_min vera_min@163.com
+ * @LastEditTime: 2023-10-25 10:20:08
+ * @FilePath: /sagoo-admin-ui/src/api/ice104/index.ts
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
+import { get, post, del, put,file } from '/@/utils/request_ice104';
+
+export default {
+  common:{
+    singleImg: (data: object) => post('/common/singleImg', data),
+  },
+  device: {
+    getList: (params: object) => get('/device/list', params),
+    addItem: (data: object) => post('/device/add', data),
+    editItem: (data: object) => put('/device/edit', data),
+    deleteItem: (ids: number) => del('/device/delete', { ids }),
+  },
+  template: {
+    getList: (params: object) => get('/template/list', params),
+    addItem: (data: object) => post('/template/add', data),
+    editItem: (data: object) => put('/template/edit', data),
+    deleteItem: (ids: number) => del('/template/delete', { ids }),
+  },
+  deviceTask: {
+    getList: (params: object) => get('/device/job/list', params),
+    addItem: (data: object) => post('/device/job/add', data),
+    editItem: (data: object) => put('/device/job/edit', data),
+    deleteItem: (ids: number) => del('/device/job/delete', { ids }),
+  },
+  deviceTemplate: {
+    getList: (params: object) => get('/device/template/list', params),
+    addItem: (data: object) => post('/device/template/add', data),
+    editItem: (data: object) => put('/device/template/edit', data),
+    deleteItem: (ids: number) => del('/device/template/delete', { ids }),
+  }
+}

+ 63 - 0
src/hooks/useCommonIce104.ts

@@ -0,0 +1,63 @@
+/*
+ * @Author: vera_min vera_min@163.com
+ * @Date: 2023-10-24 11:43:07
+ * @LastEditors: vera_min vera_min@163.com
+ * @LastEditTime: 2023-10-25 08:56:08
+ * @FilePath: /sagoo-admin-ui/src/hooks/useCommonIce104.ts
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
+import { reactive, ref } from 'vue'
+
+export default function () {
+  const statusParams = reactive({
+    status: 1
+  })
+
+  return { statusParams }
+}
+
+export function useSearch<T>(api: any, resKey: string, expandParams?: any) {
+
+  //  <pagination v-if="params.total" :total="params.total" v-model:page="params.pageNum" v-model:limit="params.pageSize" @pagination="getList()" />
+
+  // import api from '/@/api/system';
+  // import { ApiRow } from '/@/api/model/system/menu';
+  // import { useSearch } from '/@/hooks/useCommon';
+
+  // const { params, tableData, getList } = useSearch<ApiRow[]>(api.api.getList, 'Info', { name: '', address: '' });
+  // getList() // 获取列表数据
+
+  interface SearchParams {
+    status: -1 | 0 | 1,
+    pageNum: number;
+    pageSize: number;
+    total: number;
+    [key: string]: any;
+  }
+
+  const params = reactive<SearchParams>({
+    status: -1,
+    pageNum: 1,
+    pageSize: 10,
+    total: 0,
+    ...expandParams
+  })
+
+  const loading = ref(false)
+
+  const tableData = ref<T[] | any[]>([])
+
+  const getList = async (pageNum?: number) => {
+    pageNum && (params.pageNum = pageNum);
+    tableData.value = [];
+    loading.value = true;
+    params.total = 0;
+    let res = await api(params).finally(() => loading.value = false)
+    console.log(res)
+    tableData.value = (resKey ? (res[resKey]) : (res)) || [];
+    console.log(tableData.value)
+    params.total = res.total;
+  };
+
+  return { params, tableData, getList, loading }
+}

+ 138 - 0
src/utils/request_ice104.ts

@@ -0,0 +1,138 @@
+import axios from 'axios';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import getOrigin from '/@/utils/origin'
+
+// 配置新建一个 axios 实例
+const service = axios.create({
+	// baseURL: getOrigin(import.meta.env.VITE_ICE104_API),
+	baseURL: 'http://6smbwc.natappfree.cc',
+	timeout: 50000,
+	headers: { 'Content-Type': 'application/json' },
+});
+
+// 添加请求拦截器
+service.interceptors.request.use(
+	(config) => {
+		// 在发送请求之前做些什么 token
+		if (localStorage.token) {
+			(<any>config.headers).common['Authorization'] = `Bearer ${localStorage.token}`;
+		}
+		return config;
+	},
+	(error) => {
+		// 对请求错误做些什么
+		return Promise.reject(error);
+	}
+);
+
+// 添加响应拦截器
+service.interceptors.response.use(
+	(response) => {
+		// 对响应数据做点什么
+		const res = response.data;
+		const code = response.data.code
+		if (code === 401) {
+			ElMessageBox.alert('登录状态已过期,请重新登录', '提示',
+				{ confirmButtonText: '确定', showCancelButton: false, closeOnHashChange: false, closeOnPressEscape: false, closeOnClickModal: false, showClose: false })
+				.then(() => {
+					localStorage.clear(); // 清除浏览器全部临时缓存
+					window.location.href = '/'; // 去登录页
+				})
+				.catch(() => { });
+		} else if (code === undefined && res.message === undefined) { // 可能是下载文件
+			return response
+		} else if (code !== 0) {
+			ElMessage.error(res.message)
+			return Promise.reject(new Error(res.message))
+		} else {
+			// 分页的数据
+			if (res.data?.Total !== undefined) {
+				return {
+					list: res.data.Data,
+					total: res.data.Total,
+					page: res.data.currentPage,
+					...res.data,
+				}
+			}
+			// if (res.data?.Data) {
+			// 	return res.data.Data
+			// }
+			if (res.data?.Info && res.data?.Data) { // currentUser接口
+				return res.data
+			}
+			if (res.data?.Data === undefined) {
+				return res.data
+			}
+			return res.data.Data
+		}
+	},
+	(error) => {
+		// 对响应错误做点什么
+		if (error.message.indexOf('timeout') != -1) {
+			ElMessage.error('网络超时');
+		} else if (error.message == 'Network Error') {
+			ElMessage.error('网络连接错误');
+		} else {
+			if (error.response.data) ElMessage.error(error.response.statusText);
+			else ElMessage.error('接口路径找不到');
+		}
+		return Promise.reject(error);
+	}
+);
+
+// 导出 axios 实例
+export default service;
+
+export function get(url: string, params?: any, config?: any): any {
+	return service({
+		url,
+		method: "get",
+		...config,
+		params
+	})
+}
+
+export function post(url: string, data?: any, config?: any): any {
+	return service({
+		url,
+		method: "post",
+		...config,
+		data
+	})
+}
+
+export function put(url: string, data?: any): any {
+	return service({
+		url,
+		method: "put",
+		data
+	})
+}
+export function del(url: string, data?: any): any {
+	return service({
+		url,
+		method: "delete",
+		data
+	})
+}
+
+
+export function file(url: string, params?: any, method: 'get' | 'post' = 'get'): any {
+	if (method === 'get') {
+		return service({
+			url,
+			method,
+			params,
+			timeout: 30000,
+			responseType: 'arraybuffer',
+		});
+	} else {
+		return service({
+			url,
+			method,
+			timeout: 100000,
+			data: params,
+			responseType: 'blob',
+		});
+	}
+}

+ 8 - 0
src/views/iot/device/channel/index.vue

@@ -1,3 +1,11 @@
+<!--
+ * @Author: vera_min vera_min@163.com
+ * @Date: 2023-10-23 09:09:35
+ * @LastEditors: vera_min vera_min@163.com
+ * @LastEditTime: 2023-10-23 17:46:43
+ * @FilePath: /sagoo-admin-ui/src/views/iot/device/channel/index.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
 <template>
 	<div class="page">
 		<el-card shadow="hover">

+ 268 - 0
src/views/iot/ice104/device/component/detail.vue

@@ -0,0 +1,268 @@
+<template>
+	<el-dialog title="设备通道详情" v-model="dialogVisible" width="900px" :before-close="clsoeDialog" :close-on-click-modal="false">
+		<el-tabs v-model="activeName">
+			<el-tab-pane label="通道信息" name="1">
+				<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="120px" style="width: 90%; margin: 0 auto">
+					<el-form-item label="通道名称" prop="title">
+						<el-input v-model="temp.title" placeholder="请输入通道名称" />
+					</el-form-item>
+					<el-form-item label="注册码" prop="number">
+						<el-input v-model="temp.number" placeholder="请输入注册码" />
+					</el-form-item>
+					<el-form-item label="设备地址" prop="slaveId">
+						<el-input v-model.number="temp.slaveId" placeholder="请输入设备地址" />
+					</el-form-item>
+					<!-- <el-form-item label="调度周期(秒)" prop="interval">
+            <el-input v-model="temp.interval" placeholder="请输入调度周期" />
+          </el-form-item> -->
+					<el-form-item label="" prop="">
+						<div align="right">
+							<el-button @click="clsoeDialog"> 取 消 </el-button>
+							<el-button type="primary" @click="updateData()"> 保 存 </el-button>
+						</div>
+					</el-form-item>
+				</el-form>
+			</el-tab-pane>
+			<el-tab-pane label="任务" name="2">
+				<div class="filter-container">
+					<el-button class="filter-item" type="primary" icon="el-icon-circle-plus-outline" @click="handleCreate"> 添加任务 </el-button>
+				</div>
+
+				<el-table :key="tableKey" v-loading="listLoading" :data="taskList" border fit highlight-current-row style="width: 100%">
+					<el-table-column label="标题" prop="Job.title" align="center"></el-table-column>
+					<el-table-column label="调度周期" prop="Job.interval" align="center"></el-table-column>
+					<el-table-column label="转发格式" prop="encoding" align="center">
+						<template #default="{ row }">
+							{{ getCodingLabel(row) }}
+						</template>
+					</el-table-column>
+					<el-table-column label="mqtt主题" prop="Job.publishTopic" align="center"></el-table-column>
+					<el-table-column label="模板" prop="Template.title" align="center"></el-table-column>
+					<el-table-column label="操作" align="center" width="200">
+						<template #default="{ row, $index }">
+							<el-button type="primary" size="mini" @click="handleUpdate(row)"> 详情 </el-button>
+							<el-button v-if="row.status != 'deleted'" size="mini" type="danger" @click="handleDelete(row, $index)"> 删除 </el-button>
+						</template>
+					</el-table-column>
+				</el-table>
+
+				<pagination v-if="total > 0" :total="total" v-model:page="listQuery.page" v-model:limit="listQuery.size" @pagination="getList()" />
+				<TaskDialog ref="taskDialog" :formatOptions="formatOptions" @finish="getList" />
+			</el-tab-pane>
+			<el-tab-pane label="通道码流" name="3">
+				<div>
+					<el-button :type="evsrc ? 'info' : 'primary'" @click="openEv()">开始</el-button>
+					<el-button :type="evsrc ? 'primary' : 'info'" :disabled="!evsrc" @click="closeEv()">停止</el-button>
+					<el-button type="defualt" @click="clearLog()">清空</el-button>
+					<el-button type="info" style="margin-left: 150px" @click="downloadLog()">下载报文</el-button>
+					<ul id="logContainer" ref="logContainer"></ul>
+				</div>
+			</el-tab-pane>
+		</el-tabs>
+	</el-dialog>
+</template>
+
+<script lang="ts">
+import { ElMessage } from 'element-plus';
+import api from '/@/api/device/modbus';
+import getOrigin from '/@/utils/origin';
+import TaskDialog from './taskDialog.vue';
+
+export default {
+	components: { TaskDialog },
+	data() {
+		return {
+			temp: {
+				title: '',
+				number: '',
+				templateNumber: '',
+				slaveId: '',
+				interval: '',
+			},
+			rules: {
+				title: [{ required: true, message: '请输入通道名称', trigger: 'blur' }],
+				number: [{ required: true, message: '请输入注册码', trigger: 'blur' }],
+				slaveId: [{ required: true, message: '请输入设备地址', trigger: 'blur' }],
+				templateNumber: [{ required: true, message: '请输入设备模板', trigger: 'change' }],
+			},
+			dialogVisible: false,
+			activeName: '1',
+			taskList: [],
+			listLoading: false,
+			total: 0,
+			tableKey: 0,
+			listQuery: {
+				page: 1,
+				size: 10,
+			},
+			evsrc: null as any,
+			count: 0,
+			templateOptions: [],
+			formatOptions: [],
+		};
+	},
+	watch: {
+		activeName: 'handleTabClick',
+	},
+
+	methods: {
+		open(row: any) {
+			this.temp = { ...row };
+			this.getDict();
+			this.dialogVisible = true;
+		},
+		downloadLog() {
+			window.open(getOrigin(import.meta.env.VITE_MODBUS_API) + '/debug/export_message?number=' + this.temp.number);
+		},
+		clsoeDialog() {
+			this.dialogVisible = false;
+			this.activeName = '1';
+			(this.$refs['dataForm'] as any).resetFields();
+			this.closeEv();
+		},
+		updateData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					const tempData = Object.assign({}, this.temp);
+					api.channel.editDevice(tempData).then(() => {
+						this.$emit('getList');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+		// tab 切换
+		handleTabClick() {
+			this.closeEv(false);
+			if (this.activeName === '2') {
+				// 任务
+				this.getList();
+			} else if (this.activeName === '3') {
+				// 码流
+				// this.initEv()
+			}
+		},
+		handleFilter() {
+			this.listQuery.page = 1;
+			this.getList();
+		},
+		// 获取字典数据
+		getDict() {
+			api.getDict({ code: 'forwardFormat' }).then((data: any) => {
+				this.formatOptions = data.list || [];
+			});
+		},
+		// 获取任务list
+		getList() {
+			this.listLoading = true;
+			api.task
+				.getList(this.listQuery)
+				.then((res: any) => {
+					this.taskList = res.list || [];
+					this.total = res.Total;
+				})
+				.finally(() => {
+					this.listLoading = false;
+				});
+		},
+		handleDelete(row: any) {
+			this.$confirm('是否确认删除任务名称为"' + row.Job.title + '"的数据项?', '警告', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning',
+			})
+				.then(function () {
+					return api.task.deleteDeviceJob({ number: row.Job.number });
+				})
+				.then(() => {
+					this.handleFilter();
+					ElMessage.success('删除成功!');
+				})
+				.catch(function () { });
+		},
+		handleCreate() {
+			(this.$refs.taskDialog as any).openDialog({
+				dialogStatus: 'create',
+				deviceNumber: this.temp.number,
+			});
+		},
+		handleUpdate(row: any) {
+			(this.$refs.taskDialog as any).openDialog({
+				dialogStatus: 'update',
+				row,
+				deviceNumber: this.temp.number,
+			});
+		},
+		// 码流
+		initEv() {
+			if (this.evsrc) return;
+			// this.evsrc = new EventSource(`http://one.server.mydig.net/debug?number=${this.temp.number}`);
+			this.evsrc = new EventSource(`${getOrigin(import.meta.env.VITE_MODBUS_API)}/debug?number=${this.temp.number}`);
+			let that = this;
+			this.evsrc.onmessage = function (ev: any) {
+				try {
+					let obj = JSON.parse(ev.data);
+					if (obj.deviceId === that.temp.number) {
+						let color = obj.type === 'request' ? '#F56C6C' : '#409EFF';
+						let content = `${obj.type === 'request' ? '请求:' : '应答:'} ${obj.msg}`;
+						this.$refs.logContainer.insertAdjacentHTML('afterbegin', `<li style="color: ${color}">${content}</li>`);
+					}
+				} catch (_e) {
+					// @ts-ignore
+					// console.log('error', e);
+				}
+			};
+			// this.evsrc.onerror = function (_ev: any) {
+				// @ts-ignore
+				// console.log('readyState = ' + ev.currentTarget.readyState);
+			// };
+		},
+		openEv() {
+			(this.$refs.logContainer as any).insertAdjacentHTML('beforeEnd', `<li style="color: #000;">开始...</li>`);
+			this.initEv();
+		},
+		closeEv(log = true) {
+			this.evsrc && this.evsrc.close();
+			this.evsrc = null;
+			this.count = 0;
+			// this.clearLog()
+			if (log) {
+				(this.$refs.logContainer as any).insertAdjacentHTML('beforeEnd', `<li style="color: #000;">停止...</li>`);
+			}
+		},
+		// 清空码流
+		clearLog() {
+			(this.$refs.logContainer as any).innerHTML = '';
+		},
+		getCodingLabel(row: any) {
+			const item = this.formatOptions.find((item: any) => item.value === row.Job.encoding) as any;
+			return item.title;
+		},
+	},
+};
+</script>
+
+<style lang="scss" scoped>
+.filter-container {
+	margin-bottom: 10px;
+}
+
+.filter-item {
+	margin-right: 10px;
+}
+
+#logContainer {
+	height: 400px;
+	overflow: auto;
+	border: 1px solid #ddd;
+	padding: 10px;
+	padding-left: 20px;
+	border-radius: 8px;
+	margin-top: 10px;
+	word-wrap: break-word;
+	word-break: normal;
+	background-color: #f2f2f2;
+	line-height: 22px;
+}
+</style>

+ 80 - 0
src/views/iot/ice104/device/component/edit.vue

@@ -0,0 +1,80 @@
+<template>
+	<el-dialog title="添加设备通道" v-model="dialogVisible" width="600px" :before-close="clsoeDialog" :close-on-click-modal="false">
+		<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="120px" style="width: 90%; margin: 0 auto">
+			<el-form-item label="通道名称" prop="title">
+				<el-input v-model="temp.title" placeholder="请输入通道名称" />
+			</el-form-item>
+			<el-form-item label="注册码" prop="number">
+				<el-input v-model="temp.number" placeholder="请输入注册码" />
+			</el-form-item>
+			<el-form-item label="设备地址" prop="slaveId">
+				<el-input v-model.number="temp.slaveId" placeholder="请输入设备地址" />
+			</el-form-item>
+		</el-form>
+		<div slot="footer" class="dialog-footer">
+			<el-button @click="clsoeDialog()"> 取 消 </el-button>
+			<el-button type="primary" @click="createData()"> 保 存 </el-button>
+		</div>
+	</el-dialog>
+</template>
+<script lang="ts">
+import api from '/@/api/device/modbus';
+import { ElMessage } from 'element-plus';
+export default {
+	data() {
+		return {
+			temp: {
+				title: '',
+				number: '',
+				templateNumber: '',
+				slaveId: '',
+				interval: '',
+			},
+			rules: {
+				title: [{ required: true, message: '请输入通道名称', trigger: 'blur' }],
+				number: [{ required: true, message: '请输入注册码', trigger: 'blur' }],
+				slaveId: [{ required: true, message: '请输入设备地址', trigger: 'blur' }],
+			},
+			dialogVisible: false,
+			listLoading: false,
+			templateOptions: [],
+		};
+	},
+
+	methods: {
+		open() {
+			// this.getList();
+			this.dialogVisible = true;
+		},
+		clsoeDialog() {
+			(this.$refs.dataForm as any).resetFields();
+			this.dialogVisible = false;
+		},
+		// 获取模板数据
+		getList() {
+			this.listLoading = true;
+			api.channel
+				.getList({ page: 1, size: 50 })
+				.then((data: any) => {
+					this.templateOptions = data.list || [];
+				})
+				.finally(() => {
+					this.listLoading = false;
+				});
+		},
+		createData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					api.channel.addDevice(this.temp).then(() => {
+						this.$emit('getList');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+	},
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 139 - 0
src/views/iot/ice104/device/component/taskDialog.vue

@@ -0,0 +1,139 @@
+<template>
+	<el-dialog :title="textMap[dialogStatus]" v-model="dialogVisible" width="600px" :before-close="clsoeDialog" append-to-body :close-on-click-modal="false">
+		<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="120px" style="width: 400px; margin: 0 auto">
+			<el-form-item label="标题" prop="title">
+				<el-input v-model="temp.title" placeholder="请输入标题" />
+			</el-form-item>
+			<el-form-item label="调度周期" prop="interval">
+				<el-input v-model="temp.interval" placeholder="请输入调度周期" />
+			</el-form-item>
+			<el-form-item label="转发格式" prop="encoding">
+				<el-select v-model="temp.encoding" placeholder="请选择转发格式" style="width: 100%">
+					<el-option v-for="item in formatOptions" :key="item.value" :label="item.title" :value="item.value"> </el-option>
+				</el-select>
+				<!-- <el-input v-model="temp.encoding" placeholder="请输入转发格式"/> -->
+			</el-form-item>
+			<el-form-item label="mqtt主题" prop="publishTopic">
+				<el-input v-model="temp.publishTopic" placeholder="请输入mqtt主题" />
+			</el-form-item>
+			<el-form-item label="设备模板" prop="templateNumber">
+				<el-select v-model="temp.templateNumber" filterable placeholder="请选择设备模板" style="width: 100%">
+					<el-option v-for="item in templateOptions" :key="item.number" :label="item.title" :value="item.number"> </el-option>
+				</el-select>
+			</el-form-item>
+		</el-form>
+		<div slot="footer" class="dialog-footer">
+			<el-button @click="clsoeDialog"> 取 消 </el-button>
+			<el-button type="primary" @click="dialogStatus === 'create' ? createData() : updateData()"> 保 存 </el-button>
+		</div>
+	</el-dialog>
+</template>
+
+<script lang="ts">
+import api from '/@/api/device/modbus';
+import { ElMessage } from 'element-plus';
+
+export default {
+	components: {},
+	props: {
+		formatOptions: {
+			default: () => [],
+		},
+	},
+	data() {
+		return {
+			temp: {
+				title: '',
+				interval: '20s',
+				encoding: 'json',
+				publishTopic: '',
+				deviceNumber: '',
+				number: '',
+				templateNumber: '',
+			},
+			rules: {
+				title: [{ required: true, message: '请输入模板名称', trigger: 'change' }],
+				publishTopic: [{ required: true, message: '请输入mqtt主题', trigger: 'change' }],
+				mode: [{ required: true, message: '请选择模式', trigger: 'change' }],
+				templateNumber: [{ required: true, message: '请选择模式', trigger: 'change' }],
+			},
+			dialogVisible: false,
+			listLoading: false,
+			dialogStatus: '',
+			textMap: {
+				update: '任务详情',
+				create: '添加任务',
+			},
+			templateOptions: '',
+		};
+	},
+
+	methods: {
+		openDialog({ dialogStatus, row, deviceNumber }) {
+			this.dialogStatus = dialogStatus;
+			this.temp.deviceNumber = deviceNumber;
+			if (row) {
+				// this.temp = { ...row }
+				this.temp.number = row.Job.number;
+				this.temp.title = row.Job.title;
+				this.temp.interval = row.Job.interval;
+				this.temp.encoding = row.Job.encoding;
+				this.temp.publishTopic = row.Job.publishTopic;
+				this.temp.templateNumber = row?.Template?.number;
+			}
+			// this.getDict()
+			this.getTemplateList();
+			this.dialogVisible = true;
+		},
+		clsoeDialog() {
+			this.dialogVisible = false;
+			this.temp = {
+				title: '',
+				interval: '20s',
+				encoding: 'json',
+				publishTopic: '',
+				deviceNumber: '',
+				number: '',
+				templateNumber: '',
+			};
+			(this.$refs['dataForm'] as any).resetFields();
+		},
+		// 获取模板数据
+		getTemplateList() {
+			this.listLoading = true;
+			api.template
+				.getList({ page: 1, size: 50 })
+				.then((res: any) => {
+					this.templateOptions = res.list || [];
+				})
+				.finally(() => {
+					this.listLoading = false;
+				});
+		},
+		createData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					api.task.addDeviceJob(this.temp).then(() => {
+						this.$emit('finish');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+		updateData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					api.task.editDeviceJob(this.temp).then(() => {
+						this.$emit('finish');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+	},
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 106 - 0
src/views/iot/ice104/device/index.vue

@@ -0,0 +1,106 @@
+<!--
+ * @Author: vera_min vera_min@163.com
+ * @Date: 2023-10-23 17:05:57
+ * @LastEditors: vera_min vera_min@163.com
+ * @LastEditTime: 2023-10-25 10:11:06
+ * @FilePath: /sagoo-admin-ui/src/views/iot/ice104/index.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+	<div class="page page-full">
+		<el-card shadow="hover" class="page-full-part">
+			<div class="search">
+				<el-form :model="params" :inline="true" ref="queryRef">
+					<el-form-item label="通道名称" prop="title">
+						<el-input v-model="params.title" placeholder="请输入通道名称" clearablestyle="width: 240px" @keyup.enter.native="getList(1)" />
+					</el-form-item>
+					<el-form-item label="注册码" prop="number">
+						<el-input v-model="params.number" placeholder="请输入注册码" clearablestyle="width: 240px" @keyup.enter.native="getList(1)" />
+					</el-form-item>
+					<el-form-item>
+						<el-button size="default" type="primary" class="ml10" @click="getList(1)">
+							<el-icon>
+								<ele-Search />
+							</el-icon>
+							查询
+						</el-button>
+						<el-button size="default" @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>
+							新增
+						</el-button>
+					</el-form-item>
+				</el-form>
+			</div>
+			<el-table :data="tableData" style="width: 100%" v-loading="loading">
+				<el-table-column type="index" label="序号" width="80" align="center" />
+				<el-table-column prop="Title" label="设备名称"  align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="SubCode" label="mac地址" min-width="120" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="CommonAddr" label="通用设备地址" min-width="120" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="TemplateNumber" label="模版编号" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="ProductKey" label="产品key" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="DeviceKey" label="设备key" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="CreatedAt" label="创建时间" min-width="160" align="center"></el-table-column>
+				<el-table-column prop="UpdateAt" label="更新时间" min-width="160" align="center"></el-table-column>
+				<el-table-column fixed="right" label="操作" width="100" align="center">
+					<template #default="scope">
+						<el-button size="small" text type="primary" @click="viewDetail(scope.row)">编辑</el-button>
+						<el-button size="small" text type="info" @click="onDel(scope.row)">删除</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+			<pagination v-if="params.total" :total="params.total" v-model:page="params.page" v-model:limit="params.size" @pagination="getList()" />
+		</el-card>
+		<EditForm ref="editFormRef" @getList="getList(1)"></EditForm>
+		<detailForm ref="detailFormRef" @getList="getList()"></detailForm>
+	</div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import EditForm from './component/edit.vue';
+import detailForm from './component/detail.vue';
+import api from '/@/api/ice104/index';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import { useSearch } from '/@/hooks/useCommonIce104';
+
+const editFormRef = ref();
+const detailFormRef = ref();
+const queryRef = ref();
+
+const { params, tableData, getList, loading } = useSearch(api.device.getList, 'data', { keyWord: '', number: '' });
+
+getList();
+
+const addOrEdit = async (row?: any) => {
+	editFormRef.value.open(row);
+};
+const viewDetail = async (row: any) => {
+	detailFormRef.value.open(row);
+};
+
+// 重置表单
+const resetQuery = () => {
+	queryRef.value.resetFields();
+	getList(1);
+};
+
+const onDel = (row: any) => {
+	ElMessageBox.confirm(`此操作将删除接口:“${row.title}”,是否继续?`, '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+	}).then(async () => {
+		await api.channel.deleteDevice({ number: row.number });
+		ElMessage.success('删除成功');
+		getList(1);
+	});
+};
+</script>

+ 268 - 0
src/views/iot/ice104/deviceTask/component/detail.vue

@@ -0,0 +1,268 @@
+<template>
+	<el-dialog title="设备通道详情" v-model="dialogVisible" width="900px" :before-close="clsoeDialog" :close-on-click-modal="false">
+		<el-tabs v-model="activeName">
+			<el-tab-pane label="通道信息" name="1">
+				<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="120px" style="width: 90%; margin: 0 auto">
+					<el-form-item label="通道名称" prop="title">
+						<el-input v-model="temp.title" placeholder="请输入通道名称" />
+					</el-form-item>
+					<el-form-item label="注册码" prop="number">
+						<el-input v-model="temp.number" placeholder="请输入注册码" />
+					</el-form-item>
+					<el-form-item label="设备地址" prop="slaveId">
+						<el-input v-model.number="temp.slaveId" placeholder="请输入设备地址" />
+					</el-form-item>
+					<!-- <el-form-item label="调度周期(秒)" prop="interval">
+            <el-input v-model="temp.interval" placeholder="请输入调度周期" />
+          </el-form-item> -->
+					<el-form-item label="" prop="">
+						<div align="right">
+							<el-button @click="clsoeDialog"> 取 消 </el-button>
+							<el-button type="primary" @click="updateData()"> 保 存 </el-button>
+						</div>
+					</el-form-item>
+				</el-form>
+			</el-tab-pane>
+			<el-tab-pane label="任务" name="2">
+				<div class="filter-container">
+					<el-button class="filter-item" type="primary" icon="el-icon-circle-plus-outline" @click="handleCreate"> 添加任务 </el-button>
+				</div>
+
+				<el-table :key="tableKey" v-loading="listLoading" :data="taskList" border fit highlight-current-row style="width: 100%">
+					<el-table-column label="标题" prop="Job.title" align="center"></el-table-column>
+					<el-table-column label="调度周期" prop="Job.interval" align="center"></el-table-column>
+					<el-table-column label="转发格式" prop="encoding" align="center">
+						<template #default="{ row }">
+							{{ getCodingLabel(row) }}
+						</template>
+					</el-table-column>
+					<el-table-column label="mqtt主题" prop="Job.publishTopic" align="center"></el-table-column>
+					<el-table-column label="模板" prop="Template.title" align="center"></el-table-column>
+					<el-table-column label="操作" align="center" width="200">
+						<template #default="{ row, $index }">
+							<el-button type="primary" size="mini" @click="handleUpdate(row)"> 详情 </el-button>
+							<el-button v-if="row.status != 'deleted'" size="mini" type="danger" @click="handleDelete(row, $index)"> 删除 </el-button>
+						</template>
+					</el-table-column>
+				</el-table>
+
+				<pagination v-if="total > 0" :total="total" v-model:page="listQuery.page" v-model:limit="listQuery.size" @pagination="getList()" />
+				<TaskDialog ref="taskDialog" :formatOptions="formatOptions" @finish="getList" />
+			</el-tab-pane>
+			<el-tab-pane label="通道码流" name="3">
+				<div>
+					<el-button :type="evsrc ? 'info' : 'primary'" @click="openEv()">开始</el-button>
+					<el-button :type="evsrc ? 'primary' : 'info'" :disabled="!evsrc" @click="closeEv()">停止</el-button>
+					<el-button type="defualt" @click="clearLog()">清空</el-button>
+					<el-button type="info" style="margin-left: 150px" @click="downloadLog()">下载报文</el-button>
+					<ul id="logContainer" ref="logContainer"></ul>
+				</div>
+			</el-tab-pane>
+		</el-tabs>
+	</el-dialog>
+</template>
+
+<script lang="ts">
+import { ElMessage } from 'element-plus';
+import api from '/@/api/device/modbus';
+import getOrigin from '/@/utils/origin';
+import TaskDialog from './taskDialog.vue';
+
+export default {
+	components: { TaskDialog },
+	data() {
+		return {
+			temp: {
+				title: '',
+				number: '',
+				templateNumber: '',
+				slaveId: '',
+				interval: '',
+			},
+			rules: {
+				title: [{ required: true, message: '请输入通道名称', trigger: 'blur' }],
+				number: [{ required: true, message: '请输入注册码', trigger: 'blur' }],
+				slaveId: [{ required: true, message: '请输入设备地址', trigger: 'blur' }],
+				templateNumber: [{ required: true, message: '请输入设备模板', trigger: 'change' }],
+			},
+			dialogVisible: false,
+			activeName: '1',
+			taskList: [],
+			listLoading: false,
+			total: 0,
+			tableKey: 0,
+			listQuery: {
+				page: 1,
+				size: 10,
+			},
+			evsrc: null as any,
+			count: 0,
+			templateOptions: [],
+			formatOptions: [],
+		};
+	},
+	watch: {
+		activeName: 'handleTabClick',
+	},
+
+	methods: {
+		open(row: any) {
+			this.temp = { ...row };
+			this.getDict();
+			this.dialogVisible = true;
+		},
+		downloadLog() {
+			window.open(getOrigin(import.meta.env.VITE_MODBUS_API) + '/debug/export_message?number=' + this.temp.number);
+		},
+		clsoeDialog() {
+			this.dialogVisible = false;
+			this.activeName = '1';
+			(this.$refs['dataForm'] as any).resetFields();
+			this.closeEv();
+		},
+		updateData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					const tempData = Object.assign({}, this.temp);
+					api.channel.editDevice(tempData).then(() => {
+						this.$emit('getList');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+		// tab 切换
+		handleTabClick() {
+			this.closeEv(false);
+			if (this.activeName === '2') {
+				// 任务
+				this.getList();
+			} else if (this.activeName === '3') {
+				// 码流
+				// this.initEv()
+			}
+		},
+		handleFilter() {
+			this.listQuery.page = 1;
+			this.getList();
+		},
+		// 获取字典数据
+		getDict() {
+			api.getDict({ code: 'forwardFormat' }).then((data: any) => {
+				this.formatOptions = data.list || [];
+			});
+		},
+		// 获取任务list
+		getList() {
+			this.listLoading = true;
+			api.task
+				.getList(this.listQuery)
+				.then((res: any) => {
+					this.taskList = res.list || [];
+					this.total = res.Total;
+				})
+				.finally(() => {
+					this.listLoading = false;
+				});
+		},
+		handleDelete(row: any) {
+			this.$confirm('是否确认删除任务名称为"' + row.Job.title + '"的数据项?', '警告', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning',
+			})
+				.then(function () {
+					return api.task.deleteDeviceJob({ number: row.Job.number });
+				})
+				.then(() => {
+					this.handleFilter();
+					ElMessage.success('删除成功!');
+				})
+				.catch(function () { });
+		},
+		handleCreate() {
+			(this.$refs.taskDialog as any).openDialog({
+				dialogStatus: 'create',
+				deviceNumber: this.temp.number,
+			});
+		},
+		handleUpdate(row: any) {
+			(this.$refs.taskDialog as any).openDialog({
+				dialogStatus: 'update',
+				row,
+				deviceNumber: this.temp.number,
+			});
+		},
+		// 码流
+		initEv() {
+			if (this.evsrc) return;
+			// this.evsrc = new EventSource(`http://one.server.mydig.net/debug?number=${this.temp.number}`);
+			this.evsrc = new EventSource(`${getOrigin(import.meta.env.VITE_MODBUS_API)}/debug?number=${this.temp.number}`);
+			let that = this;
+			this.evsrc.onmessage = function (ev: any) {
+				try {
+					let obj = JSON.parse(ev.data);
+					if (obj.deviceId === that.temp.number) {
+						let color = obj.type === 'request' ? '#F56C6C' : '#409EFF';
+						let content = `${obj.type === 'request' ? '请求:' : '应答:'} ${obj.msg}`;
+						this.$refs.logContainer.insertAdjacentHTML('afterbegin', `<li style="color: ${color}">${content}</li>`);
+					}
+				} catch (_e) {
+					// @ts-ignore
+					// console.log('error', e);
+				}
+			};
+			// this.evsrc.onerror = function (_ev: any) {
+				// @ts-ignore
+				// console.log('readyState = ' + ev.currentTarget.readyState);
+			// };
+		},
+		openEv() {
+			(this.$refs.logContainer as any).insertAdjacentHTML('beforeEnd', `<li style="color: #000;">开始...</li>`);
+			this.initEv();
+		},
+		closeEv(log = true) {
+			this.evsrc && this.evsrc.close();
+			this.evsrc = null;
+			this.count = 0;
+			// this.clearLog()
+			if (log) {
+				(this.$refs.logContainer as any).insertAdjacentHTML('beforeEnd', `<li style="color: #000;">停止...</li>`);
+			}
+		},
+		// 清空码流
+		clearLog() {
+			(this.$refs.logContainer as any).innerHTML = '';
+		},
+		getCodingLabel(row: any) {
+			const item = this.formatOptions.find((item: any) => item.value === row.Job.encoding) as any;
+			return item.title;
+		},
+	},
+};
+</script>
+
+<style lang="scss" scoped>
+.filter-container {
+	margin-bottom: 10px;
+}
+
+.filter-item {
+	margin-right: 10px;
+}
+
+#logContainer {
+	height: 400px;
+	overflow: auto;
+	border: 1px solid #ddd;
+	padding: 10px;
+	padding-left: 20px;
+	border-radius: 8px;
+	margin-top: 10px;
+	word-wrap: break-word;
+	word-break: normal;
+	background-color: #f2f2f2;
+	line-height: 22px;
+}
+</style>

+ 80 - 0
src/views/iot/ice104/deviceTask/component/edit.vue

@@ -0,0 +1,80 @@
+<template>
+	<el-dialog title="添加设备通道" v-model="dialogVisible" width="600px" :before-close="clsoeDialog" :close-on-click-modal="false">
+		<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="120px" style="width: 90%; margin: 0 auto">
+			<el-form-item label="通道名称" prop="title">
+				<el-input v-model="temp.title" placeholder="请输入通道名称" />
+			</el-form-item>
+			<el-form-item label="注册码" prop="number">
+				<el-input v-model="temp.number" placeholder="请输入注册码" />
+			</el-form-item>
+			<el-form-item label="设备地址" prop="slaveId">
+				<el-input v-model.number="temp.slaveId" placeholder="请输入设备地址" />
+			</el-form-item>
+		</el-form>
+		<div slot="footer" class="dialog-footer">
+			<el-button @click="clsoeDialog()"> 取 消 </el-button>
+			<el-button type="primary" @click="createData()"> 保 存 </el-button>
+		</div>
+	</el-dialog>
+</template>
+<script lang="ts">
+import api from '/@/api/device/modbus';
+import { ElMessage } from 'element-plus';
+export default {
+	data() {
+		return {
+			temp: {
+				title: '',
+				number: '',
+				templateNumber: '',
+				slaveId: '',
+				interval: '',
+			},
+			rules: {
+				title: [{ required: true, message: '请输入通道名称', trigger: 'blur' }],
+				number: [{ required: true, message: '请输入注册码', trigger: 'blur' }],
+				slaveId: [{ required: true, message: '请输入设备地址', trigger: 'blur' }],
+			},
+			dialogVisible: false,
+			listLoading: false,
+			templateOptions: [],
+		};
+	},
+
+	methods: {
+		open() {
+			// this.getList();
+			this.dialogVisible = true;
+		},
+		clsoeDialog() {
+			(this.$refs.dataForm as any).resetFields();
+			this.dialogVisible = false;
+		},
+		// 获取模板数据
+		getList() {
+			this.listLoading = true;
+			api.channel
+				.getList({ page: 1, size: 50 })
+				.then((data: any) => {
+					this.templateOptions = data.list || [];
+				})
+				.finally(() => {
+					this.listLoading = false;
+				});
+		},
+		createData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					api.channel.addDevice(this.temp).then(() => {
+						this.$emit('getList');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+	},
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 139 - 0
src/views/iot/ice104/deviceTask/component/taskDialog.vue

@@ -0,0 +1,139 @@
+<template>
+	<el-dialog :title="textMap[dialogStatus]" v-model="dialogVisible" width="600px" :before-close="clsoeDialog" append-to-body :close-on-click-modal="false">
+		<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="120px" style="width: 400px; margin: 0 auto">
+			<el-form-item label="标题" prop="title">
+				<el-input v-model="temp.title" placeholder="请输入标题" />
+			</el-form-item>
+			<el-form-item label="调度周期" prop="interval">
+				<el-input v-model="temp.interval" placeholder="请输入调度周期" />
+			</el-form-item>
+			<el-form-item label="转发格式" prop="encoding">
+				<el-select v-model="temp.encoding" placeholder="请选择转发格式" style="width: 100%">
+					<el-option v-for="item in formatOptions" :key="item.value" :label="item.title" :value="item.value"> </el-option>
+				</el-select>
+				<!-- <el-input v-model="temp.encoding" placeholder="请输入转发格式"/> -->
+			</el-form-item>
+			<el-form-item label="mqtt主题" prop="publishTopic">
+				<el-input v-model="temp.publishTopic" placeholder="请输入mqtt主题" />
+			</el-form-item>
+			<el-form-item label="设备模板" prop="templateNumber">
+				<el-select v-model="temp.templateNumber" filterable placeholder="请选择设备模板" style="width: 100%">
+					<el-option v-for="item in templateOptions" :key="item.number" :label="item.title" :value="item.number"> </el-option>
+				</el-select>
+			</el-form-item>
+		</el-form>
+		<div slot="footer" class="dialog-footer">
+			<el-button @click="clsoeDialog"> 取 消 </el-button>
+			<el-button type="primary" @click="dialogStatus === 'create' ? createData() : updateData()"> 保 存 </el-button>
+		</div>
+	</el-dialog>
+</template>
+
+<script lang="ts">
+import api from '/@/api/device/modbus';
+import { ElMessage } from 'element-plus';
+
+export default {
+	components: {},
+	props: {
+		formatOptions: {
+			default: () => [],
+		},
+	},
+	data() {
+		return {
+			temp: {
+				title: '',
+				interval: '20s',
+				encoding: 'json',
+				publishTopic: '',
+				deviceNumber: '',
+				number: '',
+				templateNumber: '',
+			},
+			rules: {
+				title: [{ required: true, message: '请输入模板名称', trigger: 'change' }],
+				publishTopic: [{ required: true, message: '请输入mqtt主题', trigger: 'change' }],
+				mode: [{ required: true, message: '请选择模式', trigger: 'change' }],
+				templateNumber: [{ required: true, message: '请选择模式', trigger: 'change' }],
+			},
+			dialogVisible: false,
+			listLoading: false,
+			dialogStatus: '',
+			textMap: {
+				update: '任务详情',
+				create: '添加任务',
+			},
+			templateOptions: '',
+		};
+	},
+
+	methods: {
+		openDialog({ dialogStatus, row, deviceNumber }) {
+			this.dialogStatus = dialogStatus;
+			this.temp.deviceNumber = deviceNumber;
+			if (row) {
+				// this.temp = { ...row }
+				this.temp.number = row.Job.number;
+				this.temp.title = row.Job.title;
+				this.temp.interval = row.Job.interval;
+				this.temp.encoding = row.Job.encoding;
+				this.temp.publishTopic = row.Job.publishTopic;
+				this.temp.templateNumber = row?.Template?.number;
+			}
+			// this.getDict()
+			this.getTemplateList();
+			this.dialogVisible = true;
+		},
+		clsoeDialog() {
+			this.dialogVisible = false;
+			this.temp = {
+				title: '',
+				interval: '20s',
+				encoding: 'json',
+				publishTopic: '',
+				deviceNumber: '',
+				number: '',
+				templateNumber: '',
+			};
+			(this.$refs['dataForm'] as any).resetFields();
+		},
+		// 获取模板数据
+		getTemplateList() {
+			this.listLoading = true;
+			api.template
+				.getList({ page: 1, size: 50 })
+				.then((res: any) => {
+					this.templateOptions = res.list || [];
+				})
+				.finally(() => {
+					this.listLoading = false;
+				});
+		},
+		createData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					api.task.addDeviceJob(this.temp).then(() => {
+						this.$emit('finish');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+		updateData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					api.task.editDeviceJob(this.temp).then(() => {
+						this.$emit('finish');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+	},
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 102 - 0
src/views/iot/ice104/deviceTask/index.vue

@@ -0,0 +1,102 @@
+<!--
+ * @Author: vera_min vera_min@163.com
+ * @Date: 2023-10-23 17:05:57
+ * @LastEditors: vera_min vera_min@163.com
+ * @LastEditTime: 2023-10-25 08:53:40
+ * @FilePath: /sagoo-admin-ui/src/views/iot/ice104/index.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+	<div class="page page-full">
+		<el-card shadow="hover" class="page-full-part">
+			<div class="search">
+				<el-form :model="params" :inline="true" ref="queryRef">
+					<el-form-item label="通道名称" prop="title">
+						<el-input v-model="params.title" placeholder="请输入通道名称" clearablestyle="width: 240px" @keyup.enter.native="getList(1)" />
+					</el-form-item>
+					<el-form-item label="注册码" prop="number">
+						<el-input v-model="params.number" placeholder="请输入注册码" clearablestyle="width: 240px" @keyup.enter.native="getList(1)" />
+					</el-form-item>
+					<el-form-item>
+						<el-button size="default" type="primary" class="ml10" @click="getList(1)">
+							<el-icon>
+								<ele-Search />
+							</el-icon>
+							查询
+						</el-button>
+						<el-button size="default" @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>
+							新增通道
+						</el-button>
+					</el-form-item>
+				</el-form>
+			</div>
+			<el-table :data="tableData" style="width: 100%" v-loading="loading">
+				<el-table-column type="index" label="序号" width="80" align="center" />
+				<el-table-column prop="title" label="通道名称" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="number" label="注册码" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="slaveId" label="设备地址" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column label="操作" width="100" align="center">
+					<template #default="scope">
+						<el-button size="small" text type="primary" v-auth="'detail'" @click="viewDetail(scope.row)">详情</el-button>
+						<el-button size="small" text type="info" v-auth="'del'" @click="onDel(scope.row)">删除</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+			<pagination v-if="params.total" :total="params.total" v-model:page="params.page" v-model:limit="params.size" @pagination="getList()" />
+		</el-card>
+		<EditForm ref="editFormRef" @getList="getList(1)"></EditForm>
+		<detailForm ref="detailFormRef" @getList="getList()"></detailForm>
+	</div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import EditForm from './component/edit.vue';
+import detailForm from './component/detail.vue';
+import api from '/@/api/ice104/index';
+import { ElMessageBox, ElMessage } from 'element-plus';
+// import { useSearch } from '/@/hooks/useCommon';
+import { useSearch } from '/@/hooks/useCommonIce104';
+
+const editFormRef = ref();
+const detailFormRef = ref();
+const queryRef = ref();
+
+const { params, tableData, getList, loading } = useSearch(api.device.getList, 'list', { keyWord: '', number: '' });
+
+getList();
+
+const addOrEdit = async (row?: any) => {
+	editFormRef.value.open(row);
+};
+const viewDetail = async (row: any) => {
+	detailFormRef.value.open(row);
+};
+
+// 重置表单
+const resetQuery = () => {
+	queryRef.value.resetFields();
+	getList(1);
+};
+
+const onDel = (row: any) => {
+	ElMessageBox.confirm(`此操作将删除接口:“${row.title}”,是否继续?`, '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+	}).then(async () => {
+		await api.channel.deleteDevice({ number: row.number });
+		ElMessage.success('删除成功');
+		getList(1);
+	});
+};
+</script>

+ 268 - 0
src/views/iot/ice104/deviceTemplate/component/detail.vue

@@ -0,0 +1,268 @@
+<template>
+	<el-dialog title="设备通道详情" v-model="dialogVisible" width="900px" :before-close="clsoeDialog" :close-on-click-modal="false">
+		<el-tabs v-model="activeName">
+			<el-tab-pane label="通道信息" name="1">
+				<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="120px" style="width: 90%; margin: 0 auto">
+					<el-form-item label="通道名称" prop="title">
+						<el-input v-model="temp.title" placeholder="请输入通道名称" />
+					</el-form-item>
+					<el-form-item label="注册码" prop="number">
+						<el-input v-model="temp.number" placeholder="请输入注册码" />
+					</el-form-item>
+					<el-form-item label="设备地址" prop="slaveId">
+						<el-input v-model.number="temp.slaveId" placeholder="请输入设备地址" />
+					</el-form-item>
+					<!-- <el-form-item label="调度周期(秒)" prop="interval">
+            <el-input v-model="temp.interval" placeholder="请输入调度周期" />
+          </el-form-item> -->
+					<el-form-item label="" prop="">
+						<div align="right">
+							<el-button @click="clsoeDialog"> 取 消 </el-button>
+							<el-button type="primary" @click="updateData()"> 保 存 </el-button>
+						</div>
+					</el-form-item>
+				</el-form>
+			</el-tab-pane>
+			<el-tab-pane label="任务" name="2">
+				<div class="filter-container">
+					<el-button class="filter-item" type="primary" icon="el-icon-circle-plus-outline" @click="handleCreate"> 添加任务 </el-button>
+				</div>
+
+				<el-table :key="tableKey" v-loading="listLoading" :data="taskList" border fit highlight-current-row style="width: 100%">
+					<el-table-column label="标题" prop="Job.title" align="center"></el-table-column>
+					<el-table-column label="调度周期" prop="Job.interval" align="center"></el-table-column>
+					<el-table-column label="转发格式" prop="encoding" align="center">
+						<template #default="{ row }">
+							{{ getCodingLabel(row) }}
+						</template>
+					</el-table-column>
+					<el-table-column label="mqtt主题" prop="Job.publishTopic" align="center"></el-table-column>
+					<el-table-column label="模板" prop="Template.title" align="center"></el-table-column>
+					<el-table-column label="操作" align="center" width="200">
+						<template #default="{ row, $index }">
+							<el-button type="primary" size="mini" @click="handleUpdate(row)"> 详情 </el-button>
+							<el-button v-if="row.status != 'deleted'" size="mini" type="danger" @click="handleDelete(row, $index)"> 删除 </el-button>
+						</template>
+					</el-table-column>
+				</el-table>
+
+				<pagination v-if="total > 0" :total="total" v-model:page="listQuery.page" v-model:limit="listQuery.size" @pagination="getList()" />
+				<TaskDialog ref="taskDialog" :formatOptions="formatOptions" @finish="getList" />
+			</el-tab-pane>
+			<el-tab-pane label="通道码流" name="3">
+				<div>
+					<el-button :type="evsrc ? 'info' : 'primary'" @click="openEv()">开始</el-button>
+					<el-button :type="evsrc ? 'primary' : 'info'" :disabled="!evsrc" @click="closeEv()">停止</el-button>
+					<el-button type="defualt" @click="clearLog()">清空</el-button>
+					<el-button type="info" style="margin-left: 150px" @click="downloadLog()">下载报文</el-button>
+					<ul id="logContainer" ref="logContainer"></ul>
+				</div>
+			</el-tab-pane>
+		</el-tabs>
+	</el-dialog>
+</template>
+
+<script lang="ts">
+import { ElMessage } from 'element-plus';
+import api from '/@/api/device/modbus';
+import getOrigin from '/@/utils/origin';
+import TaskDialog from './taskDialog.vue';
+
+export default {
+	components: { TaskDialog },
+	data() {
+		return {
+			temp: {
+				title: '',
+				number: '',
+				templateNumber: '',
+				slaveId: '',
+				interval: '',
+			},
+			rules: {
+				title: [{ required: true, message: '请输入通道名称', trigger: 'blur' }],
+				number: [{ required: true, message: '请输入注册码', trigger: 'blur' }],
+				slaveId: [{ required: true, message: '请输入设备地址', trigger: 'blur' }],
+				templateNumber: [{ required: true, message: '请输入设备模板', trigger: 'change' }],
+			},
+			dialogVisible: false,
+			activeName: '1',
+			taskList: [],
+			listLoading: false,
+			total: 0,
+			tableKey: 0,
+			listQuery: {
+				page: 1,
+				size: 10,
+			},
+			evsrc: null as any,
+			count: 0,
+			templateOptions: [],
+			formatOptions: [],
+		};
+	},
+	watch: {
+		activeName: 'handleTabClick',
+	},
+
+	methods: {
+		open(row: any) {
+			this.temp = { ...row };
+			this.getDict();
+			this.dialogVisible = true;
+		},
+		downloadLog() {
+			window.open(getOrigin(import.meta.env.VITE_MODBUS_API) + '/debug/export_message?number=' + this.temp.number);
+		},
+		clsoeDialog() {
+			this.dialogVisible = false;
+			this.activeName = '1';
+			(this.$refs['dataForm'] as any).resetFields();
+			this.closeEv();
+		},
+		updateData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					const tempData = Object.assign({}, this.temp);
+					api.channel.editDevice(tempData).then(() => {
+						this.$emit('getList');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+		// tab 切换
+		handleTabClick() {
+			this.closeEv(false);
+			if (this.activeName === '2') {
+				// 任务
+				this.getList();
+			} else if (this.activeName === '3') {
+				// 码流
+				// this.initEv()
+			}
+		},
+		handleFilter() {
+			this.listQuery.page = 1;
+			this.getList();
+		},
+		// 获取字典数据
+		getDict() {
+			api.getDict({ code: 'forwardFormat' }).then((data: any) => {
+				this.formatOptions = data.list || [];
+			});
+		},
+		// 获取任务list
+		getList() {
+			this.listLoading = true;
+			api.task
+				.getList(this.listQuery)
+				.then((res: any) => {
+					this.taskList = res.list || [];
+					this.total = res.Total;
+				})
+				.finally(() => {
+					this.listLoading = false;
+				});
+		},
+		handleDelete(row: any) {
+			this.$confirm('是否确认删除任务名称为"' + row.Job.title + '"的数据项?', '警告', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning',
+			})
+				.then(function () {
+					return api.task.deleteDeviceJob({ number: row.Job.number });
+				})
+				.then(() => {
+					this.handleFilter();
+					ElMessage.success('删除成功!');
+				})
+				.catch(function () { });
+		},
+		handleCreate() {
+			(this.$refs.taskDialog as any).openDialog({
+				dialogStatus: 'create',
+				deviceNumber: this.temp.number,
+			});
+		},
+		handleUpdate(row: any) {
+			(this.$refs.taskDialog as any).openDialog({
+				dialogStatus: 'update',
+				row,
+				deviceNumber: this.temp.number,
+			});
+		},
+		// 码流
+		initEv() {
+			if (this.evsrc) return;
+			// this.evsrc = new EventSource(`http://one.server.mydig.net/debug?number=${this.temp.number}`);
+			this.evsrc = new EventSource(`${getOrigin(import.meta.env.VITE_MODBUS_API)}/debug?number=${this.temp.number}`);
+			let that = this;
+			this.evsrc.onmessage = function (ev: any) {
+				try {
+					let obj = JSON.parse(ev.data);
+					if (obj.deviceId === that.temp.number) {
+						let color = obj.type === 'request' ? '#F56C6C' : '#409EFF';
+						let content = `${obj.type === 'request' ? '请求:' : '应答:'} ${obj.msg}`;
+						this.$refs.logContainer.insertAdjacentHTML('afterbegin', `<li style="color: ${color}">${content}</li>`);
+					}
+				} catch (_e) {
+					// @ts-ignore
+					// console.log('error', e);
+				}
+			};
+			// this.evsrc.onerror = function (_ev: any) {
+				// @ts-ignore
+				// console.log('readyState = ' + ev.currentTarget.readyState);
+			// };
+		},
+		openEv() {
+			(this.$refs.logContainer as any).insertAdjacentHTML('beforeEnd', `<li style="color: #000;">开始...</li>`);
+			this.initEv();
+		},
+		closeEv(log = true) {
+			this.evsrc && this.evsrc.close();
+			this.evsrc = null;
+			this.count = 0;
+			// this.clearLog()
+			if (log) {
+				(this.$refs.logContainer as any).insertAdjacentHTML('beforeEnd', `<li style="color: #000;">停止...</li>`);
+			}
+		},
+		// 清空码流
+		clearLog() {
+			(this.$refs.logContainer as any).innerHTML = '';
+		},
+		getCodingLabel(row: any) {
+			const item = this.formatOptions.find((item: any) => item.value === row.Job.encoding) as any;
+			return item.title;
+		},
+	},
+};
+</script>
+
+<style lang="scss" scoped>
+.filter-container {
+	margin-bottom: 10px;
+}
+
+.filter-item {
+	margin-right: 10px;
+}
+
+#logContainer {
+	height: 400px;
+	overflow: auto;
+	border: 1px solid #ddd;
+	padding: 10px;
+	padding-left: 20px;
+	border-radius: 8px;
+	margin-top: 10px;
+	word-wrap: break-word;
+	word-break: normal;
+	background-color: #f2f2f2;
+	line-height: 22px;
+}
+</style>

+ 80 - 0
src/views/iot/ice104/deviceTemplate/component/edit.vue

@@ -0,0 +1,80 @@
+<template>
+	<el-dialog title="添加设备通道" v-model="dialogVisible" width="600px" :before-close="clsoeDialog" :close-on-click-modal="false">
+		<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="120px" style="width: 90%; margin: 0 auto">
+			<el-form-item label="通道名称" prop="title">
+				<el-input v-model="temp.title" placeholder="请输入通道名称" />
+			</el-form-item>
+			<el-form-item label="注册码" prop="number">
+				<el-input v-model="temp.number" placeholder="请输入注册码" />
+			</el-form-item>
+			<el-form-item label="设备地址" prop="slaveId">
+				<el-input v-model.number="temp.slaveId" placeholder="请输入设备地址" />
+			</el-form-item>
+		</el-form>
+		<div slot="footer" class="dialog-footer">
+			<el-button @click="clsoeDialog()"> 取 消 </el-button>
+			<el-button type="primary" @click="createData()"> 保 存 </el-button>
+		</div>
+	</el-dialog>
+</template>
+<script lang="ts">
+import api from '/@/api/device/modbus';
+import { ElMessage } from 'element-plus';
+export default {
+	data() {
+		return {
+			temp: {
+				title: '',
+				number: '',
+				templateNumber: '',
+				slaveId: '',
+				interval: '',
+			},
+			rules: {
+				title: [{ required: true, message: '请输入通道名称', trigger: 'blur' }],
+				number: [{ required: true, message: '请输入注册码', trigger: 'blur' }],
+				slaveId: [{ required: true, message: '请输入设备地址', trigger: 'blur' }],
+			},
+			dialogVisible: false,
+			listLoading: false,
+			templateOptions: [],
+		};
+	},
+
+	methods: {
+		open() {
+			// this.getList();
+			this.dialogVisible = true;
+		},
+		clsoeDialog() {
+			(this.$refs.dataForm as any).resetFields();
+			this.dialogVisible = false;
+		},
+		// 获取模板数据
+		getList() {
+			this.listLoading = true;
+			api.channel
+				.getList({ page: 1, size: 50 })
+				.then((data: any) => {
+					this.templateOptions = data.list || [];
+				})
+				.finally(() => {
+					this.listLoading = false;
+				});
+		},
+		createData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					api.channel.addDevice(this.temp).then(() => {
+						this.$emit('getList');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+	},
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 139 - 0
src/views/iot/ice104/deviceTemplate/component/taskDialog.vue

@@ -0,0 +1,139 @@
+<template>
+	<el-dialog :title="textMap[dialogStatus]" v-model="dialogVisible" width="600px" :before-close="clsoeDialog" append-to-body :close-on-click-modal="false">
+		<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="120px" style="width: 400px; margin: 0 auto">
+			<el-form-item label="标题" prop="title">
+				<el-input v-model="temp.title" placeholder="请输入标题" />
+			</el-form-item>
+			<el-form-item label="调度周期" prop="interval">
+				<el-input v-model="temp.interval" placeholder="请输入调度周期" />
+			</el-form-item>
+			<el-form-item label="转发格式" prop="encoding">
+				<el-select v-model="temp.encoding" placeholder="请选择转发格式" style="width: 100%">
+					<el-option v-for="item in formatOptions" :key="item.value" :label="item.title" :value="item.value"> </el-option>
+				</el-select>
+				<!-- <el-input v-model="temp.encoding" placeholder="请输入转发格式"/> -->
+			</el-form-item>
+			<el-form-item label="mqtt主题" prop="publishTopic">
+				<el-input v-model="temp.publishTopic" placeholder="请输入mqtt主题" />
+			</el-form-item>
+			<el-form-item label="设备模板" prop="templateNumber">
+				<el-select v-model="temp.templateNumber" filterable placeholder="请选择设备模板" style="width: 100%">
+					<el-option v-for="item in templateOptions" :key="item.number" :label="item.title" :value="item.number"> </el-option>
+				</el-select>
+			</el-form-item>
+		</el-form>
+		<div slot="footer" class="dialog-footer">
+			<el-button @click="clsoeDialog"> 取 消 </el-button>
+			<el-button type="primary" @click="dialogStatus === 'create' ? createData() : updateData()"> 保 存 </el-button>
+		</div>
+	</el-dialog>
+</template>
+
+<script lang="ts">
+import api from '/@/api/device/modbus';
+import { ElMessage } from 'element-plus';
+
+export default {
+	components: {},
+	props: {
+		formatOptions: {
+			default: () => [],
+		},
+	},
+	data() {
+		return {
+			temp: {
+				title: '',
+				interval: '20s',
+				encoding: 'json',
+				publishTopic: '',
+				deviceNumber: '',
+				number: '',
+				templateNumber: '',
+			},
+			rules: {
+				title: [{ required: true, message: '请输入模板名称', trigger: 'change' }],
+				publishTopic: [{ required: true, message: '请输入mqtt主题', trigger: 'change' }],
+				mode: [{ required: true, message: '请选择模式', trigger: 'change' }],
+				templateNumber: [{ required: true, message: '请选择模式', trigger: 'change' }],
+			},
+			dialogVisible: false,
+			listLoading: false,
+			dialogStatus: '',
+			textMap: {
+				update: '任务详情',
+				create: '添加任务',
+			},
+			templateOptions: '',
+		};
+	},
+
+	methods: {
+		openDialog({ dialogStatus, row, deviceNumber }) {
+			this.dialogStatus = dialogStatus;
+			this.temp.deviceNumber = deviceNumber;
+			if (row) {
+				// this.temp = { ...row }
+				this.temp.number = row.Job.number;
+				this.temp.title = row.Job.title;
+				this.temp.interval = row.Job.interval;
+				this.temp.encoding = row.Job.encoding;
+				this.temp.publishTopic = row.Job.publishTopic;
+				this.temp.templateNumber = row?.Template?.number;
+			}
+			// this.getDict()
+			this.getTemplateList();
+			this.dialogVisible = true;
+		},
+		clsoeDialog() {
+			this.dialogVisible = false;
+			this.temp = {
+				title: '',
+				interval: '20s',
+				encoding: 'json',
+				publishTopic: '',
+				deviceNumber: '',
+				number: '',
+				templateNumber: '',
+			};
+			(this.$refs['dataForm'] as any).resetFields();
+		},
+		// 获取模板数据
+		getTemplateList() {
+			this.listLoading = true;
+			api.template
+				.getList({ page: 1, size: 50 })
+				.then((res: any) => {
+					this.templateOptions = res.list || [];
+				})
+				.finally(() => {
+					this.listLoading = false;
+				});
+		},
+		createData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					api.task.addDeviceJob(this.temp).then(() => {
+						this.$emit('finish');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+		updateData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					api.task.editDeviceJob(this.temp).then(() => {
+						this.$emit('finish');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+	},
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 102 - 0
src/views/iot/ice104/deviceTemplate/index.vue

@@ -0,0 +1,102 @@
+<!--
+ * @Author: vera_min vera_min@163.com
+ * @Date: 2023-10-23 17:05:57
+ * @LastEditors: vera_min vera_min@163.com
+ * @LastEditTime: 2023-10-25 08:53:48
+ * @FilePath: /sagoo-admin-ui/src/views/iot/ice104/index.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+	<div class="page page-full">
+		<el-card shadow="hover" class="page-full-part">
+			<div class="search">
+				<el-form :model="params" :inline="true" ref="queryRef">
+					<el-form-item label="通道名称" prop="title">
+						<el-input v-model="params.title" placeholder="请输入通道名称" clearablestyle="width: 240px" @keyup.enter.native="getList(1)" />
+					</el-form-item>
+					<el-form-item label="注册码" prop="number">
+						<el-input v-model="params.number" placeholder="请输入注册码" clearablestyle="width: 240px" @keyup.enter.native="getList(1)" />
+					</el-form-item>
+					<el-form-item>
+						<el-button size="default" type="primary" class="ml10" @click="getList(1)">
+							<el-icon>
+								<ele-Search />
+							</el-icon>
+							查询
+						</el-button>
+						<el-button size="default" @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>
+							新增通道
+						</el-button>
+					</el-form-item>
+				</el-form>
+			</div>
+			<el-table :data="tableData" style="width: 100%" v-loading="loading">
+				<el-table-column type="index" label="序号" width="80" align="center" />
+				<el-table-column prop="title" label="通道名称" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="number" label="注册码" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="slaveId" label="设备地址" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column label="操作" width="100" align="center">
+					<template #default="scope">
+						<el-button size="small" text type="primary" v-auth="'detail'" @click="viewDetail(scope.row)">详情</el-button>
+						<el-button size="small" text type="info" v-auth="'del'" @click="onDel(scope.row)">删除</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+			<pagination v-if="params.total" :total="params.total" v-model:page="params.page" v-model:limit="params.size" @pagination="getList()" />
+		</el-card>
+		<EditForm ref="editFormRef" @getList="getList(1)"></EditForm>
+		<detailForm ref="detailFormRef" @getList="getList()"></detailForm>
+	</div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import EditForm from './component/edit.vue';
+import detailForm from './component/detail.vue';
+import api from '/@/api/ice104/index';
+import { ElMessageBox, ElMessage } from 'element-plus';
+// import { useSearch } from '/@/hooks/useCommon';
+import { useSearch } from '/@/hooks/useCommonIce104';
+
+const editFormRef = ref();
+const detailFormRef = ref();
+const queryRef = ref();
+
+const { params, tableData, getList, loading } = useSearch(api.device.getList, 'list', { keyWord: '', number: '' });
+
+getList();
+
+const addOrEdit = async (row?: any) => {
+	editFormRef.value.open(row);
+};
+const viewDetail = async (row: any) => {
+	detailFormRef.value.open(row);
+};
+
+// 重置表单
+const resetQuery = () => {
+	queryRef.value.resetFields();
+	getList(1);
+};
+
+const onDel = (row: any) => {
+	ElMessageBox.confirm(`此操作将删除接口:“${row.title}”,是否继续?`, '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+	}).then(async () => {
+		await api.channel.deleteDevice({ number: row.number });
+		ElMessage.success('删除成功');
+		getList(1);
+	});
+};
+</script>

+ 268 - 0
src/views/iot/ice104/template/component/detail.vue

@@ -0,0 +1,268 @@
+<template>
+	<el-dialog title="设备通道详情" v-model="dialogVisible" width="900px" :before-close="clsoeDialog" :close-on-click-modal="false">
+		<el-tabs v-model="activeName">
+			<el-tab-pane label="通道信息" name="1">
+				<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="120px" style="width: 90%; margin: 0 auto">
+					<el-form-item label="通道名称" prop="title">
+						<el-input v-model="temp.title" placeholder="请输入通道名称" />
+					</el-form-item>
+					<el-form-item label="注册码" prop="number">
+						<el-input v-model="temp.number" placeholder="请输入注册码" />
+					</el-form-item>
+					<el-form-item label="设备地址" prop="slaveId">
+						<el-input v-model.number="temp.slaveId" placeholder="请输入设备地址" />
+					</el-form-item>
+					<!-- <el-form-item label="调度周期(秒)" prop="interval">
+            <el-input v-model="temp.interval" placeholder="请输入调度周期" />
+          </el-form-item> -->
+					<el-form-item label="" prop="">
+						<div align="right">
+							<el-button @click="clsoeDialog"> 取 消 </el-button>
+							<el-button type="primary" @click="updateData()"> 保 存 </el-button>
+						</div>
+					</el-form-item>
+				</el-form>
+			</el-tab-pane>
+			<el-tab-pane label="任务" name="2">
+				<div class="filter-container">
+					<el-button class="filter-item" type="primary" icon="el-icon-circle-plus-outline" @click="handleCreate"> 添加任务 </el-button>
+				</div>
+
+				<el-table :key="tableKey" v-loading="listLoading" :data="taskList" border fit highlight-current-row style="width: 100%">
+					<el-table-column label="标题" prop="Job.title" align="center"></el-table-column>
+					<el-table-column label="调度周期" prop="Job.interval" align="center"></el-table-column>
+					<el-table-column label="转发格式" prop="encoding" align="center">
+						<template #default="{ row }">
+							{{ getCodingLabel(row) }}
+						</template>
+					</el-table-column>
+					<el-table-column label="mqtt主题" prop="Job.publishTopic" align="center"></el-table-column>
+					<el-table-column label="模板" prop="Template.title" align="center"></el-table-column>
+					<el-table-column label="操作" align="center" width="200">
+						<template #default="{ row, $index }">
+							<el-button type="primary" size="mini" @click="handleUpdate(row)"> 详情 </el-button>
+							<el-button v-if="row.status != 'deleted'" size="mini" type="danger" @click="handleDelete(row, $index)"> 删除 </el-button>
+						</template>
+					</el-table-column>
+				</el-table>
+
+				<pagination v-if="total > 0" :total="total" v-model:page="listQuery.page" v-model:limit="listQuery.size" @pagination="getList()" />
+				<TaskDialog ref="taskDialog" :formatOptions="formatOptions" @finish="getList" />
+			</el-tab-pane>
+			<el-tab-pane label="通道码流" name="3">
+				<div>
+					<el-button :type="evsrc ? 'info' : 'primary'" @click="openEv()">开始</el-button>
+					<el-button :type="evsrc ? 'primary' : 'info'" :disabled="!evsrc" @click="closeEv()">停止</el-button>
+					<el-button type="defualt" @click="clearLog()">清空</el-button>
+					<el-button type="info" style="margin-left: 150px" @click="downloadLog()">下载报文</el-button>
+					<ul id="logContainer" ref="logContainer"></ul>
+				</div>
+			</el-tab-pane>
+		</el-tabs>
+	</el-dialog>
+</template>
+
+<script lang="ts">
+import { ElMessage } from 'element-plus';
+import api from '/@/api/device/modbus';
+import getOrigin from '/@/utils/origin';
+import TaskDialog from './taskDialog.vue';
+
+export default {
+	components: { TaskDialog },
+	data() {
+		return {
+			temp: {
+				title: '',
+				number: '',
+				templateNumber: '',
+				slaveId: '',
+				interval: '',
+			},
+			rules: {
+				title: [{ required: true, message: '请输入通道名称', trigger: 'blur' }],
+				number: [{ required: true, message: '请输入注册码', trigger: 'blur' }],
+				slaveId: [{ required: true, message: '请输入设备地址', trigger: 'blur' }],
+				templateNumber: [{ required: true, message: '请输入设备模板', trigger: 'change' }],
+			},
+			dialogVisible: false,
+			activeName: '1',
+			taskList: [],
+			listLoading: false,
+			total: 0,
+			tableKey: 0,
+			listQuery: {
+				page: 1,
+				size: 10,
+			},
+			evsrc: null as any,
+			count: 0,
+			templateOptions: [],
+			formatOptions: [],
+		};
+	},
+	watch: {
+		activeName: 'handleTabClick',
+	},
+
+	methods: {
+		open(row: any) {
+			this.temp = { ...row };
+			this.getDict();
+			this.dialogVisible = true;
+		},
+		downloadLog() {
+			window.open(getOrigin(import.meta.env.VITE_MODBUS_API) + '/debug/export_message?number=' + this.temp.number);
+		},
+		clsoeDialog() {
+			this.dialogVisible = false;
+			this.activeName = '1';
+			(this.$refs['dataForm'] as any).resetFields();
+			this.closeEv();
+		},
+		updateData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					const tempData = Object.assign({}, this.temp);
+					api.channel.editDevice(tempData).then(() => {
+						this.$emit('getList');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+		// tab 切换
+		handleTabClick() {
+			this.closeEv(false);
+			if (this.activeName === '2') {
+				// 任务
+				this.getList();
+			} else if (this.activeName === '3') {
+				// 码流
+				// this.initEv()
+			}
+		},
+		handleFilter() {
+			this.listQuery.page = 1;
+			this.getList();
+		},
+		// 获取字典数据
+		getDict() {
+			api.getDict({ code: 'forwardFormat' }).then((data: any) => {
+				this.formatOptions = data.list || [];
+			});
+		},
+		// 获取任务list
+		getList() {
+			this.listLoading = true;
+			api.task
+				.getList(this.listQuery)
+				.then((res: any) => {
+					this.taskList = res.list || [];
+					this.total = res.Total;
+				})
+				.finally(() => {
+					this.listLoading = false;
+				});
+		},
+		handleDelete(row: any) {
+			this.$confirm('是否确认删除任务名称为"' + row.Job.title + '"的数据项?', '警告', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning',
+			})
+				.then(function () {
+					return api.task.deleteDeviceJob({ number: row.Job.number });
+				})
+				.then(() => {
+					this.handleFilter();
+					ElMessage.success('删除成功!');
+				})
+				.catch(function () { });
+		},
+		handleCreate() {
+			(this.$refs.taskDialog as any).openDialog({
+				dialogStatus: 'create',
+				deviceNumber: this.temp.number,
+			});
+		},
+		handleUpdate(row: any) {
+			(this.$refs.taskDialog as any).openDialog({
+				dialogStatus: 'update',
+				row,
+				deviceNumber: this.temp.number,
+			});
+		},
+		// 码流
+		initEv() {
+			if (this.evsrc) return;
+			// this.evsrc = new EventSource(`http://one.server.mydig.net/debug?number=${this.temp.number}`);
+			this.evsrc = new EventSource(`${getOrigin(import.meta.env.VITE_MODBUS_API)}/debug?number=${this.temp.number}`);
+			let that = this;
+			this.evsrc.onmessage = function (ev: any) {
+				try {
+					let obj = JSON.parse(ev.data);
+					if (obj.deviceId === that.temp.number) {
+						let color = obj.type === 'request' ? '#F56C6C' : '#409EFF';
+						let content = `${obj.type === 'request' ? '请求:' : '应答:'} ${obj.msg}`;
+						this.$refs.logContainer.insertAdjacentHTML('afterbegin', `<li style="color: ${color}">${content}</li>`);
+					}
+				} catch (_e) {
+					// @ts-ignore
+					// console.log('error', e);
+				}
+			};
+			// this.evsrc.onerror = function (_ev: any) {
+				// @ts-ignore
+				// console.log('readyState = ' + ev.currentTarget.readyState);
+			// };
+		},
+		openEv() {
+			(this.$refs.logContainer as any).insertAdjacentHTML('beforeEnd', `<li style="color: #000;">开始...</li>`);
+			this.initEv();
+		},
+		closeEv(log = true) {
+			this.evsrc && this.evsrc.close();
+			this.evsrc = null;
+			this.count = 0;
+			// this.clearLog()
+			if (log) {
+				(this.$refs.logContainer as any).insertAdjacentHTML('beforeEnd', `<li style="color: #000;">停止...</li>`);
+			}
+		},
+		// 清空码流
+		clearLog() {
+			(this.$refs.logContainer as any).innerHTML = '';
+		},
+		getCodingLabel(row: any) {
+			const item = this.formatOptions.find((item: any) => item.value === row.Job.encoding) as any;
+			return item.title;
+		},
+	},
+};
+</script>
+
+<style lang="scss" scoped>
+.filter-container {
+	margin-bottom: 10px;
+}
+
+.filter-item {
+	margin-right: 10px;
+}
+
+#logContainer {
+	height: 400px;
+	overflow: auto;
+	border: 1px solid #ddd;
+	padding: 10px;
+	padding-left: 20px;
+	border-radius: 8px;
+	margin-top: 10px;
+	word-wrap: break-word;
+	word-break: normal;
+	background-color: #f2f2f2;
+	line-height: 22px;
+}
+</style>

+ 80 - 0
src/views/iot/ice104/template/component/edit.vue

@@ -0,0 +1,80 @@
+<template>
+	<el-dialog title="添加设备通道" v-model="dialogVisible" width="600px" :before-close="clsoeDialog" :close-on-click-modal="false">
+		<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="120px" style="width: 90%; margin: 0 auto">
+			<el-form-item label="通道名称" prop="title">
+				<el-input v-model="temp.title" placeholder="请输入通道名称" />
+			</el-form-item>
+			<el-form-item label="注册码" prop="number">
+				<el-input v-model="temp.number" placeholder="请输入注册码" />
+			</el-form-item>
+			<el-form-item label="设备地址" prop="slaveId">
+				<el-input v-model.number="temp.slaveId" placeholder="请输入设备地址" />
+			</el-form-item>
+		</el-form>
+		<div slot="footer" class="dialog-footer">
+			<el-button @click="clsoeDialog()"> 取 消 </el-button>
+			<el-button type="primary" @click="createData()"> 保 存 </el-button>
+		</div>
+	</el-dialog>
+</template>
+<script lang="ts">
+import api from '/@/api/device/modbus';
+import { ElMessage } from 'element-plus';
+export default {
+	data() {
+		return {
+			temp: {
+				title: '',
+				number: '',
+				templateNumber: '',
+				slaveId: '',
+				interval: '',
+			},
+			rules: {
+				title: [{ required: true, message: '请输入通道名称', trigger: 'blur' }],
+				number: [{ required: true, message: '请输入注册码', trigger: 'blur' }],
+				slaveId: [{ required: true, message: '请输入设备地址', trigger: 'blur' }],
+			},
+			dialogVisible: false,
+			listLoading: false,
+			templateOptions: [],
+		};
+	},
+
+	methods: {
+		open() {
+			// this.getList();
+			this.dialogVisible = true;
+		},
+		clsoeDialog() {
+			(this.$refs.dataForm as any).resetFields();
+			this.dialogVisible = false;
+		},
+		// 获取模板数据
+		getList() {
+			this.listLoading = true;
+			api.channel
+				.getList({ page: 1, size: 50 })
+				.then((data: any) => {
+					this.templateOptions = data.list || [];
+				})
+				.finally(() => {
+					this.listLoading = false;
+				});
+		},
+		createData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					api.channel.addDevice(this.temp).then(() => {
+						this.$emit('getList');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+	},
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 139 - 0
src/views/iot/ice104/template/component/taskDialog.vue

@@ -0,0 +1,139 @@
+<template>
+	<el-dialog :title="textMap[dialogStatus]" v-model="dialogVisible" width="600px" :before-close="clsoeDialog" append-to-body :close-on-click-modal="false">
+		<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="120px" style="width: 400px; margin: 0 auto">
+			<el-form-item label="标题" prop="title">
+				<el-input v-model="temp.title" placeholder="请输入标题" />
+			</el-form-item>
+			<el-form-item label="调度周期" prop="interval">
+				<el-input v-model="temp.interval" placeholder="请输入调度周期" />
+			</el-form-item>
+			<el-form-item label="转发格式" prop="encoding">
+				<el-select v-model="temp.encoding" placeholder="请选择转发格式" style="width: 100%">
+					<el-option v-for="item in formatOptions" :key="item.value" :label="item.title" :value="item.value"> </el-option>
+				</el-select>
+				<!-- <el-input v-model="temp.encoding" placeholder="请输入转发格式"/> -->
+			</el-form-item>
+			<el-form-item label="mqtt主题" prop="publishTopic">
+				<el-input v-model="temp.publishTopic" placeholder="请输入mqtt主题" />
+			</el-form-item>
+			<el-form-item label="设备模板" prop="templateNumber">
+				<el-select v-model="temp.templateNumber" filterable placeholder="请选择设备模板" style="width: 100%">
+					<el-option v-for="item in templateOptions" :key="item.number" :label="item.title" :value="item.number"> </el-option>
+				</el-select>
+			</el-form-item>
+		</el-form>
+		<div slot="footer" class="dialog-footer">
+			<el-button @click="clsoeDialog"> 取 消 </el-button>
+			<el-button type="primary" @click="dialogStatus === 'create' ? createData() : updateData()"> 保 存 </el-button>
+		</div>
+	</el-dialog>
+</template>
+
+<script lang="ts">
+import api from '/@/api/device/modbus';
+import { ElMessage } from 'element-plus';
+
+export default {
+	components: {},
+	props: {
+		formatOptions: {
+			default: () => [],
+		},
+	},
+	data() {
+		return {
+			temp: {
+				title: '',
+				interval: '20s',
+				encoding: 'json',
+				publishTopic: '',
+				deviceNumber: '',
+				number: '',
+				templateNumber: '',
+			},
+			rules: {
+				title: [{ required: true, message: '请输入模板名称', trigger: 'change' }],
+				publishTopic: [{ required: true, message: '请输入mqtt主题', trigger: 'change' }],
+				mode: [{ required: true, message: '请选择模式', trigger: 'change' }],
+				templateNumber: [{ required: true, message: '请选择模式', trigger: 'change' }],
+			},
+			dialogVisible: false,
+			listLoading: false,
+			dialogStatus: '',
+			textMap: {
+				update: '任务详情',
+				create: '添加任务',
+			},
+			templateOptions: '',
+		};
+	},
+
+	methods: {
+		openDialog({ dialogStatus, row, deviceNumber }) {
+			this.dialogStatus = dialogStatus;
+			this.temp.deviceNumber = deviceNumber;
+			if (row) {
+				// this.temp = { ...row }
+				this.temp.number = row.Job.number;
+				this.temp.title = row.Job.title;
+				this.temp.interval = row.Job.interval;
+				this.temp.encoding = row.Job.encoding;
+				this.temp.publishTopic = row.Job.publishTopic;
+				this.temp.templateNumber = row?.Template?.number;
+			}
+			// this.getDict()
+			this.getTemplateList();
+			this.dialogVisible = true;
+		},
+		clsoeDialog() {
+			this.dialogVisible = false;
+			this.temp = {
+				title: '',
+				interval: '20s',
+				encoding: 'json',
+				publishTopic: '',
+				deviceNumber: '',
+				number: '',
+				templateNumber: '',
+			};
+			(this.$refs['dataForm'] as any).resetFields();
+		},
+		// 获取模板数据
+		getTemplateList() {
+			this.listLoading = true;
+			api.template
+				.getList({ page: 1, size: 50 })
+				.then((res: any) => {
+					this.templateOptions = res.list || [];
+				})
+				.finally(() => {
+					this.listLoading = false;
+				});
+		},
+		createData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					api.task.addDeviceJob(this.temp).then(() => {
+						this.$emit('finish');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+		updateData() {
+			(this.$refs['dataForm'] as any).validate((valid: boolean) => {
+				if (valid) {
+					api.task.editDeviceJob(this.temp).then(() => {
+						this.$emit('finish');
+						this.clsoeDialog();
+						ElMessage.success('操作成功!');
+					});
+				}
+			});
+		},
+	},
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 105 - 0
src/views/iot/ice104/template/index.vue

@@ -0,0 +1,105 @@
+<!--
+ * @Author: vera_min vera_min@163.com
+ * @Date: 2023-10-23 17:05:57
+ * @LastEditors: vera_min vera_min@163.com
+ * @LastEditTime: 2023-10-25 10:29:58
+ * @FilePath: /sagoo-admin-ui/src/views/iot/ice104/index.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+	<div class="page page-full">
+		<el-card shadow="hover" class="page-full-part">
+			<div class="search">
+				<el-form :model="params" :inline="true" ref="queryRef">
+					<el-form-item label="模版名称" prop="title">
+						<el-input v-model="params.title" placeholder="请输入模版名称" clearablestyle="width: 240px" @keyup.enter.native="getList(1)" />
+					</el-form-item>
+					<el-form-item>
+						<el-button size="default" type="primary" class="ml10" @click="getList(1)">
+							<el-icon>
+								<ele-Search />
+							</el-icon>
+							查询
+						</el-button>
+						<el-button size="default" @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>
+							新增
+						</el-button>
+					</el-form-item>
+				</el-form>
+			</div>
+			<el-table :data="tableData" style="width: 100%" v-loading="loading">
+				<el-table-column type="index" label="序号" width="80" align="center" />
+				<el-table-column prop="title" label="模版名称"  align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="number" label="模版编码" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column prop="status" label="状态" align="center" show-overflow-tooltip>
+					<template #default="scope">
+						<el-tag type="success" size="small" v-if="scope.row.status == 1">启用</el-tag>
+						<el-tag type="info" size="small" v-if="scope.row.status == 0">停用</el-tag>
+					</template>
+				</el-table-column>
+				<!-- <el-table-column prop="mode" label="模式" align="center" show-overflow-tooltip></el-table-column> -->
+				<el-table-column prop="remarks" label="备注" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column fixed="right" label="操作" width="100" align="center">
+					<template #default="scope">
+						<el-button size="small" text type="primary" @click="viewDetail(scope.row)">编辑</el-button>
+						<el-button size="small" text type="info" @click="onDel(scope.row)">删除</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+			<pagination v-if="params.total" :total="params.total" v-model:page="params.page" v-model:limit="params.size" @pagination="getList()" />
+		</el-card>
+		<EditForm ref="editFormRef" @getList="getList(1)"></EditForm>
+		<detailForm ref="detailFormRef" @getList="getList()"></detailForm>
+	</div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import EditForm from './component/edit.vue';
+import detailForm from './component/detail.vue';
+import api from '/@/api/ice104/index';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import { useSearch } from '/@/hooks/useCommonIce104';
+
+const editFormRef = ref();
+const detailFormRef = ref();
+const queryRef = ref();
+
+const { params, tableData, getList, loading } = useSearch(api.template.getList, 'data', { title: '' });
+
+getList();
+
+const addOrEdit = async (row?: any) => {
+	editFormRef.value.open(row);
+};
+const viewDetail = async (row: any) => {
+	detailFormRef.value.open(row);
+};
+
+// 重置表单
+const resetQuery = () => {
+	queryRef.value.resetFields();
+	getList(1);
+};
+
+const onDel = (row: any) => {
+	ElMessageBox.confirm(`此操作将删除接口:“${row.title}”,是否继续?`, '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+	}).then(async () => {
+		await api.channel.deleteDevice({ number: row.number });
+		ElMessage.success('删除成功');
+		getList(1);
+	});
+};
+</script>