浏览代码

Merge branch 'dev' into test

liujt 3 月之前
父节点
当前提交
ad49d19082
共有 34 个文件被更改,包括 4890 次插入692 次删除
  1. 131 0
      src/api/ledgerAssets/robot.js
  2. 11 0
      src/api/main/index.js
  3. 二进制
      src/assets/pcs/back.png
  4. 二进制
      src/assets/pcs/faultDevice.png
  5. 二进制
      src/assets/pcs/header.png
  6. 二进制
      src/assets/pcs/home.png
  7. 二进制
      src/assets/pcs/icon.png
  8. 二进制
      src/assets/pcs/leftItem.png
  9. 二进制
      src/assets/pcs/runDevice.png
  10. 二进制
      src/assets/pcs/select.png
  11. 二进制
      src/assets/pcs/successInfo.png
  12. 二进制
      src/assets/pcs/titleIcon.png
  13. 二进制
      src/assets/pcs/titleb.png
  14. 6 1
      src/enum/dict.js
  15. 696 0
      src/views/home/common.vue
  16. 100 0
      src/views/home/components/StatCard.vue
  17. 57 0
      src/views/home/components/TrendChart.vue
  18. 13 685
      src/views/home/index.vue
  19. 844 0
      src/views/home/yuxin.vue
  20. 6 6
      src/views/ledgerAssets/camera/index.vue
  21. 221 0
      src/views/ledgerAssets/robot/components/detail.vue
  22. 522 0
      src/views/ledgerAssets/robot/components/formModel.vue
  23. 133 0
      src/views/ledgerAssets/robot/components/presetInfo/detail.vue
  24. 226 0
      src/views/ledgerAssets/robot/components/presetInfo/formModel.vue
  25. 170 0
      src/views/ledgerAssets/robot/components/presetInfo/index.vue
  26. 44 0
      src/views/ledgerAssets/robot/components/robot-search.vue
  27. 296 0
      src/views/ledgerAssets/robot/index.vue
  28. 167 0
      src/views/ledgerAssets/subDevice/components/detail.vue
  29. 349 0
      src/views/ledgerAssets/subDevice/components/formModel.vue
  30. 133 0
      src/views/ledgerAssets/subDevice/components/presetInfo/detail.vue
  31. 226 0
      src/views/ledgerAssets/subDevice/components/presetInfo/formModel.vue
  32. 193 0
      src/views/ledgerAssets/subDevice/components/presetInfo/index.vue
  33. 50 0
      src/views/ledgerAssets/subDevice/components/subdevice-search.vue
  34. 296 0
      src/views/ledgerAssets/subDevice/index.vue

+ 131 - 0
src/api/ledgerAssets/robot.js

@@ -0,0 +1,131 @@
+import request from '@/utils/request'
+
+// 获得设备管理分页
+export async function getDevicePage(params) {
+  const res = await request.get('/main/basic/device/pageRobert', { params })
+  if (res.data.code == 0) {
+    return res.data
+  }
+  return Promise.reject(new Error(res.data.message))
+}
+
+// 获得其他设备管理列表
+export async function getOtherDeviceList(params) {
+  const res = await request.get('/isp/device/listOtherDevice', { params })
+  if (res.data.code == 0) {
+    return res.data.data
+  }
+  return Promise.reject(new Error(res.data.message))
+}
+
+// 获得其他未绑定的设备列表
+export async function getNonLoadDeviceList(params) {
+  const res = await request.get('/isp/device/listOtherNonLoadDevice', { params })
+  if (res.data.code == 0) {
+    return res.data.data
+  }
+  return Promise.reject(new Error(res.data.message))
+}
+
+// 获得设备管理列表
+export async function getDeviceList(params) {
+  const res = await request.get('/isp/device/list', { params })
+  if (res.data.code == 0) {
+    return res.data.data
+  }
+  return Promise.reject(new Error(res.data.message))
+}
+
+// 获得设备管理
+export async function getDevice(id) {
+  const res = await request.get('/isp/device/get?id=' + id)
+  if (res.data.code == 0) {
+    return res.data.data
+  }
+  return Promise.reject(new Error(res.data.message))
+}
+
+// 摄像头截图
+export async function catchPic(code) {
+  const res = await request.get({
+    url: '/isp/camera/catchPicBin?cameraCode=' + code,
+    responseType: 'blob',
+    returnType: true
+  })
+  if (res.data.code == 0) {
+    return res.data.data
+  }
+  return Promise.reject(new Error(res.data.message))
+}
+
+// 摄像头开关灯
+export async function playControl(params) {
+  const res = await request.get('/isp/camera/playControl', { params })
+  if (res.data.code == 0) {
+    return res.data.data
+  }
+  return Promise.reject(new Error(res.data.message))
+}
+
+// 创建设备管理
+export async function createDevice(data) {
+  const res = await request.post('/isp/device/create', data)
+  if (res.data.code == 0) {
+    return res.data.data
+  }
+  return Promise.reject(new Error(res.data.message))
+}
+
+// 更新设备管理
+export async function updateDevice(data) {
+  const res = await request.put('/isp/device/update', data)
+  if (res.data.code == 0) {
+    return res.data.data
+  }
+  return Promise.reject(new Error(res.data.message))
+}
+
+// 删除设备管理
+export async function deletedDevice(id) {
+  const res = await request.delete('/isp/device/delete?ids=' + id)
+  if (res.data.code == 0) {
+    return res.data.data
+  }
+  return Promise.reject(new Error(res.data.message))
+}
+
+// 导出设备管理 Excel
+export async function exportDevice(params) {
+  const res = await request.download('/isp/device/export-excel', { params })
+  if (res) {
+    return res
+  }
+  return Promise.reject(new Error(res.message))
+}
+
+// 机器人运动控制
+export async function robotCommandControl(data) {
+  const res = await request.post('/isp/robot/Command/CommandControl', data)
+  if (res.data.code == 0) {
+    return res.data.data
+  }
+  return Promise.reject(new Error(res.data.message))
+}
+
+// 机器人运动暂停
+export async function robotStop(data) {
+  const res = await request.post('/isp/robot/Command/Stop', data)
+  if (res.data.code == 0) {
+    return res.data.data
+  }
+  return Promise.reject(new Error(res.data.message))
+}
+
+// 机器人状态查询
+export async function GetWorkState(data) {
+  const res = await request.post('/isp/robot/Command/GetWorkState', data)
+  if (res.data.code == 0) {
+    return res.data.data
+  }
+  return Promise.reject(new Error(res.data.message))
+}

+ 11 - 0
src/api/main/index.js

@@ -71,3 +71,14 @@ export async function updateUnitUse(data) {
   }
   return Promise.reject(new Error(res.data.message));
 }
+
+/**
+ * 宇信统计
+ */
+export async function deviceIndexStatistics() {
+  const res = await request.get('/eam/workorder/deviceIndexStatistics');
+  if (res.data.code == 0) {
+    return Promise.resolve(res.data.data);
+  }
+  return Promise.reject(new Error(res.data.message));
+}

二进制
src/assets/pcs/back.png


二进制
src/assets/pcs/faultDevice.png


二进制
src/assets/pcs/header.png


二进制
src/assets/pcs/home.png


二进制
src/assets/pcs/icon.png


二进制
src/assets/pcs/leftItem.png


二进制
src/assets/pcs/runDevice.png


二进制
src/assets/pcs/select.png


二进制
src/assets/pcs/successInfo.png


二进制
src/assets/pcs/titleIcon.png


二进制
src/assets/pcs/titleb.png


+ 6 - 1
src/enum/dict.js

@@ -47,7 +47,12 @@ export default {
   设备管理状态: 'activity_status',
   摄像机类型: 'isp_camera_type',
   摄像头品牌: 'camera_brand',
-  NVR设备类型: 'device_type_nvr'
+  NVR设备类型:'device_type_nvr',
+  机器人品牌: 'isp_robot_brand',
+  机器人类型: 'isp_robot_type',
+  设备管理设备类型: 'isp_device_type',
+  设备品牌: 'device_brand'
+
 };
 
 export const numberList = ['patrol_cycle'];

+ 696 - 0
src/views/home/common.vue

@@ -0,0 +1,696 @@
+<template>
+  <div class="ele-body">
+    <el-row :gutter="15">
+      <el-col :span="13" style="height: 100%">
+        <el-card class="box-card box-card1" style="height: 18%">
+          <div slot="header" class="clearfix">
+            <span>工单信息</span>
+          </div>
+          <div class="list">
+            <div
+              class="item"
+              v-for="(item, index) in repaireOptions"
+              :key="index"
+            >
+              <div class="img_box">
+                <img :src="item.imgUrl" />
+              </div>
+              <div @click="handelRouterTo('/page-wt/tickets')">
+                <div>{{ item.title }}</div>
+                <div class="text">{{ item.num }}/{{ item.total }}</div>
+              </div>
+            </div>
+          </div>
+        </el-card>
+
+        <div class="char">
+          <el-card class="box-card" style="width: 55%">
+            <div slot="header" class="clearfix">
+              <span>故障设备分布(部门)</span>
+            </div>
+
+            <v-chart
+              ref="pieRef"
+              style="height: 100%"
+              :option="pieEquipmentFailureOption"
+            />
+          </el-card>
+          <el-card class="box-card" style="width: calc(45% - 15px)">
+            <div slot="header" class="clearfix">
+              <span>设备状态</span>
+            </div>
+            <div class="statusList">
+              <div v-for="(item, index) in statusList" :key="index">
+                <div class="left" :style="{ color: item.color }">
+                  <img :src="item.titleUrl" />
+                  {{ item.title }}
+                </div>
+                <div class="right">
+                  <img :src="item.numUrl" />
+                  <div>
+                    <span>{{ item.num }}</span>
+                    <span>台</span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+        <!-- <el-card class="box-card" style="height: 18%; margin-top: 15px">
+          <div slot="header" class="clearfix">
+            <span>设备状态</span>
+          </div>
+          <div class="statusList">
+            <div
+              :style="{ background: item.color }"
+              class="item"
+              v-for="(item, index) in statusList"
+              :key="index"
+            >
+              <span>{{ item.title }}</span>
+              <span>{{ item.num }}</span>
+            </div>
+          </div>
+        </el-card> -->
+        <el-card class="box-card" style="height: 42%; margin-top: 15px">
+          <div slot="header" class="clearfix">
+            <span>故障设备分布(类型)</span>
+          </div>
+          <v-chart
+            ref="barRef"
+            style="height: 100%"
+            :option="barEquipmentFailureOption"
+          />
+        </el-card>
+      </el-col>
+      <el-col :span="11" style="height: 100%">
+        <div class="char" style="height: 40%; margin: 0">
+          <el-card
+            class="box-card"
+            style="height: 100%; width: 100%; position: relative"
+          >
+            <div slot="header" class="clearfix">
+              <span>备品备件</span>
+            </div>
+            <div class="sparePart">
+              <span>消耗总量 {{ bpbjGroupSumNum }}</span>
+              <!-- <span
+                >环比-17%
+                <i
+                  class="el-icon-caret-bottom"
+                  style="color: #38994e; font-size: 1vw"
+                ></i
+              ></span>
+              <span>同比 - </span>
+              <span style="margin-top: 25px">消耗种类 20 </span> -->
+            </div>
+            <v-chart
+              ref="pieRef1"
+              style="height: 100%"
+              :option="pieSparePartOption"
+            />
+          </el-card>
+
+          <!-- <el-card
+            class="box-card"
+            style="height: 100%; width: calc(37% - 15px)"
+          >
+            <div slot="header" class="clearfix">
+              <span>排名</span>
+              <el-select
+                v-model="rankType"
+                placeholder="请选择"
+                style="width: 110px"
+              >
+                <el-option
+                  v-for="item in rankTypeOption"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                >
+                </el-option>
+              </el-select>
+            </div>
+            <div class="scrollList">
+              <div class="tableList_header">
+                <span
+                  v-for="item in rankList"
+                  :key="item.prop"
+                  :style="{ width: item.width }"
+                >
+                  {{ item.label }}
+                </span>
+              </div>
+              <div class="tableList" ref="rankTableList">
+                <vue-seamless-scroll
+                  :data="rankData"
+                  :class-option="seamlessScrollOption"
+                  class="scroll_box"
+                >
+                  <div
+                    v-for="(item, index) in rankData"
+                    :key="index"
+                    class="srcoll_item"
+                  >
+                    <template v-for="data in rankList">
+                      <span
+                        :style="{ width: data.width }"
+                        :key="data.prop"
+                        :title="item[data.prop]"
+                      >
+                        {{ item[data.prop] }}</span
+                      >
+                    </template>
+                  </div>
+                </vue-seamless-scroll>
+              </div>
+            </div>
+          </el-card> -->
+        </div>
+
+        <el-card class="box-card" style="height: 60%; margin-top: 15px">
+          <div slot="header" class="clearfix">
+            <span>维修消耗趋势</span>
+            <!-- <el-radio-group
+              style="margin-left: 15px"
+              v-model="timeType"
+              @change="timeTypeChange()"
+            >
+              <el-radio-button label="3">月度</el-radio-button>
+              <el-radio-button label="2">季度</el-radio-button>
+              <el-radio-button label="1">年度</el-radio-button>
+            </el-radio-group> -->
+          </div>
+          <v-chart ref="lineRef" style="height: 100%" :option="lineOption" />
+        </el-card>
+        <!-- <el-card class="box-card" style="height: calc(25% - 15px)">
+          <div slot="header" class="clearfix">
+            <span>待处理</span>
+          </div>
+          <div class="scrollList">
+            <div class="tableList_header">
+              <span
+                v-for="item in planList"
+                :key="item.prop"
+                :style="{ width: item.width }"
+              >
+                {{ item.label }}
+              </span>
+            </div>
+            <div class="tableList" ref="planTableList">
+              <vue-seamless-scroll
+                :data="planData"
+                :class-option="seamlessScrollOption"
+                class="scroll_box"
+              >
+                <div
+                  v-for="(item, index) in planData"
+                  :key="index"
+                  class="srcoll_item"
+                >
+                  <template v-for="data in planList">
+                    <span
+                      v-if="data.prop != 'action' && data.prop != 'code'"
+                      :style="{ width: data.width }"
+                      :key="data.prop"
+                      :title="item[data.prop]"
+                    >
+                      {{ item[data.prop] }}</span
+                    >
+                    <span
+                      v-if="data.prop == 'code'"
+                      :style="{ width: data.width }"
+                      :key="data.prop"
+                      :title="item[data.prop]"
+                      ><el-link type="primary" :underline="false">
+                        {{ item[data.prop] }}
+                      </el-link></span
+                    >
+                    <span
+                      v-if="data.prop == 'action'"
+                      :style="{ width: data.width }"
+                      :key="data.prop"
+                      ><el-link
+                        type="primary"
+                        :underline="false"
+                        icon="el-icon-edit"
+                      >
+                        审批
+                      </el-link></span
+                    >
+                  </template>
+                </div>
+              </vue-seamless-scroll>
+            </div>
+          </div>
+        </el-card> -->
+      </el-col>
+    </el-row>
+  </div>
+</template>
+<script>
+  import vueSeamlessScroll from 'vue-seamless-scroll';
+
+  import {
+    // planList,
+    statusList,
+    rankList,
+    repaireOptions,
+    pieOption,
+    barOption,
+    pieOption1,
+    lineOption
+  } from './data';
+  import { use } from 'echarts/core';
+  import { CanvasRenderer } from 'echarts/renderers';
+  import { BarChart, PieChart, LineChart } from 'echarts/charts';
+  import {
+    GridComponent,
+    TooltipComponent,
+    LegendComponent
+  } from 'echarts/components';
+
+  import VChart from 'vue-echarts';
+  import { echartsMixin } from '@/utils/echarts-mixin';
+  import {
+    listInspection,
+    queryRunStatus,
+    queryGroupName,
+    queryDevice,
+    indexGroupQuery
+  } from '@/api/main';
+  // 按需加载 echarts
+  use([
+    CanvasRenderer,
+    BarChart,
+    PieChart,
+    GridComponent,
+    TooltipComponent,
+    LegendComponent,
+    LineChart
+  ]);
+  const seamlessScrollOption = {
+    step: 0.2, // 数值越大速度滚动越快
+    limitMoveNum: 5, // 开始无缝滚动的数据量 this.dataList.length
+    hoverStop: true, // 是否开启鼠标悬停stop
+    direction: 1, // 0向下 1向上 2向左 3向右
+    openWatch: true, // 开启数据实时监控刷新dom
+    singleHeight: 0, // 单步运动停止的高度(默认值0是无缝不停止的滚动) direction => 0/1
+    singleWidth: 0, // 单步运动停止的宽度(默认值0是无缝不停止的滚动) direction => 2/3
+    waitTime: 1000 // 单步运动停止的时间(默认值1000ms)
+  };
+  export default {
+    mixins: [echartsMixin(['barRef', 'pieRef', 'pieRef1', 'lineRef'])],
+    data() {
+      return {
+        timeType: '3',
+        tabPosition: '个人',
+        bpbjGroupSumNum: 0,
+
+        dateOptions: [
+          {
+            label: '近七日',
+            value: 1
+          },
+          {
+            label: '近一个月',
+            value: 2
+          }
+        ],
+        rankTypeOption: [
+          {
+            value: '1',
+            label: '全厂'
+          },
+          {
+            value: '2',
+            label: '机加一'
+          },
+          {
+            value: '3',
+            label: '机加二'
+          },
+          {
+            value: '4',
+            label: '特种作业'
+          },
+          {
+            value: '5',
+            label: '总装'
+          }
+        ],
+        rankType: '1',
+        statusList,
+        rankList,
+        repaireOptions,
+        date: new Date(),
+        seamlessScrollOption: JSON.parse(JSON.stringify(seamlessScrollOption)),
+        // seamlessScrollOption2: JSON.parse(JSON.stringify(seamlessScrollOption)),
+        barEquipmentFailureOption: {},
+        pieEquipmentFailureOption: {},
+        lineOption: {},
+        pieSparePartOption: {}
+      };
+    },
+    components: {
+      vueSeamlessScroll,
+      VChart
+    },
+    created() {
+      this.queryRunStatus();
+      // this.timeTypeChange();
+      this.queryGroupName();
+      this.queryDevice();
+      this.indexGroupQuery();
+    },
+    methods: {
+      //工单信息
+      async getListInspection() {
+        const arr = await Promise.all(
+          [1, 2, 3].map((item) => listInspection({ type: item }))
+        );
+        arr.forEach((item, index) => {
+          this.repaireOptions[index].total = item.numberTotal;
+          this.repaireOptions[index].num = item.finishNumber;
+        });
+      },
+      async indexGroupQuery() {
+        const data = await indexGroupQuery();
+        this.bpbjGroupSumNum = data.bpbjGroupSumNum;
+        this.pieSparePartOption = pieOption1(
+          data.bpbjGroupVOList.map((item) => {
+            return {
+              name: item.deptName,
+              value: item.sumTotalNum
+            };
+          })
+        );
+        this.lineOption = lineOption(
+          data.wxxhqsGroupVOList.map((item) => {
+            return {
+              name:item.recipientName,
+              data:item.sumTotalNumList,
+              type: 'line',
+              smooth: true
+            };
+          }), data.wxxhqsGroupVOList[0]?.yearAndMonthList
+        );
+      },
+      async queryGroupName() {
+        const data = await queryGroupName();
+        this.pieEquipmentFailureOption = pieOption(
+          data.map((item) => {
+            return {
+              name: item.groupName,
+              value: item.failureRate
+            };
+          })
+        );
+      },
+      async queryDevice() {
+        const data = await queryDevice();
+        this.barEquipmentFailureOption = barOption(
+          data.map((item) => {
+            return {
+              name: item.deviceName,
+              value: item.qualifiedNumber
+            };
+          })
+        );
+      },
+      queryRunStatus() {
+        queryRunStatus().then((res) => {
+          this.statusList.forEach((item) => {
+            item.num = res[item.code];
+          });
+        });
+      },
+      handelRouterTo(path) {
+        window.history.pushState(null, '', path);
+      }
+    },
+    mounted() {
+      this.getListInspection();
+      // this.seamlessScrollOption.limitMoveNum = parseInt(
+      //   this.$refs.rankTableList.clientHeight / 25
+      // );
+      setTimeout(() => {
+        this.$refs.barRef.resize();
+        this.$refs.pieRef.resize();
+        this.$refs.pieRef1.resize();
+        this.$refs.lineRef.resize();
+      }, 300);
+      // this.seamlessScrollOption2.limitMoveNum = parseInt(
+      //   this.$refs.rankTableList.clientHeight / 25
+      // );
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  @font-face {
+    font-family: 'MyFont';
+    src: url('../../../public/tinymce/skins/ui/oxide/fonts/black.ttf')
+      format('truetype');
+    font-weight: normal;
+    font-style: normal;
+  }
+  .clearfix {
+    font-size: 0.7vw;
+
+    > span {
+      font-weight: bold;
+      margin-left: 15px;
+      margin-right: 15px;
+    }
+    :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) {
+      background-color: #398f4e;
+      border-color: #398f4e;
+    }
+    :deep(.el-radio-button__inner) {
+      font-size: 0.7vw;
+    }
+  }
+  .ele-body {
+    height: calc(100vh - 106px);
+    > .el-row {
+      height: 100%;
+    }
+    :deep(.el-calendar) {
+      font-size: 0.7vw;
+      height: 100%;
+      .el-calendar__body {
+        height: 90%;
+        padding: 0.4vw 0.7vw;
+      }
+      .el-calendar-day {
+        height: 100%;
+      }
+      .el-calendar-table {
+        height: 100%;
+      }
+      .el-calendar__header {
+        padding: 0.4vw 0.7vw;
+      }
+      .el-calendar-table thead th {
+        padding: 0.4vw 0;
+      }
+      .dateItem {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+    }
+    .el-card {
+      height: 100%;
+      // margin-top: 15px;
+
+      :deep(.el-card__body) {
+        padding: 0.3vw;
+      }
+      :deep(.el-card__header) {
+        padding: 0;
+        height: 2.5vw;
+        display: flex;
+        align-items: center;
+      }
+      :deep(.el-card__body) {
+        height: calc(100% - 2.5vw);
+      }
+      :deep(.ele-pro-table) {
+        height: 99%;
+      }
+      .sparePart {
+        font-size: 0.7vw;
+        font-weight: bold;
+        position: absolute;
+        display: flex;
+        flex-direction: column;
+        top: 20%;
+        left: 5%;
+      }
+    }
+
+    // :deep(.el-card + .el-card){
+    //     margin-top: 0;
+    //   }
+    .char {
+      display: flex;
+      justify-content: space-between;
+      margin-top: 15px;
+      height: calc(40% - 15px);
+      > .box-card {
+        margin-top: 0;
+      }
+    }
+    .list {
+      display: flex;
+      box-sizing: border-box;
+      height: 100%;
+      margin-top: 5px;
+      > div {
+        flex: 1;
+        display: flex;
+        align-items: center;
+        background-color: #398f4d;
+        margin-left: 7.5px;
+        margin-right: 7.5px;
+        border-radius: 8px;
+        color: #fff;
+        height: 90%;
+        cursor: pointer;
+        .img_box {
+          // flex: 0 0 130px;
+          width: 3vw;
+          height: 100%;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          border-top-right-radius: 50%;
+          border-bottom-right-radius: 50%;
+          border-top-left-radius: 8px;
+          border-bottom-left-radius: 8px;
+          background-color: #38994e;
+          > img {
+            width: 2.5vw;
+            height: 2.5vw;
+          }
+        }
+
+        > div {
+          flex: 1;
+          // font-size: 16px;
+          font-size: 0.7vw;
+
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          justify-content: center;
+          .text {
+            margin-top: 10px;
+
+            // font-size: 20px;
+          }
+        }
+      }
+    }
+    .statusList {
+      display: flex;
+      height: 100%;
+      color: #fff;
+      flex-wrap: wrap;
+      justify-content: space-around;
+      align-items: center;
+      > div {
+        width: 46%;
+        height: 3.3vw;
+        display: flex;
+        align-items: center;
+
+        > div {
+          position: relative;
+          display: flex;
+          height: 100%;
+          img {
+            position: absolute;
+            width: 100%;
+            height: 100%;
+            top: 0;
+            left: 0;
+          }
+        }
+        .left {
+          width: 3vw;
+          font-size: 0.7vw;
+          align-items: center;
+          justify-content: center;
+          font-weight: 600;
+        }
+        .right {
+          height: 2.6vw;
+          font-family: 'MyFont';
+          color: #fff;
+          margin-left: 8px;
+          width: calc(100% - 3vw - 5px);
+          align-items: center;
+          justify-content: center;
+          > div {
+            position: absolute;
+            font-size: 0.6vw;
+            display: flex;
+            align-items: center;
+            span:nth-of-type(1) {
+              margin-right: 0.6vw;
+              font-size: 0.9vw;
+            }
+            // top:15px
+          }
+        }
+      }
+    }
+  }
+  .scrollList {
+    height: 100%;
+    font-size: 0.6vw;
+    color: #333;
+    .tableList_header {
+      width: 100%;
+      display: flex;
+      font-weight: bold;
+
+      span {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        height: 2vw;
+        background: #f6f6f6;
+      }
+    }
+    .tableList {
+      height: calc(100% - 2vw);
+    }
+    .scroll_box {
+      height: 100%;
+      overflow: hidden;
+      .srcoll_item {
+        width: 100%;
+        display: flex;
+        min-height: 25px;
+
+        span {
+          text-align: center;
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+        }
+      }
+    }
+  }
+</style>
+<style>
+  .el-select-dropdown__item.selected {
+    color: #157a2c !important;
+  }
+</style>

+ 100 - 0
src/views/home/components/StatCard.vue

@@ -0,0 +1,100 @@
+<template>
+  <div class="stat-card" :style="backgroundStyle">
+    <!-- <div class="stat-card__icon" :style="{ background: iconBg }">
+      <i :class="icon" :style="{ color: iconColor }"></i>
+    </div> -->
+    <div class="stat-card__info">
+      <div class="stat-card__label">{{ label }}</div>
+      <div class="stat-card__value" :style="{ color: valueColor }">
+        <span class="stat-card__num">{{ value }}</span>
+        <span class="stat-card__unit">{{ unit }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'StatCard',
+  props: {
+    // 背景图:传 CSS background 完整值,如 "url('...')"、颜色值、渐变值
+    bgimg: { type: String, default: '' },
+    label: { type: String, default: '' },
+    value: { type: [Number, String], default: 0 },
+    unit: { type: String, default: '台' },
+    icon: { type: String, default: 'el-icon-monitor' },
+    iconColor: { type: String, default: '#409EFF' },
+    iconBg: { type: String, default: '#ecf5ff' },
+    valueColor: { type: String, default: '#409EFF' }
+  },
+  computed: {
+    backgroundStyle() {
+      if (this.bgimg) {
+        return { background: this.bgimg }
+      }
+      return {}
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.stat-card {
+  // border-radius: 8px;
+  // border: 1px solid #ebeef5;
+  transition: all 0.3s;
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding: 16px 20px;
+  background-size: 100% 100% !important;
+  background-repeat: no-repeat !important;
+  height: 90px;
+  width: 25%;
+
+  &__icon {
+    width: 56px;
+    height: 56px;
+    border-radius: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+
+    i {
+      font-size: 28px;
+    }
+  }
+
+  &__info {
+    flex: 1;
+    min-width: 0;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+  &__label {
+    font-family: 'Microsoft YaHei', sans-serif;
+    font-size: 2rem;
+    color: #fff;
+    margin-right: 20px;
+  }
+
+  &__value {
+    display: flex;
+    align-items: baseline;
+    font-family: '优设标题黑';
+  }
+
+  &__num {
+    font-size: 28px;
+    font-weight: 700;
+    line-height: 1;
+  }
+
+  &__unit {
+    font-size: 14px;
+    margin-left: 4px;
+  }
+}
+</style>

+ 57 - 0
src/views/home/components/TrendChart.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="trend-chart-wrapper" :style="{ height: height }">
+    <div ref="chartRef" class="trend-chart__body"></div>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+
+export default {
+  name: 'TrendChart',
+  props: {
+    option: { type: Object, default: () => ({}) },
+    height: { type: String, default: '260px' }
+  },
+  data() {
+    return { chartInstance: null }
+  },
+  watch: {
+    option: {
+      handler(val) { this.updateChart(val) },
+      deep: true
+    }
+  },
+  mounted() { this.$nextTick(() => this.initChart()) },
+  beforeDestroy() { this.disposeChart() },
+  methods: {
+    initChart() {
+      this.chartInstance = echarts.init(this.$refs.chartRef)
+      this.updateChart(this.option)
+      window.addEventListener('resize', this.handleResize)
+    },
+    updateChart(option) {
+      if (this.chartInstance && option && Object.keys(option).length > 0) {
+        this.chartInstance.setOption(option, { notMerge: true })
+      }
+    },
+    handleResize() { this.chartInstance?.resize() },
+    disposeChart() {
+      window.removeEventListener('resize', this.handleResize)
+      this.chartInstance?.dispose()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.trend-chart-wrapper {
+  width: 100%;
+  min-height: 0;
+}
+
+.trend-chart__body {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 13 - 685
src/views/home/index.vue

@@ -1,696 +1,24 @@
 <template>
-  <div class="ele-body">
-    <el-row :gutter="15">
-      <el-col :span="13" style="height: 100%">
-        <el-card class="box-card box-card1" style="height: 18%">
-          <div slot="header" class="clearfix">
-            <span>工单信息</span>
-          </div>
-          <div class="list">
-            <div
-              class="item"
-              v-for="(item, index) in repaireOptions"
-              :key="index"
-            >
-              <div class="img_box">
-                <img :src="item.imgUrl" />
-              </div>
-              <div @click="handelRouterTo('/page-wt/tickets')">
-                <div>{{ item.title }}</div>
-                <div class="text">{{ item.num }}/{{ item.total }}</div>
-              </div>
-            </div>
-          </div>
-        </el-card>
-
-        <div class="char">
-          <el-card class="box-card" style="width: 55%">
-            <div slot="header" class="clearfix">
-              <span>故障设备分布(部门)</span>
-            </div>
-
-            <v-chart
-              ref="pieRef"
-              style="height: 100%"
-              :option="pieEquipmentFailureOption"
-            />
-          </el-card>
-          <el-card class="box-card" style="width: calc(45% - 15px)">
-            <div slot="header" class="clearfix">
-              <span>设备状态</span>
-            </div>
-            <div class="statusList">
-              <div v-for="(item, index) in statusList" :key="index">
-                <div class="left" :style="{ color: item.color }">
-                  <img :src="item.titleUrl" />
-                  {{ item.title }}
-                </div>
-                <div class="right">
-                  <img :src="item.numUrl" />
-                  <div>
-                    <span>{{ item.num }}</span>
-                    <span>台</span>
-                  </div>
-                </div>
-              </div>
-            </div>
-          </el-card>
-        </div>
-        <!-- <el-card class="box-card" style="height: 18%; margin-top: 15px">
-          <div slot="header" class="clearfix">
-            <span>设备状态</span>
-          </div>
-          <div class="statusList">
-            <div
-              :style="{ background: item.color }"
-              class="item"
-              v-for="(item, index) in statusList"
-              :key="index"
-            >
-              <span>{{ item.title }}</span>
-              <span>{{ item.num }}</span>
-            </div>
-          </div>
-        </el-card> -->
-        <el-card class="box-card" style="height: 42%; margin-top: 15px">
-          <div slot="header" class="clearfix">
-            <span>故障设备分布(类型)</span>
-          </div>
-          <v-chart
-            ref="barRef"
-            style="height: 100%"
-            :option="barEquipmentFailureOption"
-          />
-        </el-card>
-      </el-col>
-      <el-col :span="11" style="height: 100%">
-        <div class="char" style="height: 40%; margin: 0">
-          <el-card
-            class="box-card"
-            style="height: 100%; width: 100%; position: relative"
-          >
-            <div slot="header" class="clearfix">
-              <span>备品备件</span>
-            </div>
-            <div class="sparePart">
-              <span>消耗总量 {{ bpbjGroupSumNum }}</span>
-              <!-- <span
-                >环比-17%
-                <i
-                  class="el-icon-caret-bottom"
-                  style="color: #38994e; font-size: 1vw"
-                ></i
-              ></span>
-              <span>同比 - </span>
-              <span style="margin-top: 25px">消耗种类 20 </span> -->
-            </div>
-            <v-chart
-              ref="pieRef1"
-              style="height: 100%"
-              :option="pieSparePartOption"
-            />
-          </el-card>
-
-          <!-- <el-card
-            class="box-card"
-            style="height: 100%; width: calc(37% - 15px)"
-          >
-            <div slot="header" class="clearfix">
-              <span>排名</span>
-              <el-select
-                v-model="rankType"
-                placeholder="请选择"
-                style="width: 110px"
-              >
-                <el-option
-                  v-for="item in rankTypeOption"
-                  :key="item.value"
-                  :label="item.label"
-                  :value="item.value"
-                >
-                </el-option>
-              </el-select>
-            </div>
-            <div class="scrollList">
-              <div class="tableList_header">
-                <span
-                  v-for="item in rankList"
-                  :key="item.prop"
-                  :style="{ width: item.width }"
-                >
-                  {{ item.label }}
-                </span>
-              </div>
-              <div class="tableList" ref="rankTableList">
-                <vue-seamless-scroll
-                  :data="rankData"
-                  :class-option="seamlessScrollOption"
-                  class="scroll_box"
-                >
-                  <div
-                    v-for="(item, index) in rankData"
-                    :key="index"
-                    class="srcoll_item"
-                  >
-                    <template v-for="data in rankList">
-                      <span
-                        :style="{ width: data.width }"
-                        :key="data.prop"
-                        :title="item[data.prop]"
-                      >
-                        {{ item[data.prop] }}</span
-                      >
-                    </template>
-                  </div>
-                </vue-seamless-scroll>
-              </div>
-            </div>
-          </el-card> -->
-        </div>
-
-        <el-card class="box-card" style="height: 60%; margin-top: 15px">
-          <div slot="header" class="clearfix">
-            <span>维修消耗趋势</span>
-            <!-- <el-radio-group
-              style="margin-left: 15px"
-              v-model="timeType"
-              @change="timeTypeChange()"
-            >
-              <el-radio-button label="3">月度</el-radio-button>
-              <el-radio-button label="2">季度</el-radio-button>
-              <el-radio-button label="1">年度</el-radio-button>
-            </el-radio-group> -->
-          </div>
-          <v-chart ref="lineRef" style="height: 100%" :option="lineOption" />
-        </el-card>
-        <!-- <el-card class="box-card" style="height: calc(25% - 15px)">
-          <div slot="header" class="clearfix">
-            <span>待处理</span>
-          </div>
-          <div class="scrollList">
-            <div class="tableList_header">
-              <span
-                v-for="item in planList"
-                :key="item.prop"
-                :style="{ width: item.width }"
-              >
-                {{ item.label }}
-              </span>
-            </div>
-            <div class="tableList" ref="planTableList">
-              <vue-seamless-scroll
-                :data="planData"
-                :class-option="seamlessScrollOption"
-                class="scroll_box"
-              >
-                <div
-                  v-for="(item, index) in planData"
-                  :key="index"
-                  class="srcoll_item"
-                >
-                  <template v-for="data in planList">
-                    <span
-                      v-if="data.prop != 'action' && data.prop != 'code'"
-                      :style="{ width: data.width }"
-                      :key="data.prop"
-                      :title="item[data.prop]"
-                    >
-                      {{ item[data.prop] }}</span
-                    >
-                    <span
-                      v-if="data.prop == 'code'"
-                      :style="{ width: data.width }"
-                      :key="data.prop"
-                      :title="item[data.prop]"
-                      ><el-link type="primary" :underline="false">
-                        {{ item[data.prop] }}
-                      </el-link></span
-                    >
-                    <span
-                      v-if="data.prop == 'action'"
-                      :style="{ width: data.width }"
-                      :key="data.prop"
-                      ><el-link
-                        type="primary"
-                        :underline="false"
-                        icon="el-icon-edit"
-                      >
-                        审批
-                      </el-link></span
-                    >
-                  </template>
-                </div>
-              </vue-seamless-scroll>
-            </div>
-          </div>
-        </el-card> -->
-      </el-col>
-    </el-row>
+  <div>
+    <yuxin v-if="$hasPermission('eam:home:yuxin')"></yuxin>
+     <!-- v-if="$store.state.user.info.clientEnvironmentId==10"     -->
+    <common v-else></common>
   </div>
 </template>
-<script>
-  import vueSeamlessScroll from 'vue-seamless-scroll';
 
-  import {
-    // planList,
-    statusList,
-    rankList,
-    repaireOptions,
-    pieOption,
-    barOption,
-    pieOption1,
-    lineOption
-  } from './data';
-  import { use } from 'echarts/core';
-  import { CanvasRenderer } from 'echarts/renderers';
-  import { BarChart, PieChart, LineChart } from 'echarts/charts';
-  import {
-    GridComponent,
-    TooltipComponent,
-    LegendComponent
-  } from 'echarts/components';
+<script>
+  import common from './common.vue';
+  import yuxin from './yuxin.vue';
 
-  import VChart from 'vue-echarts';
-  import { echartsMixin } from '@/utils/echarts-mixin';
-  import {
-    listInspection,
-    queryRunStatus,
-    queryGroupName,
-    queryDevice,
-    indexGroupQuery
-  } from '@/api/main';
-  // 按需加载 echarts
-  use([
-    CanvasRenderer,
-    BarChart,
-    PieChart,
-    GridComponent,
-    TooltipComponent,
-    LegendComponent,
-    LineChart
-  ]);
-  const seamlessScrollOption = {
-    step: 0.2, // 数值越大速度滚动越快
-    limitMoveNum: 5, // 开始无缝滚动的数据量 this.dataList.length
-    hoverStop: true, // 是否开启鼠标悬停stop
-    direction: 1, // 0向下 1向上 2向左 3向右
-    openWatch: true, // 开启数据实时监控刷新dom
-    singleHeight: 0, // 单步运动停止的高度(默认值0是无缝不停止的滚动) direction => 0/1
-    singleWidth: 0, // 单步运动停止的宽度(默认值0是无缝不停止的滚动) direction => 2/3
-    waitTime: 1000 // 单步运动停止的时间(默认值1000ms)
-  };
   export default {
-    mixins: [echartsMixin(['barRef', 'pieRef', 'pieRef1', 'lineRef'])],
+    components: { common, yuxin },
     data() {
-      return {
-        timeType: '3',
-        tabPosition: '个人',
-        bpbjGroupSumNum: 0,
-
-        dateOptions: [
-          {
-            label: '近七日',
-            value: 1
-          },
-          {
-            label: '近一个月',
-            value: 2
-          }
-        ],
-        rankTypeOption: [
-          {
-            value: '1',
-            label: '全厂'
-          },
-          {
-            value: '2',
-            label: '机加一'
-          },
-          {
-            value: '3',
-            label: '机加二'
-          },
-          {
-            value: '4',
-            label: '特种作业'
-          },
-          {
-            value: '5',
-            label: '总装'
-          }
-        ],
-        rankType: '1',
-        statusList,
-        rankList,
-        repaireOptions,
-        date: new Date(),
-        seamlessScrollOption: JSON.parse(JSON.stringify(seamlessScrollOption)),
-        // seamlessScrollOption2: JSON.parse(JSON.stringify(seamlessScrollOption)),
-        barEquipmentFailureOption: {},
-        pieEquipmentFailureOption: {},
-        lineOption: {},
-        pieSparePartOption: {}
-      };
-    },
-    components: {
-      vueSeamlessScroll,
-      VChart
-    },
-    created() {
-      this.queryRunStatus();
-      // this.timeTypeChange();
-      this.queryGroupName();
-      this.queryDevice();
-      this.indexGroupQuery();
-    },
-    methods: {
-      //工单信息
-      async getListInspection() {
-        const arr = await Promise.all(
-          [1, 2, 3].map((item) => listInspection({ type: item }))
-        );
-        arr.forEach((item, index) => {
-          this.repaireOptions[index].total = item.numberTotal;
-          this.repaireOptions[index].num = item.finishNumber;
-        });
-      },
-      async indexGroupQuery() {
-        const data = await indexGroupQuery();
-        this.bpbjGroupSumNum = data.bpbjGroupSumNum;
-        this.pieSparePartOption = pieOption1(
-          data.bpbjGroupVOList.map((item) => {
-            return {
-              name: item.deptName,
-              value: item.sumTotalNum
-            };
-          })
-        );
-        this.lineOption = lineOption(
-          data.wxxhqsGroupVOList.map((item) => {
-            return {
-              name:item.recipientName,
-              data:item.sumTotalNumList,
-              type: 'line',
-              smooth: true
-            };
-          }), data.wxxhqsGroupVOList[0]?.yearAndMonthList
-        );
-      },
-      async queryGroupName() {
-        const data = await queryGroupName();
-        this.pieEquipmentFailureOption = pieOption(
-          data.map((item) => {
-            return {
-              name: item.groupName,
-              value: item.failureRate
-            };
-          })
-        );
-      },
-      async queryDevice() {
-        const data = await queryDevice();
-        this.barEquipmentFailureOption = barOption(
-          data.map((item) => {
-            return {
-              name: item.deviceName,
-              value: item.qualifiedNumber
-            };
-          })
-        );
-      },
-      queryRunStatus() {
-        queryRunStatus().then((res) => {
-          this.statusList.forEach((item) => {
-            item.num = res[item.code];
-          });
-        });
-      },
-      handelRouterTo(path) {
-        window.history.pushState(null, '', path);
-      }
+      return {};
     },
-    mounted() {
-      this.getListInspection();
-      // this.seamlessScrollOption.limitMoveNum = parseInt(
-      //   this.$refs.rankTableList.clientHeight / 25
-      // );
-      setTimeout(() => {
-        this.$refs.barRef.resize();
-        this.$refs.pieRef.resize();
-        this.$refs.pieRef1.resize();
-        this.$refs.lineRef.resize();
-      }, 300);
-      // this.seamlessScrollOption2.limitMoveNum = parseInt(
-      //   this.$refs.rankTableList.clientHeight / 25
-      // );
-    }
+    computed: {},
+
+    mounted() {}
   };
 </script>
 
-<style lang="scss" scoped>
-  @font-face {
-    font-family: 'MyFont';
-    src: url('../../../public/tinymce/skins/ui/oxide/fonts/black.ttf')
-      format('truetype');
-    font-weight: normal;
-    font-style: normal;
-  }
-  .clearfix {
-    font-size: 0.7vw;
-
-    > span {
-      font-weight: bold;
-      margin-left: 15px;
-      margin-right: 15px;
-    }
-    :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) {
-      background-color: #398f4e;
-      border-color: #398f4e;
-    }
-    :deep(.el-radio-button__inner) {
-      font-size: 0.7vw;
-    }
-  }
-  .ele-body {
-    height: calc(100vh - 106px);
-    > .el-row {
-      height: 100%;
-    }
-    :deep(.el-calendar) {
-      font-size: 0.7vw;
-      height: 100%;
-      .el-calendar__body {
-        height: 90%;
-        padding: 0.4vw 0.7vw;
-      }
-      .el-calendar-day {
-        height: 100%;
-      }
-      .el-calendar-table {
-        height: 100%;
-      }
-      .el-calendar__header {
-        padding: 0.4vw 0.7vw;
-      }
-      .el-calendar-table thead th {
-        padding: 0.4vw 0;
-      }
-      .dateItem {
-        width: 100%;
-        height: 100%;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-      }
-    }
-    .el-card {
-      height: 100%;
-      // margin-top: 15px;
-
-      :deep(.el-card__body) {
-        padding: 0.3vw;
-      }
-      :deep(.el-card__header) {
-        padding: 0;
-        height: 2.5vw;
-        display: flex;
-        align-items: center;
-      }
-      :deep(.el-card__body) {
-        height: calc(100% - 2.5vw);
-      }
-      :deep(.ele-pro-table) {
-        height: 99%;
-      }
-      .sparePart {
-        font-size: 0.7vw;
-        font-weight: bold;
-        position: absolute;
-        display: flex;
-        flex-direction: column;
-        top: 20%;
-        left: 5%;
-      }
-    }
-
-    // :deep(.el-card + .el-card){
-    //     margin-top: 0;
-    //   }
-    .char {
-      display: flex;
-      justify-content: space-between;
-      margin-top: 15px;
-      height: calc(40% - 15px);
-      > .box-card {
-        margin-top: 0;
-      }
-    }
-    .list {
-      display: flex;
-      box-sizing: border-box;
-      height: 100%;
-      margin-top: 5px;
-      > div {
-        flex: 1;
-        display: flex;
-        align-items: center;
-        background-color: #398f4d;
-        margin-left: 7.5px;
-        margin-right: 7.5px;
-        border-radius: 8px;
-        color: #fff;
-        height: 90%;
-        cursor: pointer;
-        .img_box {
-          // flex: 0 0 130px;
-          width: 3vw;
-          height: 100%;
-          display: flex;
-          align-items: center;
-          justify-content: center;
-          border-top-right-radius: 50%;
-          border-bottom-right-radius: 50%;
-          border-top-left-radius: 8px;
-          border-bottom-left-radius: 8px;
-          background-color: #38994e;
-          > img {
-            width: 2.5vw;
-            height: 2.5vw;
-          }
-        }
-
-        > div {
-          flex: 1;
-          // font-size: 16px;
-          font-size: 0.7vw;
-
-          display: flex;
-          flex-direction: column;
-          align-items: center;
-          justify-content: center;
-          .text {
-            margin-top: 10px;
-
-            // font-size: 20px;
-          }
-        }
-      }
-    }
-    .statusList {
-      display: flex;
-      height: 100%;
-      color: #fff;
-      flex-wrap: wrap;
-      justify-content: space-around;
-      align-items: center;
-      > div {
-        width: 46%;
-        height: 3.3vw;
-        display: flex;
-        align-items: center;
-
-        > div {
-          position: relative;
-          display: flex;
-          height: 100%;
-          img {
-            position: absolute;
-            width: 100%;
-            height: 100%;
-            top: 0;
-            left: 0;
-          }
-        }
-        .left {
-          width: 3vw;
-          font-size: 0.7vw;
-          align-items: center;
-          justify-content: center;
-          font-weight: 600;
-        }
-        .right {
-          height: 2.6vw;
-          font-family: 'MyFont';
-          color: #fff;
-          margin-left: 8px;
-          width: calc(100% - 3vw - 5px);
-          align-items: center;
-          justify-content: center;
-          > div {
-            position: absolute;
-            font-size: 0.6vw;
-            display: flex;
-            align-items: center;
-            span:nth-of-type(1) {
-              margin-right: 0.6vw;
-              font-size: 0.9vw;
-            }
-            // top:15px
-          }
-        }
-      }
-    }
-  }
-  .scrollList {
-    height: 100%;
-    font-size: 0.6vw;
-    color: #333;
-    .tableList_header {
-      width: 100%;
-      display: flex;
-      font-weight: bold;
-
-      span {
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        height: 2vw;
-        background: #f6f6f6;
-      }
-    }
-    .tableList {
-      height: calc(100% - 2vw);
-    }
-    .scroll_box {
-      height: 100%;
-      overflow: hidden;
-      .srcoll_item {
-        width: 100%;
-        display: flex;
-        min-height: 25px;
-
-        span {
-          text-align: center;
-          overflow: hidden;
-          white-space: nowrap;
-          text-overflow: ellipsis;
-        }
-      }
-    }
-  }
-</style>
-<style>
-  .el-select-dropdown__item.selected {
-    color: #157a2c !important;
-  }
-</style>
+<style lang="scss" scoped></style>

+ 844 - 0
src/views/home/yuxin.vue

@@ -0,0 +1,844 @@
+<template>
+<vue-fullscreen
+    class="box-container"
+    v-cloak
+    v-model="isFullscreen"
+    fullscreenClass="box-container"
+    :exit-on-click-wrapper="false"
+  >
+  <div class="box-container" v-cloak>
+    <div class="scroll-wrapper">
+      <!-- 头部区域 -->
+      <div class="header-section">
+        <div class="box-header-scroll">
+          <!-- <div class="logo">
+            <el-select
+              v-model="selectedStation"
+              placeholder="请选择"
+              class="custom-select"
+              popper-class="custom-select-dropdown"
+            >
+              <el-option
+                v-for="item in stationList"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </div> -->
+          <div class="header-center">
+            <div class="title">设备运维看板</div>
+          </div>
+          <!-- <div class="time">
+            <div class="time-display">
+              <span class="date-text">{{ currentDate }}</span>
+              <span class="time-text">{{ currentTime }}</span>
+            </div>
+          </div> -->
+          <div class="time">
+            <div class="time-display">
+                <span class="date-text">{{ currentDate }}</span>
+                <span class="time-text">{{ currentTime }}</span>
+            </div>
+            <span class="fullscreen-toggle" @click.passive="onFullscreen">
+                <i
+                v-if="isFullscreen"
+                title="取消全屏"
+                class="el-icon-_screen-restore"
+                />
+                <i v-else title="全屏" class="el-icon-_screen-full" />
+            </span>
+          </div>
+        </div>
+      </div>
+
+        <!-- 统计卡片区域 -->
+        <div class="stats">
+            <!-- <el-row :gutter="16">
+                <el-col :span="8"> -->
+                    <stat-card
+                        v-for="item in statsCardList"
+                        :key="item.key"
+                        :label="item.label"
+                        :value="item.value"
+                        :unit="item.unit"
+                        :icon="item.icon"
+                        :icon-color="item.iconColor"
+                        :icon-bg="item.iconBg"
+                        :value-color="item.valueColor"
+                        :bgimg="'url(' + item.bgimg + ')'"
+                    />
+                    <!-- </el-col>
+                    <el-col :span="8"> -->
+                    <!-- <stat-card
+                        :bgimg="'url(' + require('@/assets/pcs/runDevice.png') + ')'"
+                        label="运行数"
+                        :value="345"
+                        unit="台"
+                        icon="el-icon-video-play"
+                        icon-color="#67C23A"
+                        icon-bg="#f0f9eb"
+                        value-color="#67C23A"
+                    /> -->
+                    <!-- </el-col>
+                    <el-col :span="8"> -->
+                    <!-- <stat-card
+                        :bgimg="'url(' + require('@/assets/pcs/runDevice.png') + ')'"
+                        label="维修数"
+                        :value="5"
+                        unit="台"
+                        icon="el-icon-setting"
+                        icon-color="#E6A23C"
+                        icon-bg="#fdf6ec"
+                        value-color="#E6A23C"
+                    /> -->
+                <!-- </el-col> -->
+            <!-- </el-row> -->
+        </div>
+
+      <!-- 双栏布局区域 -->
+      <div class="two-column-layout">
+        <!-- 左侧区域 -->
+        <div class="left-section">
+
+          <!-- 年运行率趋势图 -->
+          <div class="panel chart-panel">
+            <div class="panel-title">
+              <div class="panel-icon"></div>
+              年运行率趋势图
+            </div>
+            <div class="chart-container">
+              <trend-chart :option="yearRunRateOption" height="100%" />
+            </div>
+          </div>
+
+          <!-- 年故障率趋势图 -->
+          <div class="panel chart-panel">
+            <div class="panel-title">
+              <div class="panel-icon"></div>
+              年故障率趋势图
+            </div>
+            <div class="chart-container">
+              <trend-chart :option="yearFaultRateOption" height="100%" />
+            </div>
+          </div>
+
+          <!-- 年维修次数趋势图 -->
+          <div class="panel chart-panel">
+            <div class="panel-title">
+              <div class="panel-icon"></div>
+              年维修次数趋势图
+            </div>
+            <div class="chart-container">
+              <trend-chart :option="yearRepairCountOption" height="100%" />
+            </div>
+          </div>
+        </div>
+
+        <!-- 右侧区域 -->
+        <div class="right-section">
+
+          <!-- 月运行率趋势 -->
+          <div class="panel chart-panel">
+            <div class="panel-title">
+              <div class="panel-icon"></div>
+              月运行率趋势图
+            </div>
+            <div class="chart-container">
+              <trend-chart :option="monthRunRateOption" height="100%" />
+            </div>
+          </div>
+
+          <!-- 月故障率趋势图 -->
+          <div class="panel chart-panel">
+            <div class="panel-title">
+              <div class="panel-icon"></div>
+              月故障率趋势图
+            </div>
+            <div class="chart-container">
+              <trend-chart :option="monthFaultRateOption" height="100%" />
+            </div>
+          </div>
+
+          <!-- 月维修次数趋势图 -->
+          <div class="panel chart-panel">
+            <div class="panel-title">
+              <div class="panel-icon"></div>
+              月维修次数趋势图
+            </div>
+            <div class="chart-container">
+              <trend-chart :option="monthRepairCountOption" height="100%" />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</vue-fullscreen>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+import { component } from 'vue-fullscreen'
+import TrendChart from './components/TrendChart.vue'
+import StatCard from './components/StatCard.vue'
+import { deviceIndexStatistics } from '@/api/main/index.js'
+
+// 固定场站列表
+const FIXED_STATIONS = ['5#站', '6#站', '7#站']
+
+// 固定 12 个月份的 x 轴标签
+const MONTHS_12 = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
+
+// 获取当月所有天数(1日 ~ 28/29/30/31日)
+function getCurrentMonthDays() {
+  const now = new Date()
+  const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()
+  return Array.from({ length: daysInMonth }, (_, i) => (i + 1) + '日')
+}
+
+// 将 API 数据(dateTime="2026-05-15")格式化为当月日报图 xData + seriesConfig
+function formatDayTrendData(list, valueKey, colors) {
+  const valueField = valueKey || 'rate'
+  const DAYS = getCurrentMonthDays()
+  const stationMap = {}
+
+  list.forEach((item) => {
+    const station = item.stationName
+    const day = parseInt(item.dateTime.split('-')[2])             // "2026-05-15" → 15
+    const dayLabel = DAYS[day - 1]                                // 15 → "15日"
+    if (!stationMap[station]) stationMap[station] = {}
+    stationMap[station][dayLabel] = item[valueField]
+  })
+
+  const seriesConfig = FIXED_STATIONS.map((name, idx) => ({
+    name,
+    data: DAYS.map((d) => stationMap[name]?.[d] ?? 0),
+    color: colors[idx % colors.length]
+  }))
+
+  return { xData: DAYS, seriesConfig }
+}
+
+// 将 API 数据 [{stationName, dateTime, rate}] 格式化为 chart 所需的 xData(固定12月) + seriesConfig
+// 始终按 FIXED_STATIONS 顺序输出,无数据月份补 0
+function formatTrendData(list, valueKey, colors) {
+  const valueField = valueKey || 'rate'
+  const stationMap = {}
+
+  list.forEach((item) => {
+    const station = item.stationName
+    const month = parseInt(item.dateTime.split('-')[1])       // "2026-05" → 5
+    const monthLabel = MONTHS_12[month - 1]                    // 5 → "5月"
+    if (!stationMap[station]) stationMap[station] = {}
+    stationMap[station][monthLabel] = item[valueField]
+  })
+
+  const seriesConfig = FIXED_STATIONS.map((name, idx) => ({
+    name,
+    data: MONTHS_12.map((m) => stationMap[name]?.[m] ?? 0),    // 缺月/缺站统一补 0
+    color: colors[idx % colors.length]
+  }))
+
+  return { xData: MONTHS_12, seriesConfig }
+}
+
+const palette = ['#3b82f6', '#22d3ee', '#facc15', '#f87171', '#a78bfa', '#34d399']
+
+function makeMultiLineOption(xData, seriesConfig, unit) {
+  const series = seriesConfig.map((item, idx) => {
+    const color = item.color || palette[idx % palette.length]
+    return {
+      name: item.name,
+      type: 'line',
+      data: item.data,
+      smooth: true,
+      symbol: 'circle',
+      symbolSize: 4,
+      lineStyle: { width: 2, color: color },
+      itemStyle: { color: color },
+      areaStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: color },
+          { offset: 1, color: 'rgba(13,31,74,0.01)' }
+        ]),
+        opacity: 0.2
+      }
+    }
+  })
+
+  return {
+    tooltip: {
+      trigger: 'axis',
+      backgroundColor: 'rgba(8,21,46,0.9)',
+      borderColor: 'rgba(42,90,170,0.4)',
+      textStyle: { color: '#fff', fontSize: 12 }
+    },
+    legend: {
+      bottom: 0,
+      textStyle: { color: '#8ba3c7', fontSize: 11 },
+      itemWidth: 14,
+      itemHeight: 8
+    },
+    grid: { left: '3%', right: '4%', bottom: '12%', top: '8%', containLabel: true },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: xData,
+      axisLine: { lineStyle: { color: '#4a6fa5' } },
+      axisTick: { show: false },
+      axisLabel: { color: '#8ba3c7', fontSize: 11 }
+    },
+    yAxis: {
+      type: 'value',
+      max: function (value) { return Math.ceil(value.max * 1.1) },
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: { color: '#8ba3c7', fontSize: 11, formatter: '{value}' + unit },
+      splitLine: { lineStyle: { color: 'rgba(74,111,165,0.2)', type: 'dashed' } }
+    },
+    series: series
+  }
+}
+
+export default {
+  name: 'DeviceDashboard',
+  components: { VueFullscreen: component, TrendChart, StatCard },
+  data() {
+    return {
+      isFullscreen: false,
+      currentDate: '',
+      currentTime: '',
+      selectedStation: 'station-6',
+      stationList: [
+        { label: '5#场站', value: 'station-5' },
+        { label: '6#场站', value: 'station-6' },
+        { label: '7#场站', value: 'station-7' }
+      ],
+      statsCardList: [
+        { label: '设备总数', value: 0, unit: '台', key: 'totalNum', bgimg: require('@/assets/pcs/runDevice.png'), valueColor: '#409EFF', icon: 'el-icon-monitor', iconColor: '#409EFF', iconBg: '#ecf5ff'},
+        { label: '运行数', value: 0, unit: '台', key: 'runNum', bgimg: require('@/assets/pcs/successInfo.png'), valueColor: '#67C23A', icon: 'el-icon-play', iconColor: '#67C23A', iconBg: '#ecf5ff'},
+        { label: '维修数', value: 0, unit: '台', key: 'malfunctionNum', bgimg: require('@/assets/pcs/faultDevice.png'), valueColor: '#E6A23C', icon: 'el-icon-maintenance', iconColor: '#E6A23C', iconBg: '#ecf5ff'},
+      ],
+      yearRunRateOption: {},
+      monthRunRateOption: {},
+      yearFaultRateOption: {},
+      monthFaultRateOption: {},
+      yearRepairCountOption: {},
+      monthRepairCountOption: {}
+    }
+  },
+  computed: {},
+  mounted() {
+    this.updateTime()
+    this.initCharts()
+    this.updateTimer = setInterval(this.updateTime, 1000)
+    window.addEventListener('resize', this.handleResize)
+  },
+  beforeDestroy() {
+    clearInterval(this.updateTimer)
+    window.removeEventListener('resize', this.handleResize)
+  },
+  watch: {
+    isFullscreen: {
+      handler() {
+        this.$nextTick(() => this.handleResize())
+      }
+    }
+  },
+  methods: {
+    onFullscreen() {
+        this.isFullscreen = !this.isFullscreen;
+    },
+    handleResize() {
+      const { clientWidth, clientHeight } = document.documentElement
+      const containers = document.getElementsByClassName('box-container')
+      Array.from(containers).forEach((item) => {
+        if (this.isFullscreen) {
+          item.style.height = clientHeight + 'px'
+          item.style.width = clientWidth + 'px'
+        } else {
+          item.style.height = clientHeight - 100 + 'px'
+          item.style.width = clientWidth - 240 + 'px'
+        }
+      })
+      this.$nextTick(() => {
+        Object.values(this.$children).forEach(child => {
+          child.chartInstance && child.chartInstance.resize()
+        })
+      })
+    },
+
+    updateTime() {
+      const now = new Date()
+      const y = now.getFullYear()
+      const m = (now.getMonth() + 1)
+      const d = now.getDate()
+      this.currentDate = y + '-' + m + '-' + d
+      this.currentTime =
+        now.getHours().toString().padStart(2, '0') + ':' +
+        now.getMinutes().toString().padStart(2, '0') + ':' +
+        now.getSeconds().toString().padStart(2, '0')
+    },
+    async initCharts() {
+      const res = await deviceIndexStatistics()
+      console.log('res', res)
+      this.statsCardList.forEach((item) => {
+        item.value = res[item.key];
+      });
+
+      // ========== 年运行率趋势图 — nyxlList ==========
+      if (res.nyxlList && res.nyxlList.length) {
+        const { xData, seriesConfig } = formatTrendData(res.nyxlList, 'rate', palette)
+        this.yearRunRateOption = makeMultiLineOption(xData, seriesConfig, '%')
+      } else {
+        this.yearRunRateOption = makeMultiLineOption(MONTHS_12, [
+          { name: '暂无数据', data: new Array(12).fill(0), color: '#8ba3c7' }
+        ], '%')
+      }
+
+      // ========== 月运行率趋势图 — ryxlList(X轴=当月天数)==========
+      const daysOfMonth = getCurrentMonthDays()
+      if (res.ryxlList && res.ryxlList.length) {
+        const { xData: x2, seriesConfig: sc2 } = formatDayTrendData(res.ryxlList, 'rate', palette)
+        this.monthRunRateOption = makeMultiLineOption(x2, sc2, '%')
+      } else {
+        this.monthRunRateOption = makeMultiLineOption(daysOfMonth, [
+          { name: '暂无数据', data: new Array(daysOfMonth.length).fill(0), color: '#8ba3c7' }
+        ], '%')
+      }
+
+      // ========== 年故障率趋势图 — ngzlList ==========
+      if (res.ngzlList && res.ngzlList.length) {
+        const { xData: x3, seriesConfig: sc3 } = formatTrendData(res.ngzlList, 'rate', ['#f87171', '#facc15', '#8ba3c7'])
+        this.yearFaultRateOption = makeMultiLineOption(x3, sc3, '%')
+      } else {
+        this.yearFaultRateOption = makeMultiLineOption(MONTHS_12, [
+          { name: '暂无数据', data: new Array(12).fill(0), color: '#8ba3c7' }
+        ], '%')
+      }
+
+      // ========== 月故障率趋势图(暂用模拟,后续可接入其他数据源)==========
+      if(res.rgzList && res.rgzList.length) {
+        const { xData: x4, seriesConfig: sc4 } = formatDayTrendData(res.rgzList, 'rate', ['#f87171', '#facc15', '#8ba3c7'])
+        this.monthFaultRateOption = makeMultiLineOption(x4, sc4, '%')
+      } else {
+        this.monthFaultRateOption = makeMultiLineOption(daysOfMonth, [
+            { name: '暂无数据', data: new Array(daysOfMonth.length).fill(0), color: '#a78bfa' }
+        ], '%')
+      }
+      
+      // ========== 年维修次数趋势图 — ngzlList ==========
+      if (res.ngzlList && res.ngzlList.length) {
+        const { xData: x5, seriesConfig: sc5 } = formatTrendData(res.ngzlList, 'rate', ['#f87171', '#facc15', '#3b82f6'])
+        this.yearRepairCountOption = makeMultiLineOption(x5, sc5, '%')
+      } else {
+        this.yearRepairCountOption = makeMultiLineOption(MONTHS_12, [
+          { name: '暂无数据', data: new Array(12).fill(0), color: '#8ba3c7' }
+        ], '%')
+      }
+
+      // ========== 月维修次数趋势图(暂用默认占位,X轴=当月天数)==========
+      if(res.rgzList && res.rgzList.length) {
+        const { xData: x6, seriesConfig: sc6 } = formatDayTrendData(res.rgzList, 'rate', ['#f87171', '#facc15', '#8ba3c7'])
+        this.monthRepairCountOption = makeMultiLineOption(x6, sc6, '%')    
+      } else {
+        this.monthRepairCountOption = makeMultiLineOption(daysOfMonth, [
+            { name: '暂无数据', data: new Array(daysOfMonth.length).fill(0), color: '#a78bfa' }
+        ], '%')
+      }
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.box-container {
+  width: 100%;
+  min-height: 100vh;
+
+  .scroll-wrapper {
+    width: 100%;
+    min-height: 100vh;
+    background:
+      radial-gradient(ellipse at top, rgba(13, 45, 110, 0.3) 0%, transparent 50%),
+      radial-gradient(ellipse at bottom, rgba(8, 25, 65, 0.5) 0%, transparent 50%),
+      linear-gradient(180deg, #0a1630 0%, #0d1f4a 50%, #08152e 100%);
+    font-family: 'Microsoft YaHei', sans-serif;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .header-section {
+    flex-shrink: 0;
+    padding-top: 10px;
+  }
+
+  .box-header-scroll {
+    height: 80px;
+    background: url('~@/assets/pcs/header.png') no-repeat center top;
+    background-size: 100% 100%;
+    display: flex;
+    // align-items: center;
+    justify-content: center;
+    padding: 0 20px;
+    position: relative;
+    padding-top: 10px;
+
+    .logo {
+      padding-top: 10px;
+      position: absolute;
+      left: 80px;
+
+      ::v-deep .custom-select {
+        .el-input__inner {
+          background: #0d2960;
+          border: 1px solid #1a4a8a;
+          color: #fff;
+          height: 36px;
+          line-height: 36px;
+
+          &::placeholder { color: #8ba3c7; }
+        }
+        .el-input__suffix .el-input__icon { color: #8ba3c7; }
+      }
+    }
+
+    .header-center {
+      .title {
+        font-size: 28px;
+        font-weight: bold;
+        color: #fff;
+        text-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
+        letter-spacing: 4px;
+      }
+    }
+
+    .time {
+      position: absolute;
+      right: 30px;
+      display: flex;
+      align-items: center;
+
+      .time-display {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-end;
+
+        .date-text {
+          font-size: 14px;
+          color: #8ba3c7;
+        }
+        .time-text {
+          font-size: 20px;
+          font-weight: bold;
+          color: #fff;
+        }
+      }
+    }
+  }
+
+  .stats {
+    display: flex;
+    padding: 10px 30px;
+    justify-content: space-around;
+  }
+
+  .two-column-layout {
+    display: flex;
+    justify-content: space-between;
+    flex: 1;
+    min-height: 0;
+    padding: 0 20px 20px 20px;
+    background: url('~@/assets/pcs/back.png') no-repeat center top;
+    background-size: 100% 100%;
+    font-size: 0;
+    margin-top: 10px;
+
+    .left-section,
+    .right-section {
+      display: inline-flex;
+      flex-direction: column;
+      gap: 15px;
+      flex: 1;
+      min-height: 0;
+      font-size: 14px;
+      white-space: normal;
+      margin-right: 20px;
+
+      &:last-child { margin-right: 0; }
+    }
+  }
+
+  .panel {
+    padding: 15px;
+    min-height: 0;
+
+    .panel-title {
+      font-size: 18px;
+      font-family: 'YouSheBiaoTiHei', 'Microsoft YaHei', sans-serif;
+      color: #fff;
+      margin-bottom: 15px;
+      height: 42px;
+      padding-left: 40px;
+      position: relative;
+      display: flex;
+      align-items: center;
+      background: url('~@/assets/pcs/titleb.png') no-repeat center top;
+      background-size: 100% 100%;
+
+      .panel-icon {
+        position: absolute;
+        left: 15px;
+        width: 20px;
+        height: 25px;
+        background: url('~@/assets/pcs/titleIcon.png') no-repeat center top;
+        background-size: 100% 100%;
+      }
+    }
+  }
+
+  .data-panel {
+    flex: 0.6;
+    min-height: 200px;
+
+    .data-grid {
+      display: flex;
+      gap: 12px;
+      flex-wrap: wrap;
+      align-items: center;
+      justify-content: space-between;
+      height: calc(100% - 50px);
+
+      .data-item {
+        display: flex;
+        align-items: center;
+        padding-left: 15px;
+        width: 21%;
+        height: 60px;
+        background: url('~@/assets/pcs/leftItem.png') no-repeat center top;
+        background-size: 100% 100%;
+
+        .img {
+          width: 35px;
+          height: 35px;
+          background: url('~@/assets/pcs/icon.png') no-repeat center top;
+          background-size: 100% 100%;
+        }
+
+        .data-item_value {
+          display: flex;
+          flex-direction: column;
+          margin-left: 10px;
+        }
+
+        .data-label {
+          font-size: 12px;
+          color: #8ba3c7;
+        }
+
+        .data-value {
+          font-size: 18px;
+          font-weight: bold;
+          color: #b6ceff;
+          text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
+        }
+      }
+    }
+  }
+
+  .chart-panel {
+    flex: 1;
+    min-height: 250px;
+    display: flex;
+    flex-direction: column;
+
+    .chart-container {
+      width: 100%;
+      flex: 1;
+      min-height: 0;
+    }
+  }
+
+  .alert-panel {
+    flex: 0.6;
+    min-height: 220px;
+    display: flex;
+    flex-direction: column;
+
+    .alert-stats {
+      flex: 1;
+      display: flex;
+      align-items: center;
+      height: calc(100% - 50px);
+
+      .alert-cards {
+        width: 50%;
+        height: 100%;
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: space-around;
+        align-items: center;
+
+        .alert-card-item {
+          width: 42%;
+          height: 28%;
+          display: flex;
+          align-items: center;
+          padding-left: 15px;
+          background: url('~@/assets/pcs/leftItem.png') no-repeat center top;
+          background-size: 100% 100%;
+
+          .img {
+            width: 28px;
+            height: 28px;
+            background: url('~@/assets/pcs/icon.png') no-repeat center top;
+            background-size: 100% 100%;
+          }
+
+          .data-item_value {
+            display: flex;
+            flex-direction: column;
+            margin-left: 10px;
+          }
+
+          .data-label {
+            font-size: 11px;
+            color: #8ba3c7;
+          }
+
+          .data-value {
+            font-size: 16px;
+            font-weight: bold;
+            color: #b6ceff;
+            text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
+
+            small { font-size: 10px; color: #8ba3c7; margin-left: 2px; }
+          }
+        }
+      }
+
+      .alert-pie-chart {
+        width: 50%;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+
+        .alert-pie {
+          width: 100%;
+          height: 100%;
+          min-height: 180px;
+        }
+      }
+    }
+  }
+}
+
+//   .stat-cards-section {
+//     display: flex;
+//     gap: 20px;
+//     padding: 0 20px 10px 20px;
+//     flex-shrink: 0;
+
+//     .stat-card {
+//       flex: 1;
+//       height: 100px;
+//       background: linear-gradient(135deg, rgba(16, 42, 88, 0.9) 0%, rgba(10, 28, 65, 0.85) 100%);
+//       border: 1px solid rgba(42, 90, 170, 0.4);
+//       border-radius: 8px;
+//       display: flex;
+//       align-items: center;
+//       padding: 0 24px;
+//       position: relative;
+//       overflow: hidden;
+
+//       &::before {
+//         content: '';
+//         position: absolute;
+//         top: 0; left: 0; right: 0;
+//         height: 2px;
+//         background: linear-gradient(90deg, transparent, #00d4ff, transparent);
+//         box-shadow: 0 0 8px rgba(0, 212, 255, 0.6);
+//       }
+
+//       &.blue {
+//         .stat-icon {
+//           background: linear-gradient(135deg, #3b82f6, #2563eb);
+//           box-shadow: 0 0 15px rgba(59, 130, 246, 0.4);
+//         }
+//         .stat-value .num { color: #3b82f6; }
+//       }
+
+//       &.green {
+//         .stat-icon {
+//           background: linear-gradient(135deg, #22c55e, #16a34a);
+//           box-shadow: 0 0 15px rgba(34, 197, 94, 0.4);
+//         }
+//         .stat-value .num { color: #22c55e; }
+//       }
+
+//       &.red {
+//         .stat-icon {
+//           background: linear-gradient(135deg, #ef4444, #dc2626);
+//           box-shadow: 0 0 15px rgba(239, 68, 68, 0.4);
+//         }
+//         .stat-value .num { color: #ef4444; }
+//       }
+
+//       .stat-icon {
+//         width: 56px;
+//         height: 56px;
+//         border-radius: 50%;
+//         display: flex;
+//         align-items: center;
+//         justify-content: center;
+//         flex-shrink: 0;
+
+//         i {
+//           font-size: 24px;
+//           color: #fff;
+//         }
+//       }
+
+//       .stat-label {
+//         flex: 1;
+//         font-size: 16px;
+//         color: #8ba3c7;
+//         padding: 0 20px;
+//         text-align: center;
+//       }
+
+//       .stat-value {
+//         display: flex;
+//         align-items: baseline;
+
+//         .num {
+//           font-size: 36px;
+//           font-weight: 700;
+//           text-shadow: 0 0 10px rgba(0, 212, 255, 0.3);
+//         }
+
+//         .unit {
+//           font-size: 14px;
+//           color: #8ba3c7;
+//           margin-left: 4px;
+//         }
+//       }
+//     }
+//   }
+
+.fullscreen-toggle {
+    cursor: pointer;
+    font-size: 20px;
+    color: #8ba3c7;
+    transition: color 0.3s;
+    margin-left: 10px;
+
+    &:hover {
+    color: #3b82f6;
+    }
+}
+
+[v-cloak] { display: none; }
+</style>

+ 6 - 6
src/views/ledgerAssets/camera/index.vue

@@ -119,7 +119,7 @@
             headerAlign: 'center',
             align: 'center',
             showOverflowTooltip: true,
-            width: 150
+            width: 180
           },
           {
             prop: 'name',
@@ -135,7 +135,7 @@
             headerAlign: 'center',
             align: 'center',
             showOverflowTooltip: true,
-            width: 150
+            width: 180
           },
           {
             prop: 'modelName',
@@ -143,7 +143,7 @@
             headerAlign: 'center',
             align: 'center',
             showOverflowTooltip: true,
-            width: 150
+            width: 180
           },
           {
             prop: 'classifyName',
@@ -151,7 +151,7 @@
             headerAlign: 'center',
             align: 'center',
             showOverflowTooltip: true,
-            width: 150
+            width: 180
           },
           // {
           //   prop: 'loadDeviceName',
@@ -166,14 +166,14 @@
             label: '摄像机状态',
             align: 'center',
             showOverflowTooltip: true,
-            width: 100,
+            width: 120,
             slot: 'activityStatus'
           },
           {
             columnKey: 'action',
             fixed: 'right',
             label: '操作',
-            width: 150,
+            width: 160,
             align: 'center',
             resizable: false,
             slot: 'action',

+ 221 - 0
src/views/ledgerAssets/robot/components/detail.vue

@@ -0,0 +1,221 @@
+<template>
+  <ele-modal
+    :visible.sync="visible"
+    title="机器人详情"
+    width="1100px"
+    append-to-body
+    @close="cancel"
+  >
+    <el-tabs type="border-card" @tab-change="handleTabChange">
+      <el-tab-pane label="基础信息">
+        <el-form ref="form" :model="form" disabled label-width="120px" v-loading="loading">
+          <el-row>
+            <el-col :span="8">
+              <el-form-item label="机器人编号">
+                <span>{{ form.deviceCode }}</span>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="机器人类型">
+                <span>{{ form.classifyName }}</span>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="机器人名称">
+                <span>{{ form.name }}</span>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="8">
+              <el-form-item label="品牌">
+                <span>{{ form.brandName }}</span>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="型号">
+                <span>{{ form.modelName }}</span>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="8">
+              <el-form-item label="排序号">
+                <span>{{ form.deviceSort }}</span>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="状态">
+                <span :style="{ color: form.activityStatus == 1 ? '#0F83FF' : 'red' }">
+                  {{ form.activityStatus == 1 ? '启用' : '停用' }}
+                </span>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <div class="tit">连接配置</div>
+          <el-row>
+            <el-col :span="8">
+              <el-form-item label="IP">
+                <span>{{ form.extraInfo?.ip }}</span>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="端口">
+                <span>{{ form.extraInfo?.port }}</span>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="8">
+              <el-form-item label="账号">
+                <span>{{ form.extraInfo?.account }}</span>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="密码">
+                <span>{{ form.extraInfo?.password }}</span>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <div class="tit">固定负载</div>
+          <el-row>
+            <el-col :span="8">
+              <el-form-item label="可见光摄像机">
+                <span>{{ form.kjgLoadDeviceId }}</span>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="红外摄像机">
+                <span>{{ form.hwLoadDeviceId }}</span>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <div class="tit">其他负载</div>
+          <el-row>
+            <el-col :span="8">
+              <el-form-item label="①">
+                <span>{{ form.loadOtherDevice1 }}</span>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="②">
+                <span>{{ form.loadOtherDevice2 }}</span>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+      </el-tab-pane>
+      <el-tab-pane label="预置位信息">
+        <presetInfo ref="presetInfoRef" :deviceId="form.id" />
+      </el-tab-pane>
+    </el-tabs>
+
+    <template #footer>
+      <el-button @click="cancel">关 闭</el-button>
+    </template>
+  </ele-modal>
+</template>
+
+<script>
+import presetInfo from './presetInfo/index.vue'
+import * as RobotApi from '@/api/ledgerAssets/robot'
+
+export default {
+  name: 'RobotDetail',
+  components: {
+    presetInfo
+  },
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      form: {
+        name: undefined,
+        deviceCode: undefined,
+        classifyName: undefined,
+        brandName: undefined,
+        modelName: undefined,
+        deviceSort: undefined,
+        activityStatus: undefined,
+        kjgLoadDeviceId: undefined,
+        hwLoadDeviceId: undefined,
+        loadOtherDevice1: undefined,
+        loadOtherDevice2: undefined,
+        extraInfo: {
+          ip: undefined,
+          port: undefined,
+          account: undefined,
+          password: undefined
+        }
+      }
+    }
+  },
+  methods: {
+    /** 打开弹窗 */
+    open(id) {
+      this.visible = true
+      this.loading = true
+
+      RobotApi.getDevice(id)
+        .then((res) => {
+          this.form = res
+        })
+        .catch((e) => {
+          this.$message.error(e.message)
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+
+    /** 取消 */
+    cancel() {
+      this.resetForm()
+      this.visible = false
+    },
+
+    /** Tab切换 */
+    handleTabChange(index) {
+      if (index == 1) {
+        this.$refs.presetInfoRef?.preSetSuccess()
+      }
+    },
+
+    /** 重置表单 */
+    resetForm() {
+      this.form = {
+        name: undefined,
+        deviceCode: undefined,
+        classifyName: undefined,
+        brandName: undefined,
+        modelName: undefined,
+        deviceSort: undefined,
+        activityStatus: undefined,
+        kjgLoadDeviceId: undefined,
+        hwLoadDeviceId: undefined,
+        loadOtherDevice1: undefined,
+        loadOtherDevice2: undefined,
+        extraInfo: {
+          ip: undefined,
+          port: undefined,
+          account: undefined,
+          password: undefined
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.tit {
+  margin-left: 10px;
+  font-weight: bold;
+  border-left: 3px solid #9266f9;
+  padding-left: 10px;
+  margin-bottom: 14px;
+}
+</style>

+ 522 - 0
src/views/ledgerAssets/robot/components/formModel.vue

@@ -0,0 +1,522 @@
+<template>
+  <ele-modal
+    :visible.sync="visible"
+    :title="title"
+    width="1100px"
+    append-to-body
+    @close="cancel"
+  >
+    <el-form
+      ref="form"
+      :model="form"
+      :rules="rules"
+      label-width="120px"
+      class="create-form"
+      v-loading="loading"
+    >
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="所属场站" prop="stationName">
+            <el-input
+              v-model="form.stationName"
+              placeholder="请输入"
+              :maxlength="20"
+              class="!w-100%"
+              disabled
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="所属区域" prop="areaId">
+            <el-tree-select
+              v-model="form.areaId"
+              :data="areaTree"
+              :props="defaultProps"
+              check-strictly
+              default-expand-all
+              placeholder="请选择"
+              value-key="id"
+              @change="areaIdChange"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="机器人类型" prop="classifyCode">
+            <DictSelection
+              dictName="机器人类型"
+              v-model="form.classifyCode"
+              @itemChange="classifyCodeChange"
+            ></DictSelection>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="机器人名称" prop="name">
+            <el-input v-model="form.name" placeholder="请输入" :maxlength="20" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="机器人编号" prop="deviceCode">
+            <el-input
+              v-model="form.deviceCode"
+              placeholder="请输入"
+              :maxlength="50"
+              class="!w-100%"
+              disabled
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="品牌" prop="brandCode">
+            <DictSelection
+              dictName="机器人品牌"
+              v-model="form.brandCode"
+              @itemChange="brandCodeChange"
+            ></DictSelection>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="型号" prop="modelName">
+            <el-input
+              v-model="form.modelName"
+              placeholder="请输入"
+              :maxlength="20"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="排序号" prop="deviceSort">
+            <el-input-number
+              v-model="form.deviceSort"
+              :min="1"
+              :max="99999"
+              class="!w-100%"
+              controls-position="right"
+              placeholder="请输入"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="状态" prop="activityStatus">
+            <el-radio-group v-model="form.activityStatus" class="ml-4" style="flex-wrap: nowrap">
+              <el-radio :label="1">启用</el-radio>
+              <el-radio :label="0">停用</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <div class="tit">连接配置</div>
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="IP" prop="extraInfo.ip">
+            <el-input v-model="form.extraInfo.ip" placeholder="请输入" :maxlength="20" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="端口" prop="extraInfo.port">
+            <el-input-number
+              v-model="form.extraInfo.port"
+              :min="1"
+              :max="65535"
+              :maxlength="5"
+              controls-position="right"
+              placeholder="请输入"
+              class="!w-100%"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="账号" prop="extraInfo.account">
+            <el-input v-model="form.extraInfo.account" placeholder="请输入" :maxlength="30" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="密码" prop="extraInfo.password">
+            <el-input v-model="form.extraInfo.password" placeholder="请输入" :maxlength="30" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <div class="tit">固定负载</div>
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="可见光摄像机">
+            <el-select
+              v-model="form.kjgLoadDeviceId"
+              placeholder="请选择"
+              clearable
+              class="!w-100%"
+            >
+              <el-option
+                v-for="(item, index) in cameraTypeFzKjgList"
+                :key="index"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="红外摄像机">
+            <el-select
+              v-model="form.hwLoadDeviceId"
+              placeholder="请选择"
+              clearable
+              class="!w-100%"
+            >
+              <el-option
+                v-for="(item, index) in cameraTypeFzHwList"
+                :key="index"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <div class="tit">其他负载</div>
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="①">
+            <el-select
+              v-model="form.loadOtherDevice1"
+              @change="handleDevice($event, 2)"
+              placeholder="请选择"
+              clearable
+              class="!w-100%"
+            >
+              <el-option
+                v-for="(item, index) in deviceOtherList"
+                :key="index"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="②">
+            <el-select
+              v-model="form.loadOtherDevice2"
+              @change="handleDevice($event, 3)"
+              placeholder="请选择"
+              clearable
+              class="!w-100%"
+            >
+              <el-option
+                v-for="(item, index) in deviceOtherList"
+                :key="index"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="cancel">取消</el-button>
+      <el-button type="primary" @click="save" :loading="saving">
+        确定
+      </el-button>
+    </template>
+  </ele-modal>
+</template>
+
+<script>
+import dictMixins from '@/mixins/dictMixins';
+import * as RobotApi from '@/api/ledgerAssets/robot';
+import * as AreaApi from '@/api/basic/area';
+import { getCode } from '@/api/ruleManagement/matter';
+// import { getStationData } from '@/utils/auth';
+
+const defForm = {
+  name: '',
+  deviceCode: '',
+  classifyCode: '',
+  classifyName: '',
+  modelId: '',
+  modelName: '',
+  brandCode: '',
+  brandName: '',
+  stationName: '',
+  stationId: '',
+  areaId: '',
+  areaCode: '',
+  areaName: '',
+  deviceSort: 100,
+  activityStatus: 1,
+  kjgLoadDeviceId: undefined,
+  hwLoadDeviceId: undefined,
+  loadOtherDevice1: undefined,
+  loadOtherDevice2: undefined,
+  delayTime: '',
+  listLoad: [],
+  extraInfo: {
+    ip: '',
+    port: 8080,
+    account: '',
+    password: ''
+  }
+}
+
+export default {
+  name: 'RobotForm',
+  mixins: [dictMixins],
+  data() {
+    return {
+      visible: false,
+      title: '',
+      loading: false,
+      saving: false,
+      type: '',
+      areaTree: [],
+      cameraTypeFzKjgList: [],
+      cameraTypeFzHwList: [],
+      deviceOtherList: [],
+      defaultProps: {
+        children: 'children',
+        label: 'name'
+      },
+      form: { ...defForm },
+      rules: {
+        name: [{ required: true, message: '机器人名称不能为空', trigger: 'blur' }],
+        deviceCode: [{ required: true, message: '机器人编号不能为空', trigger: 'blur' }],
+        classifyCode: [{ required: true, message: '机器人类型不能为空', trigger: 'change' }],
+        deviceSort: [{ required: true, message: '排序号不能为空', trigger: 'change' }],
+        stationName: [{ required: true, message: '所属场站不能为空', trigger: 'change' }],
+        areaId: [{ required: true, message: '所属区域不能为空', trigger: 'change' }],
+        extraInfo: {
+          ip: [{ required: true, message: 'IP不能为空', trigger: 'blur' }],
+          port: [{ required: true, message: '端口不能为空', trigger: 'blur' }],
+          account: [{ required: true, message: '账号不能为空', trigger: 'blur' }],
+          password: [{ required: true, message: '密码不能为空', trigger: 'blur' }]
+        }
+      }
+    }
+  },
+  created() {
+    this.requestDict('机器人品牌');
+    this.requestDict('机器人类型');
+  },
+  methods: {
+    brandCodeChange(e) {
+      this.form.brandName = e.dictValue;
+    },
+    classifyCodeChange(e) {
+      this.form.classifyName = e.dictValue;
+    },
+
+    /** 选择关联设备事件 */
+    handleDevice(val, idx) {
+      if (idx == 2) {
+        if (val == this.form.loadOtherDevice2) {
+          this.form.loadOtherDevice2 = '';
+        }
+      } else if (idx == 3) {
+        if (val == this.form.loadOtherDevice1) {
+          this.form.loadOtherDevice1 = '';
+        }
+      }
+    },
+
+    /** 根据关联类型查询对应设备 */
+    async loadDeviceData() {
+      this.cameraTypeFzKjgList = [];
+      this.cameraTypeFzHwList = [];
+      this.deviceOtherList = [];
+      const allList = await RobotApi.getNonLoadDeviceList({});
+      if (allList && allList.length > 0) {
+        for (let obj of allList) {
+          if (!obj.loadId) {
+            if (obj.classifyCode == 'camera_type_fz_kjg') {
+              this.cameraTypeFzKjgList.push(obj);
+            } else if (obj.classifyCode == 'camera_type_fz_hw') {
+              this.cameraTypeFzHwList.push(obj);
+            } else {
+              this.deviceOtherList.push(obj);
+            }
+          }
+        }
+      }
+    },
+
+    /** 获得区域树 */
+    async getAreaTree(stationId) {
+      const data = await AreaApi.getAreaList({ stationId: stationId });
+      this.areaTree = this.$util.toTreeData({
+        data: data.list,
+        idField: 'id',
+        parentIdField: 'parentId'
+      });
+    },
+
+    /** 区域Change事件 */
+    areaIdChange(e) {
+      this.processingAreaId(e, this.areaTree);
+    },
+
+    processingAreaId(id, data) {
+      for (let i = 0; i < data.length; i++) {
+        if (data[i].id == id) {
+          this.form.areaCode = data[i].areaCode;
+          this.form.areaName = data[i].name;
+          return;
+        } else {
+          if (data[i].children && data[i].children.length) {
+            this.processingAreaId(id, data[i].children);
+          }
+        }
+      }
+    },
+
+    /** 打开弹窗 */
+    async open(type, id, station) {
+      this.type = type;
+      this.title = type === 'create' ? '新增机器人' : '编辑机器人';
+      this.form = { ...defForm };
+      this.areaTree = [];
+
+    //   await this.loadDeviceData();
+
+    //   if (getStationData()) {
+    //     this.form.stationName = getStationData().stationName;
+    //     this.form.stationId = getStationData().stationId || getStationData().id;
+    //   }
+      if (station) {
+        this.form.stationName = station.stationName;
+        this.form.stationId = station.stationId || station.id;
+        await this.getAreaTree(this.form.stationId);
+        if (station.stationId) {
+          this.form.areaCode = station.areaCode;
+          this.form.areaName = station.areaName;
+          this.form.areaId = station.id;
+        }
+      }
+
+      if (id) {
+        this.loading = true;
+        try {
+          const res = await RobotApi.getDevice(id);
+          // 排障,避免异常
+          if (!res.extraInfo) {
+            res.extraInfo = { ip: undefined, port: 8080, account: undefined, password: undefined };
+          }
+          // 处理负载设备
+          if (res.listLoad && res.listLoad.length > 0) {
+            for (let device of res.listLoad) {
+              let currLoadDeviceType = device.loadDeviceType,
+                currDeviceId = device.loadDeviceId,
+                loadDeviceName = device.loadDeviceName;
+              let pushObj = { name: loadDeviceName, id: currDeviceId };
+              if (currLoadDeviceType == 'camera_type_fz_kjg') {
+                res.kjgLoadDeviceId = currDeviceId;
+                this.cameraTypeFzKjgList.push(pushObj);
+              } else if (currLoadDeviceType == 'camera_type_fz_hw') {
+                res.hwLoadDeviceId = currDeviceId;
+                this.cameraTypeFzHwList.push(pushObj);
+              } else {
+                if (!res.loadOtherDevice1) {
+                  res.loadOtherDevice1 = currDeviceId;
+                } else {
+                  res.loadOtherDevice2 = currDeviceId;
+                }
+                this.deviceOtherList.push(pushObj);
+              }
+            }
+          } else {
+            res.kjgLoadDeviceId = undefined;
+            res.hwLoadDeviceId = undefined;
+            res.loadOtherDevice1 = undefined;
+            res.loadOtherDevice2 = undefined;
+            res.listLoad = [];
+          }
+          this.form = res;
+          if (this.form.stationId) {
+            await this.getAreaTree(this.form.stationId);
+          }
+        } catch (e) {
+          this.$message.error(e.message);
+        } finally {
+          this.loading = false;
+        }
+      } else {
+        this.form.deviceCode = await getCode('robot_code');
+      }
+
+      this.visible = true;
+    },
+
+    /** 取消 */
+    cancel() {
+      this.form = { ...defForm };
+      this.visible = false;
+    },
+
+    /** 保存 */
+    save() {
+      this.$refs.form.validate((valid) => {
+        if (valid) {
+          this.saving = true;
+          const data = { ...this.form };
+
+          // 构建负载列表
+          data.listLoad = [];
+          if (data.kjgLoadDeviceId) {
+            data.listLoad.push({ loadDeviceId: data.kjgLoadDeviceId });
+          }
+          if (data.hwLoadDeviceId) {
+            data.listLoad.push({ loadDeviceId: data.hwLoadDeviceId });
+          }
+          if (data.loadOtherDevice1) {
+            data.listLoad.push({ loadDeviceId: data.loadOtherDevice1 });
+          }
+          if (data.loadOtherDevice2) {
+            data.listLoad.push({ loadDeviceId: data.loadOtherDevice2 });
+          }
+
+          const action =
+            this.type === 'create' ? RobotApi.createDevice(data) : RobotApi.updateDevice(data);
+
+          action
+            .then(() => {
+              this.$message.success(this.type === 'create' ? '新增成功' : '修改成功');
+              this.cancel();
+              this.$emit('success');
+            })
+            .catch((e) => {
+              this.$message.error(e.message);
+            })
+            .finally(() => {
+              this.saving = false;
+            });
+        }
+      });
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.tit {
+  margin-left: 10px;
+  font-weight: bold;
+  border-left: 3px solid #9266f9;
+  padding-left: 10px;
+  margin-bottom: 14px;
+}
+</style>

+ 133 - 0
src/views/ledgerAssets/robot/components/presetInfo/detail.vue

@@ -0,0 +1,133 @@
+<template>
+  <ele-modal
+    :visible.sync="visible"
+    title="预置位详情"
+    width="50%"
+    append-to-body
+    @close="cancel"
+  >
+    <el-form ref="form" :model="form" label-width="120px" v-loading="loading" disabled>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="预置位名称">
+            <span>{{ form.pointName }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="16">
+          <el-form-item label="行车距离">
+            <el-progress :percentage="dirv">
+              <el-button text>3500</el-button>
+            </el-progress>
+          </el-form-item>
+        </el-col>
+        <el-col :span="4">
+          <el-form-item>
+            <span>{{ form.dirv }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="16">
+          <el-form-item label="升降距离">
+            <el-progress :percentage="lift">
+              <el-button text>3500</el-button>
+            </el-progress>
+          </el-form-item>
+        </el-col>
+        <el-col :span="4">
+          <el-form-item>
+            <span>{{ form.lift }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="排序号">
+            <span>{{ form.indexNo }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="cancel">关闭</el-button>
+    </template>
+  </ele-modal>
+</template>
+
+<script>
+import * as DevicePointApi from '@/api/ledgerAssets/device/presetInfo'
+
+const defForm = {
+  pointName: '',
+  dirv: 0,
+  lift: 0,
+  indexNo: 0
+}
+
+export default {
+  name: 'RobotPresetInfoDetail',
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      dirv: 0,
+      lift: 0,
+      form: { ...defForm }
+    }
+  },
+  methods: {
+    /** 打开弹窗 */
+    open(id) {
+      this.visible = true
+      this.loading = true
+
+      DevicePointApi.getDevicePoint(id)
+        .then((res) => {
+          const pointJson = JSON.parse(res.pointJson || '{}')
+          this.form = {
+            ...res,
+            dirv: pointJson.xcjl || 0,
+            lift: pointJson.sjjl || 0
+          }
+          this.dirv = Math.abs(Math.round((pointJson.xcjl || 0) / 35))
+          this.lift = Math.abs(Math.round((pointJson.sjjl || 0) / 35))
+        })
+        .catch((e) => {
+          this.$message.error(e.message)
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+
+    /** 取消 */
+    cancel() {
+      this.resetForm()
+      this.visible = false
+    },
+
+    /** 重置表单 */
+    resetForm() {
+      this.form = { ...defForm }
+      this.dirv = 0
+      this.lift = 0
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-col-4 {
+  .el-form-item {
+    :deep(.el-form-item__content) {
+      margin-left: 20px !important;
+    }
+  }
+}
+</style>

+ 226 - 0
src/views/ledgerAssets/robot/components/presetInfo/formModel.vue

@@ -0,0 +1,226 @@
+<template>
+  <ele-modal
+    :visible.sync="visible"
+    :title="title"
+    width="50%"
+    append-to-body
+    @close="cancel"
+  >
+    <el-form ref="form" :model="form" :rules="rules" label-width="120px" v-loading="loading">
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="预置位名称" prop="pointName">
+            <el-input v-model="form.pointName" placeholder="请输入" :maxlength="20" class="!w-100%" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="16">
+          <el-form-item label="行车距离">
+            <el-progress :percentage="dirv">
+              <el-button text>3500</el-button>
+            </el-progress>
+          </el-form-item>
+        </el-col>
+        <el-col :span="4">
+          <el-form-item prop="dirv">
+            <el-input
+              v-model="form.dirv"
+              @input="progressUpdate($event, 'dirv')"
+              placeholder="请输入"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="16">
+          <el-form-item label="升降距离">
+            <el-progress :percentage="lift">
+              <el-button text>3500</el-button>
+            </el-progress>
+          </el-form-item>
+        </el-col>
+        <el-col :span="4">
+          <el-form-item prop="lift">
+            <el-input
+              v-model="form.lift"
+              @input="progressUpdate($event, 'lift')"
+              placeholder="请输入"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="排序号" prop="indexNo">
+            <el-input-number
+              v-model="form.indexNo"
+              class="!w-100%"
+              placeholder="请输入"
+              :min="1"
+              :max="99999"
+              controls-position="right"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="cancel">取消</el-button>
+      <el-button type="primary" @click="save" :loading="saving">确定</el-button>
+    </template>
+  </ele-modal>
+</template>
+
+<script>
+import * as DevicePointApi from '@/api/ledgerAssets/device/presetInfo'
+
+const defForm = {
+  pointName: '',
+  dirv: 2000,
+  lift: 2000,
+  indexNo: 100
+}
+
+export default {
+  name: 'RobotPresetInfoForm',
+  props: {
+    dId: ''
+  },
+  data() {
+    return {
+      visible: false,
+      title: '',
+      loading: false,
+      saving: false,
+      type: '',
+      dirv: 57,
+      lift: 57,
+      form: { ...defForm },
+      rules: {
+        pointName: [{ required: true, message: '预置位名称不能为空', trigger: 'blur' }],
+        dirv: [{ required: true, message: '行车距离不能为空', trigger: 'blur' }],
+        lift: [{ required: true, message: '升降距离不能为空', trigger: 'blur' }],
+        indexNo: [{ required: true, message: '排序号不能为空', trigger: 'change' }]
+      }
+    }
+  },
+  methods: {
+    /** 进度更新 */
+    progressUpdate(value, type) {
+      let num = parseInt(value) || 0
+      if (num > 3500) {
+        num = 3500
+        this.form[type] = num
+      } else if (num < -3500) {
+        num = -3500
+        this.form[type] = num
+      }
+      this[type] = Math.abs(Math.round(num / 35))
+    },
+
+    /** 打开弹窗 */
+    open(type, id) {
+      this.type = type
+      this.title = type === 'create' ? '新增预置位' : '编辑预置位'
+      this.resetForm()
+
+      if (id) {
+        this.loading = true
+        DevicePointApi.getDevicePoint(id)
+          .then((res) => {
+            const pointJson = JSON.parse(res.pointJson || '{}')
+            this.form = {
+              ...res,
+              dirv: pointJson.xcjl || 0,
+              lift: pointJson.sjjl || 0
+            }
+            this.progressUpdate(this.form.dirv, 'dirv')
+            this.progressUpdate(this.form.lift, 'lift')
+          })
+          .catch((e) => {
+            this.$message.error(e.message)
+          })
+          .finally(() => {
+            this.loading = false
+          })
+      }
+
+      this.visible = true
+    },
+
+    /** 取消 */
+    cancel() {
+      this.resetForm()
+      this.visible = false
+    },
+
+    /** 重置表单 */
+    resetForm() {
+      this.form = { ...defForm }
+      this.dirv = 57
+      this.lift = 57
+      this.$refs.form?.resetFields()
+    },
+
+    /** 保存 */
+    save() {
+      this.$refs.form.validate((valid) => {
+        if (valid) {
+          this.saving = true
+          const data = {
+            ...this.form,
+            deviceId: this.dId,
+            pointJson: JSON.stringify({
+              xcjl: this.form.dirv,
+              sjjl: this.form.lift
+            })
+          }
+          delete data.dirv
+          delete data.lift
+
+          const action =
+            this.type === 'create'
+              ? DevicePointApi.createDevicePoint(data)
+              : DevicePointApi.updateDevicePoint(data)
+
+          action
+            .then(() => {
+              this.$message.success(this.type === 'create' ? '新增成功' : '修改成功')
+              this.cancel()
+              this.$emit('success')
+            })
+            .catch((e) => {
+              this.$message.error(e.message)
+            })
+            .finally(() => {
+              this.saving = false
+            })
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-col-4 {
+  .el-form-item {
+    :deep(.el-form-item__content) {
+      margin-left: 20px !important;
+    }
+  }
+}
+
+.tit {
+  margin-left: 10px;
+  font-weight: bold;
+  border-left: 3px solid #9266f9;
+  padding-left: 10px;
+  margin-bottom: 14px;
+}
+</style>

+ 170 - 0
src/views/ledgerAssets/robot/components/presetInfo/index.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="ele-body">
+    <el-card shadow="never">
+      <ele-pro-table
+        ref="table"
+        :columns="columns"
+        :datasource="datasource"
+        :selection.sync="selection"
+      >
+        <!-- 表头工具栏 -->
+        <template v-slot:toolbar>
+          <el-button
+            type="danger"
+            icon="el-icon-delete"
+            class="ele-btn-icon"
+            v-if="selection.length"
+            @click="handleBatchDelete"
+          >
+            批量删除
+          </el-button>
+        </template>
+
+        <!-- 操作列 -->
+        <template v-slot:action="{ row }">
+          <el-link
+            type="primary"
+            :underline="false"
+            icon="el-icon-delete"
+            :style="{ color: delColor }"
+            @click="handleDelete(row.pointId)"
+          >
+            删除
+          </el-link>
+        </template>
+      </ele-pro-table>
+    </el-card>
+
+    <!-- 新增编辑弹窗 -->
+    <formModel ref="formRef" :dId="deviceId" @success="reload" />
+  </div>
+</template>
+
+<script>
+import * as DevicePointApi from '@/api/ledgerAssets/device/presetInfo'
+import formModel from './formModel.vue'
+
+export default {
+  name: 'RobotPresetInfo',
+  components: {
+    formModel
+  },
+  props: {
+    deviceId: ''
+  },
+  data() {
+    return {
+      delColor: '#F56C6C',
+      selection: [],
+      // 表格列配置
+      columns: [
+        {
+          width: 45,
+          type: 'selection',
+          columnKey: 'selection',
+          align: 'center',
+          fixed: 'left'
+        },
+        {
+          columnKey: 'index',
+          label: '序号',
+          type: 'index',
+          width: 60,
+          align: 'center'
+        },
+        {
+          prop: 'pointName',
+          label: '预置位名称',
+          align: 'left',
+          minWidth: 200,
+          showOverflowTooltip: true
+        },
+        {
+          prop: 'indexNo',
+          label: '排序号',
+          align: 'center',
+          width: 100
+        },
+        {
+          columnKey: 'action',
+          fixed: 'right',
+          label: '操作',
+          width: 80,
+          align: 'center',
+          slot: 'action'
+        }
+      ]
+    }
+  },
+  methods: {
+    /* 表格数据源 */
+    datasource({ page, limit }) {
+      return DevicePointApi.getDevicePointPage({
+        pageNo: page,
+        pageSize: limit,
+        deviceId: this.deviceId
+      }).then((res) => {
+        return {
+          list: res.list.map((item) => ({
+            ...item,
+            pointJson: JSON.parse(item.pointJson || '{}')
+          })),
+          total: res.total
+        }
+      })
+    },
+
+    /* 刷新表格 */
+    reload() {
+      this.$refs.table.reload()
+    },
+
+    /** 预置位初始化 */
+    preSetSuccess() {
+      this.reload()
+    },
+
+    /** 打开表单 */
+    openForm(type, id) {
+      this.$refs.formRef.open(type, id)
+    },
+
+    /** 删除按钮操作 */
+    handleDelete(id) {
+      this.$confirm('是否确认删除该预置位?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(async () => {
+          await DevicePointApi.deleteDevicePoint(id)
+          this.$message.success('删除成功')
+          this.reload()
+        })
+        .catch(() => {})
+    },
+
+    /** 批量删除按钮操作 */
+    handleBatchDelete() {
+      if (this.selection.length === 0) {
+        this.$message.warning('请选择要删除的数据项')
+        return
+      }
+      const ids = this.selection.map((item) => item.pointId)
+      this.$confirm('是否确认删除选中的预置位?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(async () => {
+          await DevicePointApi.batchDeleteDevicePoint(ids)
+          this.$message.success('删除成功')
+          this.reload()
+        })
+        .catch(() => {})
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 44 - 0
src/views/ledgerAssets/robot/components/robot-search.vue

@@ -0,0 +1,44 @@
+<!-- 搜索表单 -->
+<template>
+  <seekPage :seekList="seekList" :formLength="4" @search="search"></seekPage>
+</template>
+
+<script>
+export default {
+  name: 'RobotSearch',
+  components: {},
+  data() {
+    return {}
+  },
+  computed: {
+    seekList() {
+      return [
+        {
+          label: '机器人名称:',
+          value: 'name',
+          type: 'input',
+          placeholder: '请输入'
+        },
+        {
+          label: '设备品牌:',
+          value: 'brandCode',
+          type: 'DictSelection',
+          dictName: '机器人品牌'
+        },
+        {
+          label: '设备状态:',
+          value: 'activityStatus',
+          type: 'DictSelection',
+          dictName: '设备管理状态'
+        }
+      ]
+    }
+  },
+  methods: {
+    /* 搜索 */
+    search(e) {
+      this.$emit('search', { ...e })
+    }
+  }
+}
+</script>

+ 296 - 0
src/views/ledgerAssets/robot/index.vue

@@ -0,0 +1,296 @@
+<template>
+  <div class="ele-body">
+    <!-- <el-row :gutter="20">
+      <el-col :span="4" :xs="24">
+        <el-card shadow="never" class="h-1/1">
+          <StationTree
+            ref="treeRef"
+            @node-click="handleDeptNodeClick"
+            @nodeChange="handleDeptNodeClick"
+            :isCheckbox="true"
+            :isArea="true"
+          />
+        </el-card>
+      </el-col>
+      <el-col :span="20" :xs="24"> -->
+        <el-card shadow="never">
+          <!-- 搜索表单 -->
+          <robot-search ref="searchRef" @search="reload" />
+          <!-- 数据表格 -->
+          <ele-pro-table
+            ref="table"
+            :columns="columns"
+            :datasource="datasource"
+            :selection.sync="selection"
+            :cache-key="cacheKeyUrl"
+            :response="{
+              dataName: 'data.records',
+              countName: 'data.total'
+            }"
+          >
+            <!-- 表头工具栏 -->
+            <template v-slot:toolbar>
+              <el-button
+                type="primary"
+                icon="el-icon-plus"
+                class="ele-btn-icon"
+                v-if="$hasPermission('ledgerAssets:robot:create')"
+                @click="openForm('create')"
+              >
+                新建
+              </el-button>
+              <el-button
+                type="danger"
+                icon="el-icon-delete"
+                class="ele-btn-icon"
+                v-if="
+                  $hasPermission('ledgerAssets:robot:delete') && selection.length
+                "
+                @click="handleBatchDelete"
+              >
+                批量删除
+              </el-button>
+            </template>
+
+            <!-- 设备状态列 -->
+            <template v-slot:activityStatus="{ row }">
+              <el-switch
+                v-model="row.activityStatus"
+                :active-value="1"
+                :inactive-value="0"
+                @change="handleStatusChange(row)"
+              />
+            </template>
+
+            <!-- 操作列 -->
+            <template v-slot:action="{ row }">
+              <el-link
+                type="primary"
+                :underline="false"
+                icon="el-icon-edit"
+                v-if="$hasPermission('ledgerAssets:robot:update')"
+                @click="openForm('update', row.id)"
+              >
+                编辑
+              </el-link>
+              <el-link
+                type="primary"
+                :underline="false"
+                icon="el-icon-delete"
+                :style="{ color: delColor }"
+                v-if="$hasPermission('ledgerAssets:robot:delete')"
+                @click="handleDelete(row.id)"
+              >
+                删除
+              </el-link>
+            </template>
+          </ele-pro-table>
+        </el-card>
+      <!-- </el-col>
+    </el-row> -->
+
+    <!-- 详情弹窗 -->
+    <detail ref="detailFormRef" />
+
+    <!-- 新增编辑弹窗 -->
+    <formModel ref="formModelRef" @success="reload" />
+  </div>
+</template>
+
+<script>
+import * as RobotApi from '@/api/ledgerAssets/robot';
+import formModel from './components/formModel.vue';
+import robotSearch from './components/robot-search.vue';
+import detail from './components/detail.vue';
+// import StationTree from '@/views/isp/ispLine/area/stationTree.vue';
+
+export default {
+  name: 'LedgerRobot',
+  components: {
+    formModel,
+    robotSearch,
+    detail,
+    // StationTree
+  },
+  data() {
+    return {
+      cacheKeyUrl: 'ledgerRobotTable',
+      delColor: '#F56C6C',
+      selection: [],
+      stationData: {},
+      // 表格列配置
+      columns: [
+        {
+          width: 45,
+          type: 'selection',
+          columnKey: 'selection',
+          align: 'center',
+          fixed: 'left'
+        },
+        {
+          columnKey: 'index',
+          label: '序号',
+          type: 'index',
+          width: 55,
+          align: 'center',
+          showOverflowTooltip: true,
+          fixed: 'left'
+        },
+        {
+          prop: 'deviceCode',
+          label: '机器人编号',
+          headerAlign: 'center',
+          align: 'center',
+          showOverflowTooltip: true,
+          width: 150
+        },
+        {
+          prop: 'name',
+          label: '机器人名称',
+          headerAlign: 'center',
+          align: 'center',
+          showOverflowTooltip: true,
+          minWidth: 200
+        },
+        {
+          prop: 'brandName',
+          label: '设备品牌',
+          headerAlign: 'center',
+          align: 'center',
+          showOverflowTooltip: true,
+          width: 150
+        },
+        {
+          prop: 'modelName',
+          label: '设备型号',
+          headerAlign: 'center',
+          align: 'center',
+          showOverflowTooltip: true,
+          width: 150
+        },
+        {
+          prop: 'kjgName',
+          label: '可见光摄像机',
+          headerAlign: 'center',
+          align: 'center',
+          showOverflowTooltip: true,
+          width: 150
+        },
+        {
+          prop: 'hwName',
+          label: '红外摄影机',
+          headerAlign: 'center',
+          align: 'center',
+          showOverflowTooltip: true,
+          width: 150
+        },
+        {
+          prop: 'activityStatus',
+          label: '设备状态',
+          align: 'center',
+          showOverflowTooltip: true,
+          width: 100,
+          slot: 'activityStatus'
+        },
+        {
+          columnKey: 'action',
+          fixed: 'right',
+          label: '操作',
+          width: 150,
+          align: 'center',
+          resizable: false,
+          slot: 'action',
+          showOverflowTooltip: true
+        }
+      ],
+      loading: false
+    };
+  },
+  methods: {
+    /* 表格数据源 */
+    async datasource({ page, limit, where }) {
+      return await RobotApi.getDevicePage({
+        pageNo: page,
+        pageSize: limit,
+        // stationId: this.stationData.stationId || this.stationData.id || undefined,
+        // areaId: this.stationData.stationId ? this.stationData.id : undefined,
+        ...where
+      });
+    },
+
+    /* 场站树节点点击 */
+    handleDeptNodeClick(data) {
+      this.stationData = data;
+      this.reload();
+    },
+
+    /* 刷新表格 */
+    reload(where) {
+      this.$refs.table.reload({ page: 1, where });
+    },
+
+    /** 添加/修改操作 */
+    openForm(type, id) {
+      this.$refs.formModelRef.open(type, id, this.stationData);
+    },
+
+    /** 状态切换 */
+    handleStatusChange(row) {
+      const text = row.activityStatus === 1 ? '启用' : '停用';
+      this.$confirm('确认要' + text + '机器人"' + row.name + '"吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(async () => {
+          row.extInfo = row.extInfo || '{}';
+          await RobotApi.updateDevice(row);
+          this.$message.success(text + '成功');
+          this.reload();
+        })
+        .catch(() => {
+          row.activityStatus = row.activityStatus === 1 ? 0 : 1;
+        });
+    },
+
+    /** 删除按钮操作 */
+    handleDelete(id) {
+      this.$confirm('是否确认删除该数据项?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(async () => {
+          await RobotApi.deletedDevice(id);
+          this.$message.success('删除成功');
+          this.reload();
+        })
+        .catch(() => {});
+    },
+
+    /** 批量删除按钮操作 */
+    handleBatchDelete() {
+      if (this.selection.length === 0) {
+        this.$message.warning('请选择要删除的数据项');
+        return;
+      }
+      this.$confirm('是否确认删除选中的数据项?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(async () => {
+          const ids = this.selection.map((item) => item.id);
+          for (const id of ids) {
+            await RobotApi.deletedDevice(id);
+          }
+          this.$message.success('删除成功');
+          this.reload();
+        })
+        .catch(() => {});
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 167 - 0
src/views/ledgerAssets/subDevice/components/detail.vue

@@ -0,0 +1,167 @@
+<template>
+  <ele-modal
+    :visible.sync="visible"
+    title="附属设备详情"
+    width="1100px"
+    append-to-body
+    @close="cancel"
+  >
+    <el-form ref="form" :model="form" disabled label-width="120px" v-loading="loading">
+      <el-row>
+        <el-col :span="8">
+          <el-form-item label="设备编号">
+            <span>{{ form.deviceCode }}</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="设备类型">
+            <span>{{ form.classifyName }}</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="设备名称">
+            <span>{{ form.name }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="8">
+          <el-form-item label="设备品牌">
+            <span>{{ form.brandName }}</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="设备型号">
+            <span>{{ form.modelName }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="8">
+          <el-form-item label="排序号">
+            <span>{{ form.deviceSort }}</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="状态">
+            <span :style="{ color: form.activityStatus == 1 ? '#0F83FF' : 'red' }">
+              {{ form.activityStatus == 1 ? '启用' : '停用' }}
+            </span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <div class="tit">连接配置</div>
+      <el-row>
+        <el-col :span="8">
+          <el-form-item label="IP">
+            <span>{{ form.extraInfo?.ip }}</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="端口">
+            <span>{{ form.extraInfo?.port }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="8">
+          <el-form-item label="账号">
+            <span>{{ form.extraInfo?.account }}</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="密码">
+            <span>{{ form.extraInfo?.password }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="cancel">关 闭</el-button>
+    </template>
+  </ele-modal>
+</template>
+
+<script>
+import * as DeviceApi from '@/api/ledgerAssets/device'
+
+export default {
+  name: 'SubDeviceDetail',
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      form: {
+        name: undefined,
+        deviceCode: undefined,
+        classifyName: undefined,
+        brandName: undefined,
+        modelName: undefined,
+        deviceSort: undefined,
+        activityStatus: undefined,
+        extraInfo: {
+          ip: undefined,
+          port: undefined,
+          account: undefined,
+          password: undefined
+        }
+      }
+    }
+  },
+  methods: {
+    /** 打开弹窗 */
+    open(id) {
+      this.visible = true
+      this.loading = true
+
+      DeviceApi.getDevice(id)
+        .then((res) => {
+          this.form = res
+        })
+        .catch((e) => {
+          this.$message.error(e.message)
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+
+    /** 取消 */
+    cancel() {
+      this.resetForm()
+      this.visible = false
+    },
+
+    /** 重置表单 */
+    resetForm() {
+      this.form = {
+        name: undefined,
+        deviceCode: undefined,
+        classifyName: undefined,
+        brandName: undefined,
+        modelName: undefined,
+        deviceSort: undefined,
+        activityStatus: undefined,
+        extraInfo: {
+          ip: undefined,
+          port: undefined,
+          account: undefined,
+          password: undefined
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.tit {
+  margin-left: 10px;
+  font-weight: bold;
+  border-left: 3px solid #9266f9;
+  padding-left: 10px;
+  margin-bottom: 14px;
+}
+</style>

+ 349 - 0
src/views/ledgerAssets/subDevice/components/formModel.vue

@@ -0,0 +1,349 @@
+<template>
+  <ele-modal
+    :visible.sync="visible"
+    :title="title"
+    width="1100px"
+    append-to-body
+    @close="cancel"
+  >
+    <el-form
+      ref="form"
+      :model="form"
+      :rules="rules"
+      label-width="120px"
+      class="create-form"
+      v-loading="loading"
+    >
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="所属场站" prop="stationName">
+            <el-input
+              v-model="form.stationName"
+              placeholder="请输入"
+              :maxlength="20"
+              class="!w-100%"
+              disabled
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="所属区域" prop="areaId">
+            <el-tree-select
+              v-model="form.areaId"
+              :data="areaTree"
+              :props="defaultProps"
+              check-strictly
+              default-expand-all
+              placeholder="请选择"
+              value-key="id"
+              @change="areaIdChange"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="设备编号" prop="deviceCode">
+            <el-input
+              v-model="form.deviceCode"
+              placeholder="请输入"
+              :maxlength="50"
+              class="!w-100%"
+              disabled
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="设备类型" prop="classifyCode">
+            <DictSelection
+              dictName="设备管理设备类型"
+              v-model="form.classifyCode"
+              @itemChange="classifyCodeChange"
+            ></DictSelection>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="设备名称" prop="name">
+            <el-input v-model="form.name" placeholder="请输入" :maxlength="20" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="品牌" prop="brandCode">
+            <DictSelection
+              dictName="设备品牌"
+              v-model="form.brandCode"
+              @itemChange="brandCodeChange"
+            ></DictSelection>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="型号" prop="modelName">
+            <el-input
+              v-model="form.modelName"
+              placeholder="请输入"
+              :maxlength="20"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="排序号" prop="deviceSort">
+            <el-input-number
+              v-model="form.deviceSort"
+              :min="1"
+              :max="99999"
+              class="!w-100%"
+              controls-position="right"
+              placeholder="请输入"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="状态" prop="activityStatus">
+            <el-radio-group v-model="form.activityStatus" class="ml-4" style="flex-wrap: nowrap">
+              <el-radio :label="1">启用</el-radio>
+              <el-radio :label="0">停用</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <div class="tit">连接配置</div>
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="IP" prop="extraInfo.ip">
+            <el-input v-model="form.extraInfo.ip" placeholder="请输入" :maxlength="20" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="端口" prop="extraInfo.port">
+            <el-input-number
+              v-model="form.extraInfo.port"
+              :min="1"
+              :max="65535"
+              :maxlength="5"
+              controls-position="right"
+              placeholder="请输入"
+              class="!w-100%"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-form-item label="账号" prop="extraInfo.account">
+            <el-input v-model="form.extraInfo.account" placeholder="请输入" :maxlength="30" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="密码" prop="extraInfo.password">
+            <el-input v-model="form.extraInfo.password" placeholder="请输入" :maxlength="30" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="cancel">取消</el-button>
+      <el-button type="primary" @click="save" :loading="saving">
+        确定
+      </el-button>
+    </template>
+  </ele-modal>
+</template>
+
+<script>
+import dictMixins from '@/mixins/dictMixins';
+import * as DeviceApi from '@/api/ledgerAssets/device';
+import * as AreaApi from '@/api/basic/area';
+import { getCode } from '@/api/ruleManagement/matter';
+// import { getStationData } from '@/utils/auth';
+
+const defForm = {
+  name: '',
+  deviceCode: '',
+  classifyCode: '',
+  classifyName: '',
+  modelId: '',
+  modelName: '',
+  brandCode: '',
+  brandName: '',
+  stationName: '',
+  stationId: '',
+  areaId: '',
+  areaCode: '',
+  areaName: '',
+  deviceSort: 100,
+  activityStatus: 1,
+  listLoad: [],
+  extraInfo: {
+    ip: '',
+    port: 9000,
+    account: '',
+    password: ''
+  }
+}
+
+export default {
+  name: 'SubDeviceForm',
+  mixins: [dictMixins],
+  data() {
+    return {
+      visible: false,
+      title: '',
+      loading: false,
+      saving: false,
+      type: '',
+      areaTree: [],
+      defaultProps: {
+        children: 'children',
+        label: 'name'
+      },
+      form: { ...defForm },
+      rules: {
+        name: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }],
+        deviceCode: [{ required: true, message: '设备编号不能为空', trigger: 'blur' }],
+        classifyCode: [{ required: true, message: '设备类型不能为空', trigger: 'change' }],
+        deviceSort: [{ required: true, message: '排序号不能为空', trigger: 'change' }],
+        stationName: [{ required: true, message: '所属场站不能为空', trigger: 'change' }],
+        areaId: [{ required: true, message: '所属区域不能为空', trigger: 'change' }]
+      }
+    }
+  },
+  created() {
+    this.requestDict('设备品牌');
+    this.requestDict('设备类型');
+  },
+  methods: {
+    brandCodeChange(e) {
+      this.form.brandName = e.dictValue;
+    },
+    classifyCodeChange(e) {
+      this.form.classifyName = e.dictValue;
+    },
+
+    /** 获得区域树 */
+    async getAreaTree(stationId) {
+      const data = await AreaApi.getAreaList({ stationId: stationId });
+      this.areaTree = this.$util.toTreeData({
+        data: data.list,
+        idField: 'id',
+        parentIdField: 'parentId'
+      });
+    },
+
+    /** 区域Change事件 */
+    areaIdChange(e) {
+      this.processingAreaId(e, this.areaTree);
+    },
+
+    processingAreaId(id, data) {
+      for (let i = 0; i < data.length; i++) {
+        if (data[i].id == id) {
+          this.form.areaCode = data[i].areaCode;
+          this.form.areaName = data[i].name;
+          return;
+        } else {
+          if (data[i].children && data[i].children.length) {
+            this.processingAreaId(id, data[i].children);
+          }
+        }
+      }
+    },
+
+    /** 打开弹窗 */
+    async open(type, id, station) {
+      this.type = type;
+      this.title = type === 'create' ? '新增附属设备' : '编辑附属设备';
+      this.form = { ...defForm };
+      this.areaTree = [];
+
+    //   if (getStationData()) {
+    //     this.form.stationName = getStationData().stationName;
+    //     this.form.stationId = getStationData().stationId || getStationData().id;
+    //   }
+      if (station) {
+        this.form.stationName = station.stationName;
+        this.form.stationId = station.stationId || station.id;
+        await this.getAreaTree(this.form.stationId);
+        if (station.stationId) {
+          this.form.areaCode = station.areaCode;
+          this.form.areaName = station.areaName;
+          this.form.areaId = station.id;
+        }
+      }
+
+      if (id) {
+        this.loading = true;
+        try {
+          const res = await DeviceApi.getDevice(id);
+          if (!res.extraInfo) {
+            res.extraInfo = { ip: undefined, port: 9000, account: undefined, password: undefined };
+          }
+          this.form = res;
+          if (this.form.stationId) {
+            await this.getAreaTree(this.form.stationId);
+          }
+        } catch (e) {
+          this.$message.error(e.message);
+        } finally {
+          this.loading = false;
+        }
+      } else {
+        this.form.deviceCode = await getCode('affiliate_device_code');
+      }
+
+      this.visible = true;
+    },
+
+    /** 取消 */
+    cancel() {
+      this.form = { ...defForm };
+      this.visible = false;
+    },
+
+    /** 保存 */
+    save() {
+      this.$refs.form.validate((valid) => {
+        if (valid) {
+          this.saving = true;
+          const data = { ...this.form };
+
+          const action =
+            this.type === 'create' ? DeviceApi.createDevice(data) : DeviceApi.updateDevice(data);
+
+          action
+            .then(() => {
+              this.$message.success(this.type === 'create' ? '新增成功' : '修改成功');
+              this.cancel();
+              this.$emit('success');
+            })
+            .catch((e) => {
+              this.$message.error(e.message);
+            })
+            .finally(() => {
+              this.saving = false;
+            });
+        }
+      });
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.tit {
+  margin-left: 10px;
+  font-weight: bold;
+  border-left: 3px solid #9266f9;
+  padding-left: 10px;
+  margin-bottom: 14px;
+}
+</style>

+ 133 - 0
src/views/ledgerAssets/subDevice/components/presetInfo/detail.vue

@@ -0,0 +1,133 @@
+<template>
+  <ele-modal
+    :visible.sync="visible"
+    title="预置位详情"
+    width="50%"
+    append-to-body
+    @close="cancel"
+  >
+    <el-form ref="form" :model="form" label-width="120px" v-loading="loading" disabled>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="预置位名称">
+            <span>{{ form.pointName }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="16">
+          <el-form-item label="行车距离">
+            <el-progress :percentage="dirv">
+              <el-button text>3500</el-button>
+            </el-progress>
+          </el-form-item>
+        </el-col>
+        <el-col :span="4">
+          <el-form-item>
+            <span>{{ form.dirv }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="16">
+          <el-form-item label="升降距离">
+            <el-progress :percentage="lift">
+              <el-button text>3500</el-button>
+            </el-progress>
+          </el-form-item>
+        </el-col>
+        <el-col :span="4">
+          <el-form-item>
+            <span>{{ form.lift }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="排序号">
+            <span>{{ form.indexNo }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="cancel">关闭</el-button>
+    </template>
+  </ele-modal>
+</template>
+
+<script>
+import * as DevicePointApi from '@/api/ledgerAssets/device/presetInfo'
+
+const defForm = {
+  pointName: '',
+  dirv: 0,
+  lift: 0,
+  indexNo: 0
+}
+
+export default {
+  name: 'SubDevicePresetInfoDetail',
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      dirv: 0,
+      lift: 0,
+      form: { ...defForm }
+    }
+  },
+  methods: {
+    /** 打开弹窗 */
+    open(id) {
+      this.visible = true
+      this.loading = true
+
+      DevicePointApi.getDevicePoint(id)
+        .then((res) => {
+          const pointJson = JSON.parse(res.pointJson || '{}')
+          this.form = {
+            ...res,
+            dirv: pointJson.xcjl || 0,
+            lift: pointJson.sjjl || 0
+          }
+          this.dirv = Math.abs(Math.round((pointJson.xcjl || 0) / 35))
+          this.lift = Math.abs(Math.round((pointJson.sjjl || 0) / 35))
+        })
+        .catch((e) => {
+          this.$message.error(e.message)
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+
+    /** 取消 */
+    cancel() {
+      this.resetForm()
+      this.visible = false
+    },
+
+    /** 重置表单 */
+    resetForm() {
+      this.form = { ...defForm }
+      this.dirv = 0
+      this.lift = 0
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-col-4 {
+  .el-form-item {
+    :deep(.el-form-item__content) {
+      margin-left: 20px !important;
+    }
+  }
+}
+</style>

+ 226 - 0
src/views/ledgerAssets/subDevice/components/presetInfo/formModel.vue

@@ -0,0 +1,226 @@
+<template>
+  <ele-modal
+    :visible.sync="visible"
+    :title="title"
+    width="50%"
+    append-to-body
+    @close="cancel"
+  >
+    <el-form ref="form" :model="form" :rules="rules" label-width="120px" v-loading="loading">
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="预置位名称" prop="pointName">
+            <el-input v-model="form.pointName" placeholder="请输入" :maxlength="20" class="!w-100%" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="16">
+          <el-form-item label="行车距离">
+            <el-progress :percentage="dirv">
+              <el-button text>3500</el-button>
+            </el-progress>
+          </el-form-item>
+        </el-col>
+        <el-col :span="4">
+          <el-form-item prop="dirv">
+            <el-input
+              v-model="form.dirv"
+              @input="progressUpdate($event, 'dirv')"
+              placeholder="请输入"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="16">
+          <el-form-item label="升降距离">
+            <el-progress :percentage="lift">
+              <el-button text>3500</el-button>
+            </el-progress>
+          </el-form-item>
+        </el-col>
+        <el-col :span="4">
+          <el-form-item prop="lift">
+            <el-input
+              v-model="form.lift"
+              @input="progressUpdate($event, 'lift')"
+              placeholder="请输入"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="排序号" prop="indexNo">
+            <el-input-number
+              v-model="form.indexNo"
+              class="!w-100%"
+              placeholder="请输入"
+              :min="1"
+              :max="99999"
+              controls-position="right"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="cancel">取消</el-button>
+      <el-button type="primary" @click="save" :loading="saving">确定</el-button>
+    </template>
+  </ele-modal>
+</template>
+
+<script>
+import * as DevicePointApi from '@/api/ledgerAssets/device/presetInfo'
+
+const defForm = {
+  pointName: '',
+  dirv: 2000,
+  lift: 2000,
+  indexNo: 100
+}
+
+export default {
+  name: 'SubDevicePresetInfoForm',
+  props: {
+    dId: ''
+  },
+  data() {
+    return {
+      visible: false,
+      title: '',
+      loading: false,
+      saving: false,
+      type: '',
+      dirv: 57,
+      lift: 57,
+      form: { ...defForm },
+      rules: {
+        pointName: [{ required: true, message: '预置位名称不能为空', trigger: 'blur' }],
+        dirv: [{ required: true, message: '行车距离不能为空', trigger: 'blur' }],
+        lift: [{ required: true, message: '升降距离不能为空', trigger: 'blur' }],
+        indexNo: [{ required: true, message: '排序号不能为空', trigger: 'change' }]
+      }
+    }
+  },
+  methods: {
+    /** 进度更新 */
+    progressUpdate(value, type) {
+      let num = parseInt(value) || 0
+      if (num > 3500) {
+        num = 3500
+        this.form[type] = num
+      } else if (num < -3500) {
+        num = -3500
+        this.form[type] = num
+      }
+      this[type] = Math.abs(Math.round(num / 35))
+    },
+
+    /** 打开弹窗 */
+    open(type, id) {
+      this.type = type
+      this.title = type === 'create' ? '新增预置位' : '编辑预置位'
+      this.resetForm()
+
+      if (id) {
+        this.loading = true
+        DevicePointApi.getDevicePoint(id)
+          .then((res) => {
+            const pointJson = JSON.parse(res.pointJson || '{}')
+            this.form = {
+              ...res,
+              dirv: pointJson.xcjl || 0,
+              lift: pointJson.sjjl || 0
+            }
+            this.progressUpdate(this.form.dirv, 'dirv')
+            this.progressUpdate(this.form.lift, 'lift')
+          })
+          .catch((e) => {
+            this.$message.error(e.message)
+          })
+          .finally(() => {
+            this.loading = false
+          })
+      }
+
+      this.visible = true
+    },
+
+    /** 取消 */
+    cancel() {
+      this.resetForm()
+      this.visible = false
+    },
+
+    /** 重置表单 */
+    resetForm() {
+      this.form = { ...defForm }
+      this.dirv = 57
+      this.lift = 57
+      this.$refs.form?.resetFields()
+    },
+
+    /** 保存 */
+    save() {
+      this.$refs.form.validate((valid) => {
+        if (valid) {
+          this.saving = true
+          const data = {
+            ...this.form,
+            deviceId: this.dId,
+            pointJson: JSON.stringify({
+              xcjl: this.form.dirv,
+              sjjl: this.form.lift
+            })
+          }
+          delete data.dirv
+          delete data.lift
+
+          const action =
+            this.type === 'create'
+              ? DevicePointApi.createDevicePoint(data)
+              : DevicePointApi.updateDevicePoint(data)
+
+          action
+            .then(() => {
+              this.$message.success(this.type === 'create' ? '新增成功' : '修改成功')
+              this.cancel()
+              this.$emit('success')
+            })
+            .catch((e) => {
+              this.$message.error(e.message)
+            })
+            .finally(() => {
+              this.saving = false
+            })
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-col-4 {
+  .el-form-item {
+    :deep(.el-form-item__content) {
+      margin-left: 20px !important;
+    }
+  }
+}
+
+.tit {
+  margin-left: 10px;
+  font-weight: bold;
+  border-left: 3px solid #9266f9;
+  padding-left: 10px;
+  margin-bottom: 14px;
+}
+</style>

+ 193 - 0
src/views/ledgerAssets/subDevice/components/presetInfo/index.vue

@@ -0,0 +1,193 @@
+<template>
+  <div class="ele-body">
+    <el-card shadow="never">
+      <ele-pro-table
+        ref="table"
+        :columns="columns"
+        :datasource="datasource"
+        :selection.sync="selection"
+      >
+        <!-- 表头工具栏 -->
+        <template v-slot:toolbar>
+          <el-button
+            type="primary"
+            icon="el-icon-plus"
+            class="ele-btn-icon"
+            @click="openForm('create')"
+          >
+            新建
+          </el-button>
+          <el-button
+            type="danger"
+            icon="el-icon-delete"
+            class="ele-btn-icon"
+            v-if="selection.length"
+            @click="handleBatchDelete"
+          >
+            批量删除
+          </el-button>
+        </template>
+
+        <!-- 操作列 -->
+        <template v-slot:action="{ row }">
+          <el-link
+            type="primary"
+            :underline="false"
+            icon="el-icon-edit"
+            @click="openForm('update', row.pointId)"
+          >
+            编辑
+          </el-link>
+          <el-link
+            type="primary"
+            :underline="false"
+            icon="el-icon-delete"
+            :style="{ color: delColor }"
+            @click="handleDelete(row.pointId)"
+          >
+            删除
+          </el-link>
+        </template>
+      </ele-pro-table>
+    </el-card>
+
+    <!-- 新增编辑弹窗 -->
+    <formModel ref="formRef" :dId="deviceId" @success="reload" />
+  </div>
+</template>
+
+<script>
+import * as DevicePointApi from '@/api/ledgerAssets/device/presetInfo'
+import formModel from './formModel.vue'
+
+export default {
+  name: 'SubDevicePresetInfo',
+  components: {
+    formModel
+  },
+  props: {
+    deviceId: ''
+  },
+  data() {
+    return {
+      delColor: '#F56C6C',
+      selection: [],
+      // 表格列配置
+      columns: [
+        {
+          width: 45,
+          type: 'selection',
+          columnKey: 'selection',
+          align: 'center',
+          fixed: 'left'
+        },
+        {
+          columnKey: 'index',
+          label: '序号',
+          type: 'index',
+          width: 60,
+          align: 'center'
+        },
+        {
+          prop: 'pointName',
+          label: '预置位名称',
+          align: 'left',
+          minWidth: 200,
+          showOverflowTooltip: true
+        },
+        {
+          prop: 'pointJson',
+          label: '预置位信息(单位:m)',
+          align: 'center',
+          width: 200,
+          showOverflowTooltip: true
+        },
+        {
+          prop: 'indexNo',
+          label: '排序号',
+          align: 'center',
+          width: 100
+        },
+        {
+          columnKey: 'action',
+          fixed: 'right',
+          label: '操作',
+          width: 150,
+          align: 'center',
+          slot: 'action'
+        }
+      ]
+    }
+  },
+  methods: {
+    /* 表格数据源 */
+    datasource({ page, limit }) {
+      return DevicePointApi.getDevicePointPage({
+        pageNo: page,
+        pageSize: limit,
+        deviceId: this.deviceId
+      }).then((res) => {
+        return {
+          list: res.list.map((item) => ({
+            ...item,
+            pointJson: JSON.parse(item.pointJson || '{}')
+          })),
+          total: res.total
+        }
+      })
+    },
+
+    /* 刷新表格 */
+    reload() {
+      this.$refs.table.reload()
+    },
+
+    /** 预置位初始化 */
+    preSetSuccess() {
+      this.reload()
+    },
+
+    /** 打开表单 */
+    openForm(type, id) {
+      this.$refs.formRef.open(type, id)
+    },
+
+    /** 删除按钮操作 */
+    handleDelete(id) {
+      this.$confirm('是否确认删除该预置位?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(async () => {
+          await DevicePointApi.deleteDevicePoint(id)
+          this.$message.success('删除成功')
+          this.reload()
+        })
+        .catch(() => {})
+    },
+
+    /** 批量删除按钮操作 */
+    handleBatchDelete() {
+      if (this.selection.length === 0) {
+        this.$message.warning('请选择要删除的数据项')
+        return
+      }
+      const ids = this.selection.map((item) => item.pointId)
+      this.$confirm('是否确认删除选中的预置位?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(async () => {
+          await DevicePointApi.batchDeleteDevicePoint(ids)
+          this.$message.success('删除成功')
+          this.reload()
+        })
+        .catch(() => {})
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 50 - 0
src/views/ledgerAssets/subDevice/components/subdevice-search.vue

@@ -0,0 +1,50 @@
+<!-- 搜索表单 -->
+<template>
+  <seekPage :seekList="seekList" :formLength="4" @search="search"></seekPage>
+</template>
+
+<script>
+export default {
+  name: 'SubDeviceSearch',
+  components: {},
+  data() {
+    return {}
+  },
+  computed: {
+    seekList() {
+      return [
+        {
+          label: '设备名称:',
+          value: 'name',
+          type: 'input',
+          placeholder: '请输入'
+        },
+        {
+          label: '设备品牌:',
+          value: 'brandCode',
+          type: 'DictSelection',
+          dictName: '设备品牌'
+        },
+        {
+          label: '设备类型:',
+          value: 'classifyCode',
+          type: 'DictSelection',
+          dictName: '设备类型'
+        },
+        {
+          label: '设备状态:',
+          value: 'activityStatus',
+          type: 'DictSelection',
+          dictName: '设备管理状态'
+        }
+      ]
+    }
+  },
+  methods: {
+    /* 搜索 */
+    search(e) {
+      this.$emit('search', { ...e })
+    }
+  }
+}
+</script>

+ 296 - 0
src/views/ledgerAssets/subDevice/index.vue

@@ -0,0 +1,296 @@
+<template>
+  <div class="ele-body">
+    <!-- <el-row :gutter="20">
+      <el-col :span="4" :xs="24">
+        <el-card shadow="never" class="h-1/1">
+          <StationTree
+            ref="treeRef"
+            @node-click="handleDeptNodeClick"
+            @nodeChange="handleDeptNodeClick"
+            :isCheckbox="true"
+            :isArea="true"
+          />
+        </el-card>
+      </el-col>
+      <el-col :span="20" :xs="24"> -->
+        <el-card shadow="never">
+          <!-- 搜索表单 -->
+          <subdevice-search ref="searchRef" @search="reload" />
+          <!-- 数据表格 -->
+          <ele-pro-table
+            ref="table"
+            :columns="columns"
+            :datasource="datasource"
+            :selection.sync="selection"
+            :cache-key="cacheKeyUrl"
+            :response="{
+              dataName: 'data.records',
+              countName: 'data.total'
+            }"
+          >
+            <!-- 表头工具栏 -->
+            <template v-slot:toolbar>
+              <el-button
+                type="primary"
+                icon="el-icon-plus"
+                class="ele-btn-icon"
+                v-if="$hasPermission('ledgerAssets:subDevice:create')"
+                @click="openForm('create')"
+              >
+                新建
+              </el-button>
+              <el-button
+                type="danger"
+                icon="el-icon-delete"
+                class="ele-btn-icon"
+                v-if="
+                  $hasPermission('ledgerAssets:subDevice:delete') && selection.length
+                "
+                @click="handleBatchDelete"
+              >
+                批量删除
+              </el-button>
+            </template>
+
+            <!-- 设备状态列 -->
+            <template v-slot:activityStatus="{ row }">
+              <el-switch
+                v-model="row.activityStatus"
+                :active-value="1"
+                :inactive-value="0"
+                @change="handleStatusChange(row)"
+              />
+            </template>
+
+            <!-- 操作列 -->
+            <template v-slot:action="{ row }">
+              <el-link
+                type="primary"
+                :underline="false"
+                icon="el-icon-edit"
+                v-if="$hasPermission('ledgerAssets:subDevice:update')"
+                @click="openForm('update', row.id)"
+              >
+                编辑
+              </el-link>
+              <el-link
+                type="primary"
+                :underline="false"
+                icon="el-icon-delete"
+                :style="{ color: delColor }"
+                v-if="$hasPermission('ledgerAssets:subDevice:delete')"
+                @click="handleDelete(row.id)"
+              >
+                删除
+              </el-link>
+            </template>
+          </ele-pro-table>
+        </el-card>
+      <!-- </el-col>
+    </el-row> -->
+
+    <!-- 详情弹窗 -->
+    <detail ref="detailFormRef" />
+
+    <!-- 新增编辑弹窗 -->
+    <formModel ref="formModelRef" @success="reload" />
+  </div>
+</template>
+
+<script>
+import * as DeviceApi from '@/api/ledgerAssets/device';
+import formModel from './components/formModel.vue';
+import subdeviceSearch from './components/subdevice-search.vue';
+import detail from './components/detail.vue';
+// import StationTree from '@/views/isp/ispLine/area/stationTree.vue';
+
+export default {
+  name: 'LedgerSubDevice',
+  components: {
+    formModel,
+    subdeviceSearch,
+    detail,
+    // StationTree
+  },
+  data() {
+    return {
+      cacheKeyUrl: 'ledgerSubDeviceTable',
+      delColor: '#F56C6C',
+      selection: [],
+      stationData: {},
+      // 表格列配置
+      columns: [
+        {
+          width: 45,
+          type: 'selection',
+          columnKey: 'selection',
+          align: 'center',
+          fixed: 'left'
+        },
+        {
+          columnKey: 'index',
+          label: '序号',
+          type: 'index',
+          width: 55,
+          align: 'center',
+          showOverflowTooltip: true,
+          fixed: 'left'
+        },
+        {
+          prop: 'deviceCode',
+          label: '设备编号',
+          headerAlign: 'center',
+          align: 'center',
+          showOverflowTooltip: true,
+          width: 150
+        },
+        {
+          prop: 'name',
+          label: '设备名称',
+          headerAlign: 'center',
+          align: 'center',
+          showOverflowTooltip: true,
+          minWidth: 200
+        },
+        {
+          prop: 'brandName',
+          label: '设备品牌',
+          headerAlign: 'center',
+          align: 'center',
+          showOverflowTooltip: true,
+          width: 150
+        },
+        {
+          prop: 'modelName',
+          label: '设备型号',
+          headerAlign: 'center',
+          align: 'center',
+          showOverflowTooltip: true,
+          width: 150
+        },
+        {
+          prop: 'classifyName',
+          label: '设备类型',
+          headerAlign: 'center',
+          align: 'center',
+          showOverflowTooltip: true,
+          width: 150
+        },
+        {
+          prop: 'loadDeviceName',
+          label: '关联设备',
+          headerAlign: 'center',
+          align: 'center',
+          showOverflowTooltip: true,
+          width: 150
+        },
+        {
+          prop: 'activityStatus',
+          label: '设备状态',
+          align: 'center',
+          showOverflowTooltip: true,
+          width: 100,
+          slot: 'activityStatus'
+        },
+        {
+          columnKey: 'action',
+          fixed: 'right',
+          label: '操作',
+          width: 150,
+          align: 'center',
+          resizable: false,
+          slot: 'action',
+          showOverflowTooltip: true
+        }
+      ],
+      loading: false
+    };
+  },
+  methods: {
+    /* 表格数据源 */
+    async datasource({ page, limit, where }) {
+      return await DeviceApi.getDevicePage({
+        pageNo: page,
+        pageSize: limit,
+        stationId: this.stationData.stationId || this.stationData.id || undefined,
+        areaId: this.stationData.stationId ? this.stationData.id : undefined,
+        ...where
+      });
+    },
+
+    /* 场站树节点点击 */
+    handleDeptNodeClick(data) {
+      this.stationData = data;
+      this.reload();
+    },
+
+    /* 刷新表格 */
+    reload(where) {
+      this.$refs.table.reload({ page: 1, where });
+    },
+
+    /** 添加/修改操作 */
+    openForm(type, id) {
+      this.$refs.formModelRef.open(type, id, this.stationData);
+    },
+
+    /** 状态切换 */
+    handleStatusChange(row) {
+      const text = row.activityStatus === 1 ? '启用' : '停用';
+      this.$confirm('确认要' + text + '设备"' + row.name + '"吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(async () => {
+          row.extInfo = row.extInfo || '{}';
+          await DeviceApi.updateDevice(row);
+          this.$message.success(text + '成功');
+          this.reload();
+        })
+        .catch(() => {
+          row.activityStatus = row.activityStatus === 1 ? 0 : 1;
+        });
+    },
+
+    /** 删除按钮操作 */
+    handleDelete(id) {
+      this.$confirm('是否确认删除该数据项?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(async () => {
+          await DeviceApi.deleteDevice(id);
+          this.$message.success('删除成功');
+          this.reload();
+        })
+        .catch(() => {});
+    },
+
+    /** 批量删除按钮操作 */
+    handleBatchDelete() {
+      if (this.selection.length === 0) {
+        this.$message.warning('请选择要删除的数据项');
+        return;
+      }
+      this.$confirm('是否确认删除选中的数据项?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(async () => {
+          const ids = this.selection.map((item) => item.id);
+          for (const id of ids) {
+            await DeviceApi.deleteDevice(id);
+          }
+          this.$message.success('删除成功');
+          this.reload();
+        })
+        .catch(() => {});
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped></style>