|
|
@@ -0,0 +1,654 @@
|
|
|
+<template>
|
|
|
+ <el-dialog
|
|
|
+ :title="'网关 [' + gatewayName + '] 对接配置'"
|
|
|
+ :visible.sync="visible"
|
|
|
+ width="70%"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ append-to-body
|
|
|
+ @close="handleClose"
|
|
|
+ >
|
|
|
+ <el-tabs v-model="activeTab" type="card">
|
|
|
+ <!-- Tab 1: 串口配置 -->
|
|
|
+ <el-tab-pane label="串口配置" name="serial">
|
|
|
+ <div style="margin-bottom:10px">
|
|
|
+ <input type="file" ref="serialFile" accept=".json" style="display:none" @change="handleImportJSON('serial')" />
|
|
|
+ <el-button size="small" icon="el-icon-upload" @click="$refs.serialFile.click()">导入JSON</el-button>
|
|
|
+ <el-button type="primary" size="small" icon="el-icon-plus" @click="addSerial">新增串口</el-button>
|
|
|
+ <el-button type="success" size="small" icon="el-icon-upload2" @click="handlePushSerial" :loading="pushSerialLoading">下发串口配置</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="serialList" border stripe v-loading="serialLoading">
|
|
|
+ <el-table-column prop="ComID" label="编号" width="80" />
|
|
|
+ <el-table-column prop="Interface" label="接口位置" width="140" />
|
|
|
+ <el-table-column prop="Address" label="设备地址" width="160" />
|
|
|
+ <el-table-column prop="BaudRate" label="波特率" width="100" />
|
|
|
+ <el-table-column prop="DataBits" label="数据位" width="80" />
|
|
|
+ <el-table-column prop="StopBits" label="停止位" width="80" />
|
|
|
+ <el-table-column prop="Parity" label="校验位" width="80" />
|
|
|
+ <el-table-column prop="Timeout" label="超时(μs)" width="100" />
|
|
|
+ <el-table-column label="协议类型" width="120">
|
|
|
+ <template slot-scope="{row}">{{ protocolTypeMap[row.ProtocolType] || row.ProtocolType }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="160">
|
|
|
+ <template slot-scope="{row}">
|
|
|
+ <el-button type="text" size="small" @click="editSerial(row)">编辑</el-button>
|
|
|
+ <el-button type="text" size="small" style="color:#F56C6C" @click="handleDeleteSerial(row)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- Tab 2: 设备配置 -->
|
|
|
+ <el-tab-pane label="设备配置" name="dev">
|
|
|
+ <div style="margin-bottom:10px">
|
|
|
+ <input type="file" ref="devFile" accept=".json" style="display:none" @change="handleImportJSON('dev')" />
|
|
|
+ <el-button size="small" icon="el-icon-upload" @click="$refs.devFile.click()">导入JSON</el-button>
|
|
|
+ <span style="margin-right:8px;margin-left:8px">串口编号:</span>
|
|
|
+ <el-select v-model="devCode" placeholder="选择串口" @change="loadDevList" size="small" style="width:120px">
|
|
|
+ <el-option v-for="s in serialList" :key="s.ComID" :label="'串口'+s.ComID" :value="s.ComID" />
|
|
|
+ </el-select>
|
|
|
+ <el-button type="primary" size="small" icon="el-icon-plus" @click="addDev" style="margin-left:10px" :disabled="!devCode">新增设备</el-button>
|
|
|
+ <el-button type="success" size="small" icon="el-icon-upload2" @click="handlePushDev" :loading="pushDevLoading" :disabled="!devCode">下发设备配置</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="devList" border stripe v-loading="devLoading">
|
|
|
+ <el-table-column prop="ID" label="DevCode" width="160" />
|
|
|
+ <el-table-column prop="Name" label="名称" width="140" />
|
|
|
+ <el-table-column prop="RtuID" label="DevID" width="80" />
|
|
|
+ <el-table-column prop="TID" label="TID" width="80" />
|
|
|
+ <el-table-column prop="ProtocolType" label="协议类型" width="100">
|
|
|
+ <template slot-scope="{row}">{{ protocolTypeMap[row.ProtocolType] }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="DevType" label="设备类型" width="100">
|
|
|
+ <template slot-scope="{row}">{{ devTypeMap[row.DevType] }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="SendCloud" label="采集间隔" width="100" />
|
|
|
+ <el-table-column label="操作" width="120">
|
|
|
+ <template slot-scope="{row}">
|
|
|
+ <el-button type="text" size="small" @click="editDev(row)">编辑</el-button>
|
|
|
+ <el-button type="text" size="small" style="color:#F56C6C" @click="handleDeleteDev(row)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- Tab 3: 物模型 -->
|
|
|
+ <el-tab-pane label="物模型" name="model">
|
|
|
+ <div style="margin-bottom:10px">
|
|
|
+ <el-button type="primary" size="small" icon="el-icon-plus" @click="addModel">新增物模型</el-button>
|
|
|
+ <el-button type="success" size="small" icon="el-icon-upload2" @click="handlePushModel" :loading="pushModelLoading" :disabled="modelList.length===0">下发全部物模型</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="modelList" border stripe v-loading="modelLoading" @row-click="modelTid = $event.tid">
|
|
|
+ <el-table-column prop="tid" label="TID" width="80" />
|
|
|
+ <el-table-column prop="device" label="设备名称" width="200" />
|
|
|
+ <el-table-column prop="model" label="型号" width="200" />
|
|
|
+ <el-table-column prop="protocol" label="协议" width="140" />
|
|
|
+ <el-table-column label="操作" width="200">
|
|
|
+ <template slot-scope="{row}">
|
|
|
+ <el-button type="text" size="small" @click="editModel(row)">编辑</el-button>
|
|
|
+ <el-button type="text" size="small" style="color:#F56C6C" @click="handleDeleteModel(row)">删除</el-button>
|
|
|
+ <el-button v-if="row.file" type="text" size="small" @click="viewModelJSON(row)">查看JSON</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- Tab 4: 日志 -->
|
|
|
+ <el-tab-pane label="日志" name="log">
|
|
|
+ <div style="margin-bottom:10px">
|
|
|
+ <el-button type="primary" size="small" icon="el-icon-search" @click="handleQueryLog" :loading="logQueryLoading">查询日志</el-button>
|
|
|
+ <el-button size="small" icon="el-icon-refresh" @click="loadLogList">刷新</el-button>
|
|
|
+ <el-button type="danger" size="small" icon="el-icon-delete" @click="handleDeleteLog" :loading="logDeleteLoading">删除日志</el-button>
|
|
|
+ <el-divider direction="vertical" />
|
|
|
+ <span style="font-size:12px;color:#909399">日志等级:</span>
|
|
|
+ <el-select v-model="logCfgGlobal" size="small" style="width:90px;margin-left:6px" @change="logCfgDirty=true">
|
|
|
+ <el-option v-for="lv in ['debug','info','warn','error']" :key="lv" :label="lv" :value="lv" />
|
|
|
+ </el-select>
|
|
|
+ <el-button type="warning" size="small" :disabled="!logCfgDirty" :loading="logCfgLoading" @click="handleSetLogCfg" style="margin-left:8px">应用</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="logFiles" border stripe v-loading="logLoading" max-height="260">
|
|
|
+ <el-table-column prop="name" label="文件名" width="250" />
|
|
|
+ <el-table-column prop="size" label="大小" width="100">
|
|
|
+ <template slot-scope="{row}">{{ (row.size/1024).toFixed(1) }}KB</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="time" label="时间" width="180" />
|
|
|
+ <el-table-column label="操作" width="120">
|
|
|
+ <template slot-scope="{row}">
|
|
|
+ <el-button type="text" size="small" @click="handleReadLog(row)">查看</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 日志内容弹窗 -->
|
|
|
+ <el-dialog :title="'日志: ' + logFileName" :visible.sync="logViewVisible" width="85%" append-to-body @opened="onLogViewOpened">
|
|
|
+ <div style="margin-bottom:10px">
|
|
|
+ <span style="font-size:12px;color:#909399">级别:</span>
|
|
|
+ <el-select v-model="logFilterLevel" size="small" style="width:90px;margin:0 8px" @change="applyLogFilter" clearable placeholder="全部">
|
|
|
+ <el-option v-for="lv in ['debug','info','warn','error']" :key="lv" :label="lv" :value="lv" />
|
|
|
+ </el-select>
|
|
|
+ <span style="font-size:12px;color:#909399">tag:</span>
|
|
|
+ <el-select v-model="logFilterTag" size="small" style="width:110px;margin:0 8px" @change="applyLogFilter" clearable placeholder="全部">
|
|
|
+ <el-option v-for="t in logTags" :key="t" :label="t" :value="t" />
|
|
|
+ </el-select>
|
|
|
+ <el-button size="small" @click="logViewMode = logViewMode==='table'?'raw':'table'" style="float:right">
|
|
|
+ {{ logViewMode === 'table' ? 'JSON视图' : '表格视图' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <!-- 表格视图 -->
|
|
|
+ <el-table v-if="logViewMode==='table'" :data="logDisplayLines" border stripe size="small" max-height="450" :row-class-name="logRowClass">
|
|
|
+ <el-table-column prop="time" label="时间" width="100" />
|
|
|
+ <el-table-column prop="tag" label="Tag" width="110">
|
|
|
+ <template slot-scope="{row}"><el-tag :type="tagType(row.tag)" size="mini">{{ row.tag }}</el-tag></template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="level" label="级别" width="70">
|
|
|
+ <template slot-scope="{row}"><span :style="{color:levelColor(row.level),fontWeight:'bold',fontSize:'12px'}">{{ row.level }}</span></template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="msg" label="内容" show-overflow-tooltip />
|
|
|
+ </el-table>
|
|
|
+ <!-- 原始JSON视图 -->
|
|
|
+ <div v-else style="max-height:450px;overflow:auto;background:#1e1e1e;color:#d4d4d4;font-family:monospace;font-size:12px;padding:10px;white-space:pre-wrap;word-break:break-all">{{ logContent }}</div>
|
|
|
+ <div style="margin-top:8px;text-align:right;font-size:12px;color:#909399" v-if="logViewMode==='table'">
|
|
|
+ 共 {{ logFilteredCount }} 条 / 总计 {{ logParsedLines.length }} 条
|
|
|
+ <el-pagination v-if="logFilteredCount > 100" layout="prev,next" :page-size="100" :total="logFilteredCount" @current-change="v => logPage=v" style="display:inline-block;margin-left:10px" small />
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+
|
|
|
+ <!-- 串口编辑弹窗 -->
|
|
|
+ <el-dialog title="串口配置" :visible.sync="serialFormVisible" width="500px" append-to-body>
|
|
|
+ <el-form :model="serialForm" label-width="100px" size="small">
|
|
|
+ <el-form-item label="串口编号"><el-input-number v-model="serialForm.comid" :min="1" :max="32" :disabled="serialForm.isEdit" /></el-form-item>
|
|
|
+ <el-form-item label="接口位置"><el-input v-model="serialForm.interface" placeholder="如 RS485-1" /></el-form-item>
|
|
|
+ <el-form-item label="设备地址"><el-input v-model="serialForm.address" placeholder="如 /dev/ttymxc3" /></el-form-item>
|
|
|
+ <el-form-item label="波特率"><el-input-number v-model="serialForm.baudrate" :min="1200" :max="115200" step="100" /></el-form-item>
|
|
|
+ <el-form-item label="数据位"><el-input-number v-model="serialForm.databits" :min="5" :max="8" /></el-form-item>
|
|
|
+ <el-form-item label="停止位"><el-input-number v-model="serialForm.stopbits" :min="1" :max="2" /></el-form-item>
|
|
|
+ <el-form-item label="校验位"><el-select v-model="serialForm.parity"><el-option v-for="p in ['N','E','O']" :key="p" :label="p" :value="p" /></el-select></el-form-item>
|
|
|
+ <el-form-item label="超时(μs)"><el-input-number v-model="serialForm.timeout" :min="100" :max="10000" /></el-form-item>
|
|
|
+ <el-form-item label="协议类型"><el-select v-model="serialForm.protocoltype"><el-option v-for="(v,k) in protocolTypeMap" :key="k" :label="v" :value="Number(k)" /></el-select></el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button @click="serialFormVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleSaveSerial">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 设备编辑弹窗 -->
|
|
|
+ <el-dialog title="设备配置" :visible.sync="devFormVisible" width="500px" append-to-body>
|
|
|
+ <el-form :model="devForm" label-width="100px" size="small">
|
|
|
+ <el-form-item label="DevCode"><el-input v-model="devForm.devcode" :disabled="devForm.isEdit" /></el-form-item>
|
|
|
+ <el-form-item label="名称"><el-input v-model="devForm.name" /></el-form-item>
|
|
|
+ <el-form-item label="DevID"><el-input-number v-model="devForm.rtuid" :min="1" :max="500" /></el-form-item>
|
|
|
+ <el-form-item label="TID"><el-input-number v-model="devForm.tid" :min="1" /></el-form-item>
|
|
|
+ <el-form-item label="协议类型"><el-select v-model="devForm.protocoltype"><el-option v-for="(v,k) in protocolTypeMap" :key="k" :label="v" :value="Number(k)" /></el-select></el-form-item>
|
|
|
+ <el-form-item label="设备类型"><el-select v-model="devForm.devtype"><el-option v-for="(v,k) in devTypeMap" :key="k" :label="v" :value="Number(k)" /></el-select></el-form-item>
|
|
|
+ <el-form-item label="采集间隔(ms)"><el-input-number v-model="devForm.sendcloud" :min="1000" :max="300000" step="1000" /></el-form-item>
|
|
|
+ <el-form-item label="WaitTime(ms)"><el-input-number v-model="devForm.waittime" :min="0" :max="10000" /></el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button @click="devFormVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleSaveDev">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 物模型编辑弹窗 -->
|
|
|
+ <el-dialog title="物模型配置" :visible.sync="modelFormVisible" width="500px" append-to-body>
|
|
|
+ <el-form :model="modelForm" label-width="100px" size="small">
|
|
|
+ <el-form-item label="TID"><el-input-number v-model="modelForm.tid" :min="1" :disabled="modelForm.isEdit" /></el-form-item>
|
|
|
+ <el-form-item label="设备名称"><el-input v-model="modelForm.device" /></el-form-item>
|
|
|
+ <el-form-item label="型号"><el-input v-model="modelForm.modelName" /></el-form-item>
|
|
|
+ <el-form-item label="协议类型"><el-select v-model="modelForm.protocol"><el-option v-for="p in ['ModbusRTU','yuming485','zigbee']" :key="p" :label="p" :value="p" /></el-select></el-form-item>
|
|
|
+ <el-form-item label="JSON文件">
|
|
|
+ <input type="file" ref="modelFormFile" accept=".json" style="display:none" @change="onModelFileChange" />
|
|
|
+ <el-button size="small" @click="$refs.modelFormFile.click()">选择文件</el-button>
|
|
|
+ <span v-if="modelForm.file" style="margin-left:8px;color:#67C23A">已选择</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button @click="modelFormVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleSaveModel">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </el-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import {
|
|
|
+ getSerialList, saveSerial, deleteSerial, pushSerialConfig,
|
|
|
+ getDevList, saveDev, deleteDev, pushDevConfig,
|
|
|
+ getModelList, pushModelConfig,
|
|
|
+ queryLog, getLogList, readLog, deleteLog, setLogCfg
|
|
|
+} from '@/api/longchi/device/gatewayconfig';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'GatewayConfigDialog',
|
|
|
+ props: {
|
|
|
+ visible: { type: Boolean, default: false },
|
|
|
+ gatewayId: { type: String, default: '' },
|
|
|
+ gatewayName: { type: String, default: '' },
|
|
|
+ tenant: { type: String, default: '' }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ activeTab: 'serial',
|
|
|
+ // Serial
|
|
|
+ serialList: [], serialLoading: false, pushSerialLoading: false,
|
|
|
+ serialFormVisible: false,
|
|
|
+ serialForm: { isEdit: false, comid: null, interface: '', address: '', baudrate: 9600, databits: 8, stopbits: 1, parity: 'N', timeout: 2000, protocoltype: 0 },
|
|
|
+ // Dev
|
|
|
+ devCode: null, devList: [], devLoading: false, pushDevLoading: false,
|
|
|
+ devFormVisible: false,
|
|
|
+ devForm: { isEdit: false, devcode: '', name: '', comid: null, rtuid: 1, tid: 1, protocoltype: 0, devtype: 2, sendcloud: 5000, waittime: 200 },
|
|
|
+ // Model
|
|
|
+ modelTid: null, modelList: [], modelLoading: false, pushModelLoading: false,
|
|
|
+ modelFormVisible: false, modelForm: { isEdit: false, tid: null, device: '', modelName: '', protocol: 'ModbusRTU', file: '' },
|
|
|
+ // Maps
|
|
|
+ protocolTypeMap: { 0: 'ModbusRTU', 1: 'Zigbee集中器', 2: '裕明485灯控' },
|
|
|
+ devTypeMap: { 1: '灯控类', 2: '环境监测', 3: '裕明485灯控', 4: '液位计', 5: '路面状况传感器' },
|
|
|
+ // Log
|
|
|
+ logFiles: [], logLoading: false, logQueryLoading: false, logDeleteLoading: false, logCfgLoading: false,
|
|
|
+ logViewVisible: false, logContent: '', logFileName: '', logViewMode: 'table',
|
|
|
+ logFilterLevel: '', logFilterTag: '', logPage: 1,
|
|
|
+ logParsedLines: [], logTags: [],
|
|
|
+ logCfgGlobal: 'debug', logCfgDirty: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ visible(val) {
|
|
|
+ if (val) {
|
|
|
+ this.loadSerialList();
|
|
|
+ this.loadModelList();
|
|
|
+ this.loadLogList();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ logFilteredLines() {
|
|
|
+ let lines = this.logParsedLines;
|
|
|
+ if (this.logFilterLevel) {
|
|
|
+ lines = lines.filter(l => l.level === this.logFilterLevel.toUpperCase());
|
|
|
+ }
|
|
|
+ if (this.logFilterTag) {
|
|
|
+ lines = lines.filter(l => l.tag === this.logFilterTag);
|
|
|
+ }
|
|
|
+ return lines;
|
|
|
+ },
|
|
|
+ logFilteredCount() {
|
|
|
+ return this.logFilteredLines.length;
|
|
|
+ },
|
|
|
+ logDisplayLines() {
|
|
|
+ const start = (this.logPage - 1) * 100;
|
|
|
+ return this.logFilteredLines.slice(start, start + 100);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ handleClose() {
|
|
|
+ this.$emit('update:visible', false);
|
|
|
+ },
|
|
|
+ ok(res, successMsg) {
|
|
|
+ if (res.data && res.data.code === 0) {
|
|
|
+ if (successMsg) this.$message.success(successMsg);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ this.$message.error(res.data ? res.data.msg : '请求失败');
|
|
|
+ return false;
|
|
|
+ },
|
|
|
+ buildSerialJSON() {
|
|
|
+ const serial = {};
|
|
|
+ this.serialList.forEach(s => {
|
|
|
+ serial[s.ComID] = {
|
|
|
+ Code: s.ComID, Interface: s.Interface, Address: s.Address,
|
|
|
+ BaudRate: s.BaudRate, DataBits: s.DataBits, StopBits: s.StopBits,
|
|
|
+ Parity: s.Parity, Timeout: s.Timeout, ProtocolType: s.ProtocolType || 0
|
|
|
+ };
|
|
|
+ });
|
|
|
+ return { serial };
|
|
|
+ },
|
|
|
+ buildDevJSON() {
|
|
|
+ const dev = this.devList.map(d => ({
|
|
|
+ Code: d.ComID, DevID: d.RtuID, DevCode: d.ID, TID: d.TID,
|
|
|
+ Name: d.Name, SendCloud: d.SendCloud, WaitTime: d.WaitTime,
|
|
|
+ ProtocolType: d.ProtocolType, DevType: d.DevType
|
|
|
+ }));
|
|
|
+ return { dev };
|
|
|
+ },
|
|
|
+ handleImportJSON(type) {
|
|
|
+ const refMap = { serial: 'serialFile', dev: 'devFile', model: 'modelFile' };
|
|
|
+ const file = this.$refs[refMap[type]].files[0];
|
|
|
+ if (!file) return;
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = (e) => {
|
|
|
+ try {
|
|
|
+ const json = JSON.parse(e.target.result);
|
|
|
+ if (type === 'serial') {
|
|
|
+ // serial.json 格式: {"serial":{"1":{...},"2":{...}}}
|
|
|
+ const serialMap = json.serial || json;
|
|
|
+ const list = [];
|
|
|
+ Object.keys(serialMap).forEach(code => {
|
|
|
+ const s = serialMap[code];
|
|
|
+ list.push({
|
|
|
+ ComID: s.Code || Number(code),
|
|
|
+ Interface: s.Interface || '',
|
|
|
+ Address: s.Address || '',
|
|
|
+ BaudRate: s.BaudRate || 9600,
|
|
|
+ DataBits: s.DataBits || 8,
|
|
|
+ StopBits: s.StopBits || 1,
|
|
|
+ Parity: s.Parity || 'N',
|
|
|
+ Timeout: s.Timeout || 2000,
|
|
|
+ ProtocolType: s.ProtocolType || 0
|
|
|
+ });
|
|
|
+ });
|
|
|
+ this.serialList = list;
|
|
|
+ this.$message.success(`已导入 ${list.length} 个串口配置,请逐条保存`);
|
|
|
+ } else if (type === 'dev') {
|
|
|
+ // dev/{code}.json 格式: {"dev":[{...}]}
|
|
|
+ const devArr = json.dev || json;
|
|
|
+ const list = (Array.isArray(devArr) ? devArr : []).map(d => ({
|
|
|
+ ID: d.DevCode || '',
|
|
|
+ Name: d.Name || '',
|
|
|
+ ComID: d.Code || 0,
|
|
|
+ RtuID: d.DevID || 0,
|
|
|
+ TID: d.TID || 0,
|
|
|
+ ProtocolType: d.ProtocolType || 0,
|
|
|
+ DevType: d.DevType || 2,
|
|
|
+ SendCloud: d.SendCloud || 5000,
|
|
|
+ WaitTime: d.WaitTime || 200
|
|
|
+ }));
|
|
|
+ this.devList = list;
|
|
|
+ this.$message.success(`已导入 ${list.length} 个设备配置,请逐条保存`);
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ this.$message.error('JSON 解析失败: ' + err.message);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ reader.readAsText(file);
|
|
|
+ // 清除 input 以便重复选同一个文件
|
|
|
+ this.$refs[refMap[type]].value = '';
|
|
|
+ },
|
|
|
+ // ===== Serial =====
|
|
|
+ loadSerialList() {
|
|
|
+ this.serialLoading = true;
|
|
|
+ getSerialList(this.gatewayId).then(res => {
|
|
|
+ this.serialList = res.data.data || [];
|
|
|
+ }).finally(() => { this.serialLoading = false; });
|
|
|
+ },
|
|
|
+ addSerial() {
|
|
|
+ this.serialForm = { isEdit: false, comid: null, interface: '', address: '', baudrate: 9600, databits: 8, stopbits: 1, parity: 'N', timeout: 2000, protocoltype: 0 };
|
|
|
+ this.serialFormVisible = true;
|
|
|
+ },
|
|
|
+ editSerial(row) {
|
|
|
+ this.serialForm = {
|
|
|
+ isEdit: true, comid: row.ComID, interface: row.Interface, address: row.Address,
|
|
|
+ baudrate: row.BaudRate, databits: row.DataBits, stopbits: row.StopBits,
|
|
|
+ parity: row.Parity, timeout: row.Timeout, protocoltype: row.ProtocolType || 0
|
|
|
+ };
|
|
|
+ this.serialFormVisible = true;
|
|
|
+ },
|
|
|
+ handleSaveSerial() {
|
|
|
+ const f = this.serialForm;
|
|
|
+ saveSerial({ gid: this.gatewayId, comid: f.comid, interface: f.interface, address: f.address,
|
|
|
+ baudrate: f.baudrate, databits: f.databits, stopbits: f.stopbits, parity: f.parity, timeout: f.timeout, protocoltype: f.protocoltype
|
|
|
+ }).then(res => {
|
|
|
+ if (this.ok(res, '保存成功')) {
|
|
|
+ this.serialFormVisible = false;
|
|
|
+ this.loadSerialList();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ handleDeleteSerial(row) {
|
|
|
+ this.$confirm('确认删除串口 ' + row.ComID + ' 的配置?', '提示', { type: 'warning' }).then(() => {
|
|
|
+ deleteSerial(this.gatewayId, row.ComID).then(res => {
|
|
|
+ if (this.ok(res, '删除成功')) this.loadSerialList();
|
|
|
+ });
|
|
|
+ }).catch(() => {});
|
|
|
+ },
|
|
|
+ handlePushSerial() {
|
|
|
+ if (this.serialList.length === 0) { this.$message.warning('无串口配置可下发'); return; }
|
|
|
+ this.$confirm('确认下发串口配置到网关?', '提示', { type: 'info' }).then(() => {
|
|
|
+ this.pushSerialLoading = true;
|
|
|
+ pushSerialConfig(this.gatewayId, (this.tenant || '000000'), this.buildSerialJSON()).then(res => {
|
|
|
+ this.ok(res, '串口配置已下发');
|
|
|
+ }).finally(() => { this.pushSerialLoading = false; });
|
|
|
+ }).catch(() => {});
|
|
|
+ },
|
|
|
+ // ===== Dev =====
|
|
|
+ loadDevList() {
|
|
|
+ if (!this.devCode) { this.devList = []; return; }
|
|
|
+ this.devLoading = true;
|
|
|
+ getDevList(this.gatewayId, this.devCode).then(res => {
|
|
|
+ this.devList = res.data.data || [];
|
|
|
+ }).finally(() => { this.devLoading = false; });
|
|
|
+ },
|
|
|
+ addDev() {
|
|
|
+ this.devForm = { isEdit: false, devcode: '', name: '', comid: this.devCode, rtuid: 1, tid: 1, protocoltype: 0, devtype: 2, sendcloud: 5000, waittime: 200 };
|
|
|
+ this.devFormVisible = true;
|
|
|
+ },
|
|
|
+ editDev(row) {
|
|
|
+ this.devForm = {
|
|
|
+ isEdit: true, devcode: row.ID, name: row.Name, comid: row.ComID, rtuid: row.RtuID,
|
|
|
+ tid: row.TID, protocoltype: row.ProtocolType, devtype: row.DevType,
|
|
|
+ sendcloud: row.SendCloud, waittime: row.WaitTime
|
|
|
+ };
|
|
|
+ this.devFormVisible = true;
|
|
|
+ },
|
|
|
+ handleSaveDev() {
|
|
|
+ const f = this.devForm;
|
|
|
+ saveDev({ gid: this.gatewayId, tenant: (this.tenant || '000000'), devcode: f.devcode, name: f.name,
|
|
|
+ comid: f.comid, rtuid: f.rtuid, tid: f.tid, protocoltype: f.protocoltype,
|
|
|
+ devtype: f.devtype, sendcloud: f.sendcloud, waittime: f.waittime
|
|
|
+ }).then(res => {
|
|
|
+ if (this.ok(res, '保存成功')) {
|
|
|
+ this.devFormVisible = false;
|
|
|
+ this.loadDevList();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ handleDeleteDev(row) {
|
|
|
+ this.$confirm('确认删除设备 ' + row.ID + '?', '提示', { type: 'warning' }).then(() => {
|
|
|
+ deleteDev(row.ID).then(res => {
|
|
|
+ if (this.ok(res, '删除成功')) this.loadDevList();
|
|
|
+ });
|
|
|
+ }).catch(() => {});
|
|
|
+ },
|
|
|
+ handlePushDev() {
|
|
|
+ if (!this.devCode) { this.$message.warning('请选择串口'); return; }
|
|
|
+ this.$confirm('确认下发串口 ' + this.devCode + ' 的设备配置?', '提示', { type: 'info' }).then(() => {
|
|
|
+ this.pushDevLoading = true;
|
|
|
+ pushDevConfig(this.gatewayId, (this.tenant || '000000'), this.devCode, this.buildDevJSON()).then(res => {
|
|
|
+ this.ok(res, '设备配置已下发');
|
|
|
+ }).finally(() => { this.pushDevLoading = false; });
|
|
|
+ }).catch(() => {});
|
|
|
+ },
|
|
|
+ // ===== Model =====
|
|
|
+ loadModelList() {
|
|
|
+ this.modelLoading = true;
|
|
|
+ getModelList(this.gatewayId).then(res => {
|
|
|
+ this.modelList = (res.data.data || []).map(m => ({
|
|
|
+ ...m, file: m.file || ''
|
|
|
+ }));
|
|
|
+ }).finally(() => { this.modelLoading = false; });
|
|
|
+ },
|
|
|
+ viewModelJSON(row) {
|
|
|
+ if (!row.file) return;
|
|
|
+ const blob = new Blob([row.file], { type: 'application/json' });
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+ window.open(url);
|
|
|
+ },
|
|
|
+ // ===== Log =====
|
|
|
+ loadLogList() {
|
|
|
+ this.logLoading = true;
|
|
|
+ getLogList(this.gatewayId, (this.tenant || '000000')).then(res => {
|
|
|
+ this.logFiles = res.data.data || [];
|
|
|
+ }).finally(() => { this.logLoading = false; });
|
|
|
+ },
|
|
|
+ handleQueryLog() {
|
|
|
+ this.logQueryLoading = true;
|
|
|
+ queryLog(this.gatewayId, (this.tenant || '000000')).then(res => {
|
|
|
+ if (this.ok(res)) {
|
|
|
+ this.$message.info('查询指令已发送,等待边缘响应...');
|
|
|
+ setTimeout(() => this.loadLogList(), 3000);
|
|
|
+ }
|
|
|
+ }).finally(() => { this.logQueryLoading = false; });
|
|
|
+ },
|
|
|
+ handleDeleteLog() {
|
|
|
+ this.$confirm('确认删除网关 [' + this.gatewayName + '] 的所有日志文件?', '提示', { type: 'warning' }).then(() => {
|
|
|
+ this.logDeleteLoading = true;
|
|
|
+ deleteLog(this.gatewayId, (this.tenant || '000000')).then(res => {
|
|
|
+ if (this.ok(res, '日志删除指令已发送')) {
|
|
|
+ setTimeout(() => this.loadLogList(), 3000);
|
|
|
+ }
|
|
|
+ }).finally(() => { this.logDeleteLoading = false; });
|
|
|
+ }).catch(() => {});
|
|
|
+ },
|
|
|
+ handleReadLog(row) {
|
|
|
+ this.logFileName = row.name;
|
|
|
+ this.logViewVisible = true;
|
|
|
+ this.logViewMode = 'table';
|
|
|
+ this.logFilterLevel = '';
|
|
|
+ this.logFilterTag = '';
|
|
|
+ this.logPage = 1;
|
|
|
+ this.logContent = '加载中...';
|
|
|
+ readLog(this.gatewayId, (this.tenant || '000000'), row.name).then(res => {
|
|
|
+ this.logContent = res.data.data || '(空)';
|
|
|
+ this.parseLogLines();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ parseLogLines() {
|
|
|
+ const lines = [];
|
|
|
+ const tags = new Set();
|
|
|
+ this.logContent.split('\n').forEach(line => {
|
|
|
+ line = line.trim();
|
|
|
+ if (!line) return;
|
|
|
+ try {
|
|
|
+ const obj = JSON.parse(line);
|
|
|
+ tags.add(obj.tag || '');
|
|
|
+ lines.push({
|
|
|
+ time: (obj.time || '').slice(-8) || '',
|
|
|
+ tag: obj.tag || '',
|
|
|
+ level: (obj.level || 'INFO').toUpperCase(),
|
|
|
+ msg: obj.msg || line
|
|
|
+ });
|
|
|
+ } catch (e) {
|
|
|
+ tags.add('raw');
|
|
|
+ lines.push({ time: '', tag: 'raw', level: '', msg: line });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ this.logParsedLines = lines;
|
|
|
+ this.logTags = Array.from(tags).filter(Boolean).sort();
|
|
|
+ this.logFilteredCount = lines.length;
|
|
|
+ },
|
|
|
+ applyLogFilter() {
|
|
|
+ this.logPage = 1;
|
|
|
+ },
|
|
|
+ onLogViewOpened() {
|
|
|
+ // already parsed in handleReadLog
|
|
|
+ },
|
|
|
+ logRowClass({ row }) {
|
|
|
+ if (row.level === 'ERROR') return 'log-row-error';
|
|
|
+ if (row.level === 'WARN' || row.level === 'WARNING') return 'log-row-warn';
|
|
|
+ if (row.level === 'DEBUG') return 'log-row-debug';
|
|
|
+ return '';
|
|
|
+ },
|
|
|
+ tagType(tag) {
|
|
|
+ const map = { modbus: '', ym485: 'success', concentrator: 'warning', radar: 'danger', sys: 'info' };
|
|
|
+ return map[tag] || '';
|
|
|
+ },
|
|
|
+ levelColor(level) {
|
|
|
+ const map = { ERROR: '#F56C6C', WARN: '#E6A23C', WARNING: '#E6A23C', INFO: '#409EFF', DEBUG: '#909399' };
|
|
|
+ return map[level] || '#909399';
|
|
|
+ },
|
|
|
+ handleSetLogCfg() {
|
|
|
+ this.logCfgLoading = true;
|
|
|
+ const data = { level: this.logCfgGlobal };
|
|
|
+ setLogCfg(this.gatewayId, (this.tenant || '000000'), data).then(res => {
|
|
|
+ if (this.ok(res, '日志等级配置已下发')) {
|
|
|
+ this.logCfgDirty = false;
|
|
|
+ }
|
|
|
+ }).finally(() => { this.logCfgLoading = false; });
|
|
|
+ },
|
|
|
+ addModel() {
|
|
|
+ this.modelForm = { isEdit: false, tid: null, device: '', modelName: '', protocol: 'ModbusRTU', file: '' };
|
|
|
+ this.modelFormVisible = true;
|
|
|
+ },
|
|
|
+ editModel(row) {
|
|
|
+ this.modelForm = {
|
|
|
+ isEdit: true, tid: row.tid, device: row.device, modelName: row.model,
|
|
|
+ protocol: row.protocol, file: row.file || ''
|
|
|
+ };
|
|
|
+ this.modelFormVisible = true;
|
|
|
+ },
|
|
|
+ onModelFileChange(e) {
|
|
|
+ const file = e.target.files[0];
|
|
|
+ if (!file) return;
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = (ev) => {
|
|
|
+ try {
|
|
|
+ const json = JSON.parse(ev.target.result);
|
|
|
+ this.modelForm.device = json.Device || json.device || this.modelForm.device;
|
|
|
+ this.modelForm.modelName = json.Model || json.model || this.modelForm.modelName;
|
|
|
+ this.modelForm.protocol = json.Protocol || json.protocol || this.modelForm.protocol;
|
|
|
+ this.modelForm.file = ev.target.result;
|
|
|
+ this.$message.success(`JSON已加载: ${this.modelForm.device}(${this.modelForm.protocol})`);
|
|
|
+ } catch (err) {
|
|
|
+ this.$message.error('JSON解析失败: ' + err.message);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ reader.readAsText(file);
|
|
|
+ this.$refs.modelFormFile.value = '';
|
|
|
+ },
|
|
|
+ handleSaveModel() {
|
|
|
+ const f = this.modelForm;
|
|
|
+ if (!f.file) {
|
|
|
+ this.$message.warning('请先选择JSON文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const exists = this.modelList.findIndex(x => x.tid === f.tid);
|
|
|
+ const item = { tid: f.tid, device: f.device, model: f.modelName, protocol: f.protocol, file: f.file };
|
|
|
+ if (f.isEdit && exists >= 0) {
|
|
|
+ this.$set(this.modelList, exists, item);
|
|
|
+ } else {
|
|
|
+ this.modelList.push(item);
|
|
|
+ }
|
|
|
+ this.modelFormVisible = false;
|
|
|
+ this.$message.success(`已更新TID=${f.tid}到列表(file=${f.file.length}字节),请点击「下发全部物模型」推送到网关`);
|
|
|
+ },
|
|
|
+ handleDeleteModel(row) {
|
|
|
+ this.$confirm('确认删除物模型 TID=' + row.tid + '?', '提示', { type: 'warning' }).then(() => {
|
|
|
+ const idx = this.modelList.findIndex(x => x.tid === row.tid);
|
|
|
+ if (idx >= 0) this.modelList.splice(idx, 1);
|
|
|
+ this.$message.success('已从列表删除,请点击「下发全部物模型」同步到网关');
|
|
|
+ }).catch(() => {});
|
|
|
+ },
|
|
|
+ handlePushModel() {
|
|
|
+ if (this.modelList.length === 0) { this.$message.warning('无物模型可下发'); return; }
|
|
|
+ const missing = this.modelList.filter(m => !m.file);
|
|
|
+ if (missing.length > 0) {
|
|
|
+ this.$message.error(`以下物模型缺少JSON文件内容: TID=${missing.map(m=>m.tid).join(',')},请编辑并选择JSON文件`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$confirm('确认下发全部物模型到网关?', '提示', { type: 'info' }).then(() => {
|
|
|
+ this.pushModelLoading = true;
|
|
|
+ let cnt = 0; let lastErr = null;
|
|
|
+ const pushOne = () => {
|
|
|
+ const m = this.modelList[cnt];
|
|
|
+ pushModelConfig(this.gatewayId, (this.tenant || '000000'), m.tid, m.file).then(res => {
|
|
|
+ if (!this.ok(res)) lastErr = res;
|
|
|
+ }).finally(() => {
|
|
|
+ cnt++;
|
|
|
+ if (cnt < this.modelList.length) pushOne();
|
|
|
+ else {
|
|
|
+ this.pushModelLoading = false;
|
|
|
+ if (!lastErr) this.$message.success('全部物模型已下发');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+ pushOne();
|
|
|
+ }).catch(() => {});
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+<style scoped>
|
|
|
+.log-row-error >>> td { background-color: #fef0f0 !important; }
|
|
|
+.log-row-warn >>> td { background-color: #fdf6ec !important; }
|
|
|
+.log-row-debug >>> td { background-color: #f4f4f5 !important; color: #909399 !important; }
|
|
|
+</style>
|