Sfoglia il codice sorgente

@author liuqing
@commit 网关配置下发功能和日志功能

lq 2 mesi fa
parent
commit
070dbf15b9

+ 145 - 0
src/api/longchi/device/gatewayconfig.js

@@ -0,0 +1,145 @@
+import request from '@/router/axios';
+
+// 串口配置
+export const getSerialList = (gid) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/serial/list',
+    method: 'get',
+    params: { gid }
+  })
+}
+
+export const saveSerial = (params) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/serial/save',
+    method: 'post',
+    params
+  })
+}
+
+export const deleteSerial = (gid, comid) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/serial/delete',
+    method: 'post',
+    params: { gid, comid }
+  })
+}
+
+export const pushSerialConfig = (gid, tenant, data) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/serial/push',
+    method: 'post',
+    params: { gid, tenant },
+    data
+  })
+}
+
+// 设备配置
+export const getDevList = (gid, code) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/dev/list',
+    method: 'get',
+    params: { gid, code }
+  })
+}
+
+export const saveDev = (params) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/dev/save',
+    method: 'post',
+    params
+  })
+}
+
+export const deleteDev = (devcode) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/dev/delete',
+    method: 'post',
+    params: { devcode }
+  })
+}
+
+export const pushDevConfig = (gid, tenant, code, data) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/dev/push',
+    method: 'post',
+    params: { gid, tenant, code },
+    data
+  })
+}
+
+// 物模型配置
+export const getModelList = (gid) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/model/list',
+    method: 'get',
+    params: gid ? { gid } : {}
+  })
+}
+
+export const pushModelConfig = (gid, tenant, tid, data) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/model/push',
+    method: 'post',
+    params: { gid, tenant, tid },
+    data
+  })
+}
+
+export const saveModel = (params) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/model/save',
+    method: 'post',
+    params
+  })
+}
+
+export const deleteModel = (gid, tid) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/model/delete',
+    method: 'post',
+    params: { gid, tid }
+  })
+}
+
+// 日志
+export const queryLog = (gid, tenant) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/log/query',
+    method: 'post',
+    params: { gid, tenant }
+  })
+}
+
+export const getLogList = (gid, tenant) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/log/list',
+    method: 'get',
+    params: { gid, tenant }
+  })
+}
+
+export const readLog = (gid, tenant, file) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/log/read',
+    method: 'get',
+    params: { gid, tenant, file }
+  })
+}
+
+export const deleteLog = (gid, tenant) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/log/delete',
+    method: 'post',
+    params: { gid, tenant }
+  })
+}
+
+export const setLogCfg = (gid, tenant, data) => {
+  return request({
+    url: '/api/longchi/device/gatewayconfig/log/cfg',
+    method: 'post',
+    params: { gid, tenant },
+    data
+  })
+}

+ 654 - 0
src/views/longchi/device/GatewayConfigDialog.vue

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

+ 22 - 1
src/views/longchi/device/wisdomgateway.vue

@@ -40,6 +40,9 @@
                @size-change="sizeChange"
                @refresh-change="refreshChange"
                @on-load="onLoad">
+      <template slot="menu" slot-scope="{row}">
+        <el-button type="text" size="small" icon="el-icon-setting" @click="openGatewayConfig(row)">配置下发</el-button>
+      </template>
       <!-- <template slot="menuLeft">
         <el-button type="danger"
                    size="small"
@@ -453,6 +456,12 @@
         </template>
       </avue-form>
     </el-dialog>
+    <GatewayConfigDialog
+      :visible.sync="gatewayConfigVisible"
+      :gatewayId="configGatewayId"
+      :gatewayName="configGatewayName"
+      :tenant="configTenant"
+    />
   </basic-container>
 </template>
 
@@ -460,12 +469,14 @@
   import {getList, getDetail, add, update, remove, getLampPoleList, getRelevanceDetail, getTenantCode} from "@/api/longchi/device/wisdomgateway";
   import {mapGetters} from "vuex";
   import amapLong from '../amapLong2'
+  import GatewayConfigDialog from './GatewayConfigDialog.vue'
   import website from '@/config/website';
   import {getToken} from '@/util/auth';
   import {filterParams} from '@/util/validate'
   export default {
     components:{
-      amapLong
+      amapLong,
+      GatewayConfigDialog
     },
     data() {
       const validateIp = (rule, value, callback) => {
@@ -489,6 +500,10 @@
           region: ''
         },
         dialogVisible:false,
+        gatewayConfigVisible: false,
+        configGatewayId: '',
+        configGatewayName: '',
+        configTenant: '',
         cameraList:[],
         activeName: 'first',
         excelBox: false,
@@ -1078,6 +1093,12 @@
         })
         this.dialogVisible = true;
       },
+      openGatewayConfig(row) {
+        this.configGatewayId = row.gatewaySn;
+        this.configGatewayName = row.gatewayName;
+        this.configTenant = row.tenantCode || this.tenantId;
+        this.gatewayConfigVisible = true;
+      },
       getTenantCodeFun(){
        let _this = this;
        getTenantCode('ZHWG').then((res) =>{