浏览代码

Merge branch 'master' into professional2

yanglzh 1 年之前
父节点
当前提交
f7e89f6bd1

+ 30 - 0
src/api/iotCard/index.ts

@@ -0,0 +1,30 @@
+
+/*
+ * @Author: vera_min vera_min@163.com
+ * @Date: 2023-10-23 22:45:52
+ * @LastEditors: vera_min vera_min@163.com
+ * @LastEditTime: 2023-10-25 10:20:08
+ * @FilePath: /sagoo-admin-ui/src/api/ice104/index.ts
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
+import { get, post, del, put } from '/@/utils/request';
+
+export default {
+  // 物联网卡列表
+  simCard: {
+    getList: (params: object) => get('/sim_collect/list', params),
+    addItem: (data: object) => post('/device/add', data),
+    editItem: (data: object) => put('/device/edit', data),
+    deleteItem: (data: object) => del('/sim_collect/delete', data),
+    detailItem: (params: object) => get('/sim_collect/one', params),
+    getFlowDataByDateRange: (data: object) => post('/sim_collect/flow_date', data),
+  },
+  dashboard: {
+    getFlowDataByDateRange: (data: object) => post('/sim_history_traffic/date', data),
+    getTop10Data: (data: object) => post('/sim_traffic_statis/top_flow', data),
+    getFlowData: (data: object) => get('/sim_traffic_statis/get', data)
+  },
+  platform: {
+    getList: (params: object) => get('/sim_factory/list', params),
+  }
+}

+ 1 - 0
src/api/ota/index.ts

@@ -23,6 +23,7 @@ export default {
     del: (ids: number) => del('/operate/ota_strategy/delete', {ids}),
     add: (data: any) => post('/operate/ota_strategy/add', data),
     edit: (data: any) => put('/operate/ota_strategy/edit', data),
+    distribute: (data: any) => post('/operate/ota_strategy/distribute', data)
   },
   device: {
     getList: (data: any) => get('/operate/ota_detail/list', data),

+ 3 - 4
src/components/vue3cron/vue3cron.vue

@@ -1,9 +1,8 @@
 
 <template>
   <div class="vue3-cron-div">
-    <!-- <el-button class="language" type="text" @click="state.language = state.language === 'en' ? 'cn' : 'en'">{{
-            state.language === 'en' ? 'cn' : 'en'
-        }}</el-button> -->
+    <el-button class="language" type="text" >
+  </el-button>
     <el-tabs type="border-card">
       <el-tab-pane>
         <template #label>
@@ -255,7 +254,7 @@
         <span> cron预览: </span>
         <el-tag type="primary">
           {{ state.cron }}
-        </el-tag>
+        </el-tag> <span>{秒数} {分钟} {小时} {日期} {月份} {?} {年份}</span>
       </div>
       <div class="buttonDiv" style="text-align: right;">
         <el-button type="primary" size="mini" @click.stop="handleChange">{{ state.text.Save }}</el-button>

+ 2 - 1
src/i18n/lang/zh-cn.ts

@@ -94,7 +94,8 @@ export default {
 		dropdown3: '404',
 		dropdown4: '401',
 		dropdown5: '退出登录',
-		dropdown6: '代码仓库',
+		dropdown7: '代码仓库',
+		dropdown8: '在线文档',
 		searchPlaceholder: '菜单搜索:支持中文、路由路径',
 		newTitle: '通知',
 		newBtn: '全部已读',

+ 3 - 0
src/layout/navBars/breadcrumb/user.vue

@@ -65,6 +65,7 @@
         <el-dropdown-menu>
           <!-- <el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item> -->
           <el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
+          <el-dropdown-item command="document">{{ $t('message.user.dropdown8') }}</el-dropdown-item>
           <el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
         </el-dropdown-menu>
       </template>
@@ -177,6 +178,8 @@ export default defineComponent({
           .catch(() => { });
       } else if (path === 'wareHouse') {
         window.open('https://sagoo.cn');
+      } else if (path === 'document') {
+        window.open('https://iotdoc.sagoo.cn/')
       } else {
         router.push(path);
       }

+ 8 - 0
src/theme/dark.scss

@@ -154,6 +154,14 @@
 	.home-card-item {
 		border: 1px solid var(--next-border-color-light) !important;
 	}
+	.flow-line-wrap {
+		background-color: transparent!important;
+		border: 1px solid var(--next-border-color-light) !important;
+		
+		.text {
+		 color: var(--next-bg-menuBar-black) !important;
+		}
+	}
 
 	.el-card {
 		background-color: var(--el-color-white) !important;

+ 10 - 0
src/utils/common.ts

@@ -59,3 +59,13 @@ export function selectDictLabel(data: any[], value: string): string {
     })
     return actions.join('');
 }
+
+export function formatSize(kb:number) {
+    if (kb <= 0) {
+        return "0 B";
+    }
+
+    const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
+    const i = Math.floor(Math.log(kb) / Math.log(1024));
+    return (kb / Math.pow(1024, i)).toFixed(2) + " " + units[i];
+}

+ 54 - 63
src/views/iot/device/instance/detail.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="page bg">
+  <div class="page bg page-full">
     <div class="content">
       <div class="cont_box">
         <div class="title">设备:{{ detail.name }}</div>
@@ -9,11 +9,11 @@
       </div>
     </div>
 
-    <div class="content-box">
+    <div class="content-box page-full-part page-full">
       <el-tabs v-model="activeName" @tab-click="handleClick">
 
         <el-tab-pane label="运行状态" name="3">
-          <div style=" display: flex; padding: 10px;flex-wrap: wrap;">
+          <div style=" display: flex;flex-wrap: wrap;">
             <div class="ant-card">
               <div class="ant-card-body">
                 <div class="cardflex">
@@ -316,35 +316,35 @@
           <el-form label-width="110px">
 
             <!--            <FromData :DataList="Datalist" v-if="Datalist && Datalist.length > 0" disable="true"></FromData>-->
-<!--            <div class="pro-box">-->
-<!--              <div class="protitle">设备档案</div>-->
-<!--              <div>-->
-<!--                <el-button type="primary" v-auth="'edit'" @click="onOpenEditAsset">编辑</el-button>-->
-<!--              </div>-->
-<!--            </div>-->
-
-<!--            <div class="ant-descriptions-view">-->
-<!--              <table>-->
-<!--                <tbody>-->
-<!--                <tr class="ant-descriptions-row" v-for="(item, index) in dataList" :key="index">-->
-<!--                  <th class="ant-descriptions-item-label ant-descriptions-item-colon">{{ item.title }}</th>-->
-<!--                  <td class="ant-descriptions-item-content" colspan="1">-->
-<!--                    <view v-if="item.types === 'file'">-->
-<!--                      <img :src="deviceAssetMetadata[item.name]" class="avatar" />-->
-<!--                    </view>-->
-<!--                    <view v-else>-->
-<!--                      <view v-if="item.pattern">-->
-<!--                        <el-link :href="deviceAssetMetadata[item.name]" type="primary" target="_blank">{{ deviceAssetMetadata[item.name] }}</el-link>-->
-<!--                      </view>-->
-<!--                      <view v-else>-->
-<!--                        {{ deviceAssetMetadata[item.name] }}-->
-<!--                      </view>-->
-<!--                    </view>-->
-<!--                  </td>-->
-<!--                </tr>-->
-<!--                </tbody>-->
-<!--              </table>-->
-<!--            </div>-->
+            <!--            <div class="pro-box">-->
+            <!--              <div class="protitle">设备档案</div>-->
+            <!--              <div>-->
+            <!--                <el-button type="primary" v-auth="'edit'" @click="onOpenEditAsset">编辑</el-button>-->
+            <!--              </div>-->
+            <!--            </div>-->
+
+            <!--            <div class="ant-descriptions-view">-->
+            <!--              <table>-->
+            <!--                <tbody>-->
+            <!--                <tr class="ant-descriptions-row" v-for="(item, index) in dataList" :key="index">-->
+            <!--                  <th class="ant-descriptions-item-label ant-descriptions-item-colon">{{ item.title }}</th>-->
+            <!--                  <td class="ant-descriptions-item-content" colspan="1">-->
+            <!--                    <view v-if="item.types === 'file'">-->
+            <!--                      <img :src="deviceAssetMetadata[item.name]" class="avatar" />-->
+            <!--                    </view>-->
+            <!--                    <view v-else>-->
+            <!--                      <view v-if="item.pattern">-->
+            <!--                        <el-link :href="deviceAssetMetadata[item.name]" type="primary" target="_blank">{{ deviceAssetMetadata[item.name] }}</el-link>-->
+            <!--                      </view>-->
+            <!--                      <view v-else>-->
+            <!--                        {{ deviceAssetMetadata[item.name] }}-->
+            <!--                      </view>-->
+            <!--                    </view>-->
+            <!--                  </td>-->
+            <!--                </tr>-->
+            <!--                </tbody>-->
+            <!--              </table>-->
+            <!--            </div>-->
 
             <div class="pro-box">
               <div class="protitle">设备档案</div>
@@ -472,11 +472,14 @@ interface TableDataState {
 }
 export default defineComponent({
   name: 'deviceEditPro',
-  components: {EditAssetRef, FromData, SubDeviceMutipleBind, SubDevice, EditDic, EditAttr, EditFun, EditEvent, EditTab, devantd, ListDic, functionCom, setAttr },
+  components: { EditAssetRef, FromData, SubDeviceMutipleBind, SubDevice, EditDic, EditAttr, EditFun, EditEvent, EditTab, devantd, ListDic, functionCom, setAttr },
 
   setup(prop, context) {
     const logqueryRef = ref();
 
+    // 属性列表,查询保留小数位使用
+    const propertyMap = new Map()
+
     const array_list = ref([]);
     const route = useRoute();
     const editDicRef = ref();
@@ -569,12 +572,7 @@ export default defineComponent({
         //加载全部属性
         datahub.node.getpropertyList({ key: state.detail.product.key }).then((re: any) => {
           array_list.value = re;
-        });
-
-        //第一次加载
-        api.model.property(state.tableData.param).then((res: any) => {
-          state.tableData.data = res.Data;
-          state.tableData.total = res.Total;
+          re.forEach((item: any) => propertyMap.set(item.key, item?.valueType));
         });
 
         // 加载对应设备档案
@@ -859,23 +857,22 @@ export default defineComponent({
       }
     };
 
-    const getValueText = (key, value) => {
-      let data = array_list.value;
-      for (let i = 0; i < data.length; i++) {
-        const item = data[i];
-        if (item.key === key) {
-          if (item.valueType.type === "enum") {
-            const option = item.valueType.elements.find((element) => element.value === value);
-            if (option) {
-              return option.text;
-            }
-          } else {
-            return value;
-          }
+    const getValueText = (key: String, value: String) => {
+      const item = propertyMap.get(key)
+
+      if (!item) return value
+
+      if (item.type === "enum") {
+        const option = item.elements.find((element: any) => element.value === value);
+        if (option) {
+          return option.text;
         }
+      } else if (item?.type === 'float' && item?.decimals) {
+        //  根据属性确定保留小数位数
+        return Number(value).toFixed(item.decimals)
+      } else {
+        return value;
       }
-
-      return value;
     }
     const getStatusText = (name, value) => {
       let data = array_list.value;
@@ -1164,13 +1161,7 @@ tr {
   border-color: inherit;
 }
 
-.wu-box {
-  border: #e8e8e8 solid 1px;
-  padding: 20px;
-  width: 100%;
-}
-
-.wu-box .wu-title {
+.wu-title {
   display: flex;
   flex-direction: row;
   justify-content: space-between;
@@ -1178,7 +1169,7 @@ tr {
   border-bottom: #e8e8e8 1px solid;
 }
 
-.wu-box .wu-title .title {
+.wu-title .title {
   font-size: 18px;
 }
 
@@ -1199,7 +1190,7 @@ tr {
 }
 
 .ant-card-body {
-  padding: 24px;
+  padding: 12px;
   zoom: 1;
 }
 

+ 737 - 0
src/views/iot/iotCard/dashboard.vue

@@ -0,0 +1,737 @@
+<!-- 物联网卡-详情 -->
+<template>
+  <div class="page">
+		<el-card shadow="nover" class="page-full-part">
+			<div class="select-wrap">
+				<el-select v-model="types" placeholder="请选择" style="width: 320px" @change="typeChange()">
+					<!-- 1电信,2联通,3移动 -->
+					<el-option label="电信" :value="1" />
+					<el-option label="联通" :value="2" />
+					<el-option label="移动" :value="3" />
+				</el-select>
+			</div>
+
+		  <div shadow="nover" class="top-wrap">
+        <div class="title">数据统计</div>
+				<div class="top-inner-wrap">
+					<div class="line-wrap flow-line-wrap">
+						<div class="text-wrap">
+							<div class="text">昨日流量消耗</div>
+							<div>{{formatSize(statisticsData.yesterdayTotal * 1024 * 1024)}}</div>
+						</div>
+						<div class="line-inner-wrap" ref="yesterdayLine"></div>
+					</div>
+					<div class="line-wrap flow-line-wrap">
+						<div class="text-wrap">
+							<div class="text">当月流量消耗</div>
+							<div>{{formatSize(statisticsData.realMonthTotal * 1024 * 1024)}}</div>
+						</div>
+						<div class="line-inner-wrap" ref="monthLine"></div>
+					</div>
+
+					<div class="line-wrap flow-line-wrap">
+						<div class="text-wrap">
+							<div class="text">本年流量消耗</div>
+							<div>{{formatSize(statisticsData.yearTotal * 1024 * 1024)}}</div>
+						</div>
+						<div class="line-inner-wrap" ref="yearLine"></div>
+					</div>
+				</div>
+
+        
+      </div>
+
+
+			<div class="statistics-wrap gap-3">
+				<el-card shadow="nover" class="left-wrap">
+					<div class="top-title-wrap">
+							<div class="title">流量统计</div>
+							<div class="operate-wrap">
+								<el-button-group>
+									<el-button @click="changeDate(1)" :type="activeIndex == 1 ?  'primary' : ''">昨日</el-button>
+									<el-button @click="changeDate(2)" :type="activeIndex == 2 ?  'primary' : ''">近一周</el-button>
+									<el-button @click="changeDate(3)" :type="activeIndex == 3 ?  'primary' : ''">近一月</el-button>
+									<el-button @click="changeDate(4)" :type="activeIndex == 4 ?  'primary' : ''">近一年</el-button>
+								</el-button-group>
+									<el-date-picker
+										class="date-picker-wrap"
+										v-model="dateTimeRange"
+										:disabled-date="disabledDate"
+										type="datetimerange"
+										range-separator="至"
+										start-placeholder="开始时间"
+										end-placeholder="结束时间"
+										format="YYYY-MM-DD HH:mm:ss"
+										date-format="YYYY/MM/DD"
+										time-format="hh:mm:ss"
+									/>
+							</div>
+					</div>
+					<div style="height: 460px;" ref="flowLine"></div>
+				</el-card>
+
+				<el-card shadow="nover" class="right-wrap">
+					<div class="top-title-wrap">
+						<div class="title">流量使用TOP10</div>
+						<el-date-picker
+							class="date-picker-wrap"
+							v-model="dateRange"
+							:disabled-date="disabledDate"
+							type="daterange"
+							range-separator="至"
+							start-placeholder="开始日期"
+							end-placeholder="结束日期"
+							format="YYYY-MM-DD"
+							date-format="YYYY/MM/DD"
+						/>
+					</div>
+					<div class="ranking-list">
+						<div class="rank-item" v-for="(item, index) in rankList" :key="index">
+							<div :class="`number-item-${++index}`" class="number">{{index++}}</div>
+							<div class="card-num">{{item.accessNumber}}</div>
+							<el-progress class="progress-wrap" :text-inside="true" :stroke-width="16" :percentage="totalNum ? (item.value / totalNum * 100).toFixed(2) : 0" />
+							<div class="flow-num">{{formatSize(item.value * 1024 * 1024)}}</div>
+						</div>
+					</div>
+					
+				</el-card>
+			</div>
+		</el-card>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, nextTick, watch, markRaw } from "vue"
+import { formatSize } from "/@/utils/common";
+import api from '/@/api/iotCard';
+import { useSearch } from "/@/hooks/useCommon";
+import { useStore } from '/@/store/index';
+import { useRoute } from 'vue-router';
+import * as echarts from 'echarts';
+import dayjs from 'dayjs';
+
+const store = useStore();
+const route = useRoute();
+const sim = ref({
+  accNumber: "",// 卡号
+  iccid: "",// ICCID
+  bindDeviceName: "",// 绑定设备
+  platName: "",// 平台对接
+  types: "",// 运营商
+  simTypes: "",// 类型
+  totalFlow: "",// 总流量
+  usedFlow: "",// 使用流量
+  leaveFlow: "",// 剩余流量
+  activationTime: "",// 激活日期
+  updatedAt: "",// 更新时间
+  simStatus: "",// 状态
+  remark: ""// 说明
+})
+
+const types = ref(1);
+const statisticsData = ref({
+	realMonthTotal: 0,
+	yearTotal: 0,
+	yesterdayTotal: 0
+})
+
+const flowLine = ref();
+const yesterdayLine = ref();
+const monthLine = ref();
+const yearLine = ref();
+
+const dateRange = ref<any>([
+  dayjs(new Date()).subtract(1, 'month'),
+  dayjs(new Date()),
+])
+
+
+const dateTimeRange = ref<any>([
+  dayjs(new Date()).subtract(6, 'day'),
+  dayjs(new Date()),
+])
+
+const totalNum = ref(0);
+
+const activeIndex= ref(2);
+
+const rankList = ref([]);
+
+const flowLineXAxisData = ref<any>([]);
+const flowLineData = ref<any>([]);
+
+const yearLineXAxisData = ref<any>([]);
+const yearLineData = ref<any>([]);
+
+const monthLineXAxisData = ref<any>([]);
+const monthLineData = ref<any>([]);
+
+const yesterdayLineXAxisData = ref<any>([dayjs(new Date()).subtract(1, 'day').format('YYYY-MM-DD')]);
+const yesterdayLineData = ref<any>([]);
+
+
+
+const state = reactive({
+	global: {
+		yesterdayLine: null,
+    monthLine: null,
+    yearLine: null,
+		dispose: [null, '', undefined],
+	} as any,
+	myCharts: [],
+	charts: {
+		theme: '',
+		bgColor: '',
+		color: '#303133',
+	},
+});
+
+const disabledDate = (time: Date) => {
+  return time.getTime() > Date.now()
+}
+
+const getFlowDataByDateRange = async (dateRangeData:any) => {
+  const simFlowRes = await api.dashboard.getFlowDataByDateRange({
+    sdate: dayjs(dateRangeData[0]).format('YYYY-MM-DD HH:mm:ss'),
+    edate: dayjs(dateRangeData[1]).format('YYYY-MM-DD HH:mm:ss'),
+    types: types.value
+  })
+	return simFlowRes.data;
+}
+
+const getYesterdayFlowData = async () => {
+	  const yesterday = dayjs(new Date()).subtract(1, 'day').format('YYYY-MM-DD')
+		const res = await getFlowDataByDateRange([
+      yesterday + " 00:00:00",
+      yesterday + " 23:59:59"
+    ])
+		yesterdayLineData.value = [res[0].value];
+		initYesterdayLineChart();
+}
+const getMonthFlowData = async () => {
+		monthLineXAxisData.value = [];
+		monthLineData.value = [];
+	  const monthDay1 = dayjs(new Date()).startOf('month').format('YYYY-MM-DD');
+		const monthDay2 = dayjs(new Date()).endOf('month').format('YYYY-MM-DD');
+		const res = await getFlowDataByDateRange([monthDay1, monthDay2])
+		res.reverse().forEach((item:any) => {
+			monthLineXAxisData.value.push(item.date);
+			monthLineData.value.push(item.value);
+		})
+
+		initMonthLineChart();
+}
+const getYearFlowData = async () => {
+	yearLineXAxisData.value = [];
+	yearLineData.value = [];
+	const year = dayjs(new Date()).startOf('year').format('YYYY');
+	const res = await getFlowDataByDateRange([
+		year + '-01-01',
+		year + '-12-31'
+	])
+	res.reverse().forEach((item:any) => {
+		yearLineXAxisData.value.push(item.date);
+		yearLineData.value.push(item.value);
+	})
+	initYearLineChart();
+}
+
+const getFlowData = async () => {
+	flowLineXAxisData.value = [];
+  flowLineData.value = [];
+	const res = await getFlowDataByDateRange(dateTimeRange.value)
+	res.reverse().forEach((item:any) => {
+		flowLineXAxisData.value.push(item.date);
+		flowLineData.value.push(item.value);
+	})
+	iniFlowLineChart();
+}
+
+const changeDate = (key:number) => {
+  // 1 昨天 2近一周 3近一月 4近一年
+  activeIndex.value = key;
+  if(key === 1) {
+    // 昨天
+    const yesterday = dayjs(new Date()).subtract(1, 'day').format('YYYY-MM-DD')
+    dateTimeRange.value = [
+      yesterday + " 00:00:00",
+      yesterday + " 23:59:59"
+    ]
+  }else if(key === 2) {
+    // 近一周
+    dateTimeRange.value = [
+      dayjs(new Date()).subtract(6, 'day'),
+      dayjs(new Date()),
+    ]
+  }else if(key === 3) {
+    // 近一月
+    dateTimeRange.value = [
+      dayjs(new Date()).subtract(1, 'month'),
+      dayjs(new Date()),
+    ]
+  }else if(key === 4) {
+    // 近一年
+    dateTimeRange.value = [
+      dayjs(new Date()).subtract(1, 'year'),
+      dayjs(new Date()),
+    ]
+  }
+  getFlowData();
+}
+
+const typeChange = (value:any) => {
+	getYesterdayFlowData();
+	getMonthFlowData();
+	getYearFlowData();
+	getFlowData();
+	getTop10Data();
+}
+
+const getTop10Data = async () => {
+  const top10Res = await api.dashboard.getTop10Data({
+    sdate: dayjs(dateRange.value[0]).format('YYYY-MM-DD'),
+    edate: dayjs(dateRange.value[1]).format('YYYY-MM-DD'),
+    types: types.value
+  })
+	// if(!top10Res.data) return;
+	rankList.value = top10Res.data || [];
+	totalNum.value = top10Res.data ? top10Res.data[0].value : 0;
+}
+const getFlowAllData = async () => {
+  const res = await api.dashboard.getFlowData({
+    types: types.value
+  })
+	statisticsData.value = res
+}
+
+const formatOperator = (val:any) => {
+  // 1电信,2联通,3移动
+  if(val == 1) {
+    return "电信"
+  }else if(val == 2) {
+    return "联通"
+  }else if(val == 3) {
+    return "移动"
+  }
+}
+
+const formatType = (val:any) => {
+  // 1月卡,2季卡,3年卡,4其他
+  if(val == 1) {
+    return "月卡"
+  }else if(val == 2) {
+    return "季卡"
+  }else if(val == 3) {
+    return "年卡"
+  }else if(val == 4) {
+    return "其他"
+  }
+}
+
+const formatStatus = (val:any) => {
+  // 1:可激活 2:测试激活 3:测试去激活 4:在用5:停机6:运营商管理状态
+  if(val == 1) {
+    return "可激活"
+  }else if(val == 2) {
+    return "测试激活"
+  }else if(val == 3) {
+    return "测试去激活"
+  }else if(val == 4) {
+    return "在用"
+  }else if(val == 5) {
+    return "停机"
+  }else if(val == 6) {
+    return "运营商管理状态"
+  }
+}
+
+// 折线图 - 昨日流量消耗
+const initYesterdayLineChart = () => {
+  if (!state.global.dispose.some((b: any) => b === state.global.yesterdayLine)) state.global.yesterdayLine.dispose();
+	state.global.yesterdayLine = markRaw(echarts.init(yesterdayLine.value, state.charts.theme));
+  const option = {
+    backgroundColor: state.charts.bgColor,
+    xAxis: {
+      data: yesterdayLineXAxisData.value,
+      show: false,
+    },
+    yAxis: [
+      {
+        type: 'value',
+        name: '',
+        show: false,
+        splitLine: { show: false, lineStyle: { type: 'dashed', color: '#f5f5f5' } },
+      },
+    ],
+    tooltip: { 
+			trigger: 'axis',
+			formatter: function (params:any) {
+					var relVal = params[0].name
+					let circle = `<i style="margin-right:4px;display: inline-block;width: 10px;height: 10px;border-radius: 50%;background-color:${params[0].color}"></i>`
+					relVal += '<br/>' + circle + ' 流量: ' + formatSize(params[0].value*1024*1024)
+					return relVal;
+				}
+		},
+    grid: { top: 10, right: 10, bottom: 10, left: 10 },
+    series: [
+      {
+        name: '流量',
+        type: 'line',
+        smooth: true,
+        data: yesterdayLineData.value,
+        lineStyle: { color: '#fe9a8b' },
+        itemStyle: { color: '#fe9a8b', borderColor: '#fe9a8b' },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#fe9a8bb3' },
+            { offset: 1, color: '#fe9a8b03' },
+          ]),
+        },
+      }
+    ],
+  };
+  (<any>state.global.yesterdayLine).setOption(option);
+  (<any>state.myCharts).push(state.global.yesterdayLine);
+};
+
+
+// 折线图 - 当月流量消耗
+const initMonthLineChart = () => {
+  if (!state.global.dispose.some((b: any) => b === state.global.monthLine)) state.global.monthLine.dispose();
+	state.global.monthLine = markRaw(echarts.init(monthLine.value, state.charts.theme));
+  const option = {
+    backgroundColor: state.charts.bgColor,
+    xAxis: {
+      data: monthLineXAxisData.value,
+      show: false,
+    },
+    yAxis: [
+      {
+        type: 'value',
+        name: '',
+        show: false,
+      },
+    ],
+    tooltip: { 
+			trigger: 'axis',
+			formatter: function (params:any) {
+					var relVal = params[0].name
+					let circle = `<i style="margin-right:4px;display: inline-block;width: 10px;height: 10px;border-radius: 50%;background-color:${params[0].color}"></i>`
+					relVal += '<br/>' + circle + ' 流量: ' + formatSize(params[0].value*1024*1024)
+					return relVal;
+				}
+		},
+    grid: { top: 10, right: 10, bottom: 10, left: 10 },
+    series: [
+      {
+        name: '流量',
+        type: 'line',
+        smooth: true,
+        data: monthLineData.value,
+        lineStyle: { color: '#9E87FF' },
+        itemStyle: { color: '#9E87FF', borderColor: '#9E87FF' },
+				areaStyle: {
+					color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+						{ offset: 0, color: '#9E87FFb3' },
+						{ offset: 1, color: '#9E87FF03' },
+					]),
+				},
+      }
+    ],
+  };
+  (<any>state.global.monthLine).setOption(option);
+  (<any>state.myCharts).push(state.global.monthLine);
+};
+
+// 折线图 - 本年流量消耗
+const initYearLineChart = () => {
+  if (!state.global.dispose.some((b: any) => b === state.global.yearLine)) state.global.yearLine.dispose();
+	state.global.yearLine = markRaw(echarts.init(yearLine.value, state.charts.theme));
+  const option = {
+    backgroundColor: state.charts.bgColor,
+    xAxis: {
+      data: yearLineXAxisData.value,
+      show: false,
+    },
+    yAxis: [
+      {
+        type: 'value',
+        name: '',
+        show: false,
+        splitLine: { show: false, lineStyle: { type: 'dashed', color: '#f5f5f5' } }
+			}
+    ],
+    tooltip: { 
+			trigger: 'axis',
+			formatter: function (params:any) {
+					var relVal = params[0].name
+					let circle = `<i style="margin-right:4px;display: inline-block;width: 10px;height: 10px;border-radius: 50%;background-color:${params[0].color}"></i>`
+					relVal += '<br/>' + circle + ' 流量: ' + formatSize(params[0].value*1024*1024)
+					return relVal;
+				}
+		},
+    grid: { top: 10, right: 10, bottom: 10, left: 10 },
+    series: [
+      {
+        name: '流量',
+        type: 'line',
+        smooth: true,
+        data: yearLineData.value,
+        lineStyle: { color: '#fe9a8b' },
+        itemStyle: { color: '#fe9a8b', borderColor: '#fe9a8b' },
+				areaStyle: {
+					color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+						{ offset: 0, color: '#fe9a8bb3' },
+						{ offset: 1, color: '#fe9a8b03' },
+					]),
+				},
+      }
+    ],
+  };
+  (<any>state.global.yearLine).setOption(option);
+  (<any>state.myCharts).push(state.global.yearLine);
+};
+
+// 折线图 - 流量统计
+const iniFlowLineChart = async () => {
+  if (!state.global.dispose.some((b: any) => b === state.global.flowLine)) state.global.flowLine.dispose();
+	state.global.flowLine = markRaw(echarts.init(flowLine.value, state.charts.theme));
+  const option = {
+    backgroundColor: state.charts.bgColor,
+    grid: { top: 70, right: 20, bottom: 30, left: 30 },
+    tooltip: { 
+			trigger: 'axis',
+			formatter: function (params:any) {
+					var relVal = params[0].name
+					let circle = `<i style="margin-right:4px;display: inline-block;width: 10px;height: 10px;border-radius: 50%;background-color:${params[0].color}"></i>`
+					relVal += '<br/>' + circle + ' 流量: ' + params[0].value + 'MB'
+					return relVal;
+				}
+		},
+    xAxis: {
+      data: flowLineXAxisData.value,
+    },
+    yAxis: [
+      {
+        type: 'value',
+        name: '',
+        splitLine: { show: true, lineStyle: { type: 'dashed', color: '#f5f5f5' } },
+        axisLabel: {
+          margin: 2,
+          formatter: function (value:any, index:any) {
+            if (value >= 10000 && value < 10000000) {
+              value = value / 10000 + "W";
+            } else if (value >= 10000000) {
+              value = value / 10000000 + "KW";
+            }
+            return value;
+          }
+        },
+      },
+    ],
+    series: [
+      {
+        name: '流量',
+        type: 'line',
+        symbolSize: 6,
+        symbol: 'circle',
+        smooth: true,
+        data: flowLineData.value,
+        lineStyle: { color: '#9E87FF' },
+        itemStyle: { color: '#9E87FF', borderColor: '#9E87FF' },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#9E87FFb3' },
+            { offset: 1, color: '#9E87FF03' },
+          ]),
+        },
+        emphasis: {
+          itemStyle: {
+            color: {
+              type: 'radial',
+              x: 0.5,
+              y: 0.5,
+              r: 0.5,
+              colorStops: [
+                { offset: 0, color: '#9E87FF' },
+                { offset: 0.4, color: '#9E87FF' },
+                { offset: 0.5, color: '#fff' },
+                { offset: 0.7, color: '#fff' },
+                { offset: 0.8, color: '#fff' },
+                { offset: 1, color: '#fff' },
+              ],
+            },
+            borderColor: '#9E87FF',
+            borderWidth: 2,
+          },
+        },
+      },
+    ],
+  };
+  (<any>state.global.flowLine).setOption(option);
+  (<any>state.myCharts).push(state.global.flowLine);
+};
+
+getFlowAllData();
+getYesterdayFlowData();
+getMonthFlowData();
+getYearFlowData();
+getFlowData();
+getTop10Data();
+
+// 监听 vuex 中是否开启深色主题
+watch(
+  () => store.state.themeConfig.themeConfig.isIsDark,
+  (isIsDark) => {
+    nextTick(() => {
+      state.charts.theme = isIsDark ? 'dark' : '';
+      state.charts.bgColor = isIsDark ? 'transparent' : '';
+      state.charts.color = isIsDark ? '#dadada' : '#303133';
+      setTimeout(() => {
+        iniFlowLineChart();
+        initYesterdayLineChart();
+        initMonthLineChart();
+        initYearLineChart();
+      }, 500);
+    });
+  },
+  {
+    deep: true,
+    immediate: true,
+  }
+);
+</script>
+
+<style lang="scss" scoped>
+.select-wrap {
+	text-align: right;
+}
+.top-wrap {
+	width: 100%;
+	.top-inner-wrap {
+		display: flex;
+		margin-top: 10px;
+
+	}
+	.line-wrap {
+		flex: 1;
+		background-color: #fff;
+		background: #fcfcfc;
+		border: 1px solid #e0e4e8;
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		padding: 20px;
+		position: relative;
+		.text-wrap {
+			position: absolute;
+			left: 20px;
+			top: 40px;
+			.text {
+				font-size: 14px;
+				color: #000000a3;
+			}
+			div:nth-child(2) {
+				font-size: 32px;
+				font-weight: 700;
+				overflow: hidden;
+				white-space: nowrap;
+				text-overflow: ellipsis;
+			}
+		}
+		.line-inner-wrap {
+			height: 100px;
+			width: calc(100% - 150px);
+			margin-left: 150px;
+		}
+	}
+	.line-wrap:not(:nth-child(1)) {
+		margin-left: 20px;
+	}
+}
+.title {
+	color: var(--el-text-color-primary);
+	font-size: 16px;
+	font-weight: 700;
+}
+.statistics-wrap {
+  display: flex;
+  justify-content: space-between;
+  align-items: normal;
+  margin-top: 20px;
+
+  .left-wrap {
+    width: 66%;
+    .top-title-wrap {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 16px;
+      ::v-deep .el-date-editor.el-input__wrapper {
+        width: 360px!important;
+        margin-left: 12px;
+      }
+      .operate-wrap {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
+    }
+  }
+  .right-wrap {
+    width: 36%;
+		.top-title-wrap {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 16px;
+      ::v-deep .el-date-editor.el-input__wrapper {
+        max-width: 220px!important;
+      }
+    }
+		.ranking-list {
+			width: 100%;
+			.rank-item {
+				width: 100%;
+				display: flex;
+				justify-content: space-between;
+				align-items: center;
+				margin-bottom: 24px;
+
+				.number {
+					flex: 0 0 24px;
+					height: 24px;
+					color: #fff;
+					font-weight: 700;
+					line-height: 24px;
+					text-align: center;
+					background-color: #d1d1d1;
+				}
+				.number-item-1 {
+					color: #e50012;
+    			background-color: #e500121a;
+				}
+				.number-item-2 {
+    			color: #fba500;
+    			background-color: #fba5001a;
+				}
+				.number-item-3 {
+					color: #597ef7;
+    			background-color: #597ef71a;
+				}
+				.card-num {
+					width: 110px;
+				}
+				.progress-wrap {
+					width: 32%;
+				}
+				.flow-num {
+					width: 90px;
+					text-align: right;
+				}
+			}
+		}
+  }
+}
+  
+</style>

+ 748 - 0
src/views/iot/iotCard/index/detail.vue

@@ -0,0 +1,748 @@
+<!-- 物联网卡-详情 -->
+<template>
+  <div>
+    <el-card shadow="nover">
+       <el-descriptions
+        class="margin-top"
+        title="基本信息"
+        :column="3"
+        :size="size"
+        border
+      >
+        <!-- <template #extra>
+          <el-button type="primary">编辑</el-button>
+        </template> -->
+        <!-- 卡号 -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <user />
+              </el-icon>
+              卡号
+            </div>
+          </template>
+          {{sim.accNumber}}
+        </el-descriptions-item>
+
+        <!-- ICCID -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <iphone />
+              </el-icon>
+              ICCID
+            </div>
+          </template>
+          {{sim.iccid}}
+        </el-descriptions-item>
+
+        <!-- 绑定设备 -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <iphone />
+              </el-icon>
+              绑定设备
+            </div>
+          </template>
+          {{sim.bindDeviceName}}
+        </el-descriptions-item>
+
+        <!-- 平台类型 -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <iphone />
+              </el-icon>
+              平台类型
+            </div>
+          </template>
+          {{sim.platTypes}}
+        </el-descriptions-item>
+
+        <!-- 	平台名称 -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <location />
+              </el-icon>
+              平台名称
+            </div>
+          </template>
+          {{sim.platName}}
+        </el-descriptions-item>
+
+        <!-- 运营商 -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <location />
+              </el-icon>
+              运营商
+            </div>
+          </template>
+          {{formatOperator(sim.types)}}
+        </el-descriptions-item>
+
+        <!-- 	类型 -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <location />
+              </el-icon>
+              类型
+            </div>
+          </template>
+          {{formatType(sim.simTypes)}}
+        </el-descriptions-item>
+        
+        <!-- 	激活日期 -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <location />
+              </el-icon>
+              激活日期
+            </div>
+          </template>
+          {{sim.activationTime}}
+        </el-descriptions-item>
+
+        <!-- 更新时间 -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <location />
+              </el-icon>
+              更新时间
+            </div>
+          </template>
+          {{sim.updatedAt}}
+        </el-descriptions-item>
+
+
+        <!-- 总流量 -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <location />
+              </el-icon>
+              总流量
+            </div>
+          </template>
+          {{sim.totalFlow}}
+        </el-descriptions-item>
+
+        <!-- 使用流量 -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <location />
+              </el-icon>
+              使用流量
+            </div>
+          </template>
+          {{sim.usedFlow}}
+        </el-descriptions-item>
+
+        <!-- 剩余流量 -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <location />
+              </el-icon>
+              剩余流量
+            </div>
+          </template>
+          {{sim.leaveFlow}}
+        </el-descriptions-item>
+
+        <!-- 状态 -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <tickets />
+              </el-icon>
+              状态
+            </div>
+          </template>
+          <el-tag size="small">{{formatStatus(sim.simStatus)}}</el-tag>
+        </el-descriptions-item>
+
+        <!-- 说明 -->
+        <el-descriptions-item>
+          <template #label>
+            <div class="cell-item">
+              <el-icon :style="iconStyle">
+                <office-building />
+              </el-icon>
+              说明
+            </div>
+          </template>
+          {{sim.iccid}}
+        </el-descriptions-item>
+      </el-descriptions>
+    </el-card>
+
+    <div class="statistics-wrap gap-3">
+      <el-card shadow="nover" class="left-wrap">
+        <div class="top-title-wrap">
+            <div class="title">流量统计</div>
+            <div class="operate-wrap">
+              <el-button-group>
+                <el-button @click="changeDate(1)" :type="activeIndex == 1 ?  'primary' : ''">昨日</el-button>
+                <el-button @click="changeDate(2)" :type="activeIndex == 2 ?  'primary' : ''">近一周</el-button>
+                <el-button @click="changeDate(3)" :type="activeIndex == 3 ?  'primary' : ''">近一月</el-button>
+                <el-button @click="changeDate(4)" :type="activeIndex == 4 ?  'primary' : ''">近一年</el-button>
+              </el-button-group>
+                <el-date-picker
+                  class="date-picker-wrap"
+                  v-model="dateRange"
+                  type="datetimerange"
+                  range-separator="至"
+                  start-placeholder="开始时间"
+                  end-placeholder="结束时间"
+                  format="YYYY-MM-DD HH:mm:ss"
+                  date-format="YYYY/MM/DD"
+                  time-format="hh:mm:ss"
+                />
+            </div>
+        </div>
+        <div style="height: 460px;" ref="flowLine"></div>
+      </el-card>
+
+      <el-card shadow="nover" class="right-wrap">
+        <div class="title">数据统计</div>
+        <div class="line-wrap flow-line-wrap">
+          <div class="text-wrap">
+            <div class="text">昨日流量消耗</div>
+            <div>{{formatSize(statisticsData.yesterdayTotal * 1024 * 1024)}}</div>
+          </div>
+          <div class="line-inner-wrap" ref="yesterdayLine"></div>
+        </div>
+        <div class="line-wrap flow-line-wrap">
+          <div class="text-wrap">
+            <div class="text">当月流量消耗</div>
+            <div>{{formatSize(statisticsData.monthTotal * 1024 * 1024)}}</div>
+          </div>
+          <div class="line-inner-wrap" ref="monthLine"></div>
+        </div>
+
+        <div class="line-wrap flow-line-wrap">
+          <div class="text-wrap">
+            <div class="text">本年流量消耗</div>
+            <div>{{formatSize(statisticsData.yearTotal * 1024 * 1024)}}</div>
+          </div>
+          <div class="line-inner-wrap" ref="yearLine"></div>
+        </div>
+        
+      </el-card>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, nextTick, watch, markRaw } from "vue";
+import { formatSize } from "/@/utils/common";
+import api from '/@/api/iotCard';
+import { useSearch } from "/@/hooks/useCommon";
+import { useStore } from '/@/store/index';
+import { useRoute } from 'vue-router';
+import * as echarts from 'echarts';
+import dayjs from 'dayjs';
+
+const store = useStore();
+const route = useRoute();
+const sim = ref({
+  accNumber: "",// 卡号
+  iccid: "",// ICCID
+  bindDeviceName: "",// 绑定设备
+  platName: "",// 平台对接
+  types: "",// 运营商
+  simTypes: "",// 类型
+  totalFlow: "",// 总流量
+  usedFlow: "",// 使用流量
+  leaveFlow: "",// 剩余流量
+  activationTime: "",// 激活日期
+  updatedAt: "",// 更新时间
+  simStatus: "",// 状态
+  remark: ""// 说明
+})
+
+const flowLine = ref();
+const yesterdayLine = ref();
+const monthLine = ref();
+const yearLine = ref();
+
+const dateRange = ref<any>([
+  dayjs(new Date()).subtract(6, 'day'),
+  dayjs(new Date()),
+])
+const activeIndex= ref(2);
+
+const flowLineXAxisData = ref<any>([]);
+const flowLineData = ref<any>([]);
+
+const yearLineXAxisData = ref<any>([]);
+const yearLineData = ref<any>([]);
+
+const monthLineXAxisData = ref<any>([]);
+const monthLineData = ref<any>([]);
+
+const yesterdayLineXAxisData = ref<any>([dayjs(new Date()).subtract(1, 'day').format('YYYY-MM-DD')]);
+const yesterdayLineData = ref<any>([]);
+
+const statisticsData = ref({
+	monthTotal: 0,
+	yearTotal: 0,
+	yesterdayTotal: 0
+})
+
+const state = reactive({
+	global: {
+		yesterdayLine: null,
+    monthLine: null,
+    yearLine: null,
+		dispose: [null, '', undefined],
+	} as any,
+	myCharts: [],
+	charts: {
+		theme: '',
+		bgColor: '',
+		color: '#303133',
+	},
+});
+
+const getDetailInfo = async () => {
+  const res = await api.simCard.detailItem({ id: route.params.id });
+  sim.value = res.sim;
+  statisticsData.value = {
+    monthTotal: res.monthFlow,
+    yearTotal: res.yearFlow,
+    yesterdayTotal: res.yesterdayFlow
+  }
+  await getFlowDataByDateRange();
+  res.yearDataList.reverse().forEach((item:any) => {
+    yearLineXAxisData.value.push(item.date);
+    yearLineData.value.push(item.value);
+  })
+  res.monthDataList.reverse().forEach((item:any) => {
+    monthLineXAxisData.value.push(item.date);
+    monthLineData.value.push(item.value);
+  })
+  yesterdayLineData.value = [res.yearFlow];
+  iniFlowLineChart();
+  initYesterdayLineChart();
+  initMonthLineChart();
+  initYearLineChart();
+}
+
+
+getDetailInfo();
+
+
+const getFlowDataByDateRange = async () => {
+  const simFlowRes = await api.simCard.getFlowDataByDateRange({
+    sdate: activeIndex.value !== 1 ? dateRange.value[0].format('YYYY-MM-DD HH:mm:ss') : dateRange.value[0],
+    edate: activeIndex.value !== 1 ? dateRange.value[1].format('YYYY-MM-DD HH:mm:ss') : dateRange.value[1],
+    accNumber: sim.value.accNumber,
+    types: sim.value.types
+  })
+  simFlowRes.data.reverse().forEach((item:any) => {
+    flowLineXAxisData.value.push(item.date);
+    flowLineData.value.push(item.value);
+  })
+  iniFlowLineChart();
+}
+
+const changeDate = (key:number) => {
+  // 1 昨天 2近一周 3近一月 4近一年
+  activeIndex.value = key;
+  if(key === 1) {
+    // 昨天
+    const yesterday = dayjs(new Date()).subtract(1, 'day').format('YYYY-MM-DD')
+    dateRange.value = [
+      yesterday + " 00:00:00",
+      yesterday + " 23:59:59"
+    ]
+  }else if(key === 2) {
+    // 近一周
+    dateRange.value = [
+      dayjs(new Date()).subtract(6, 'day'),
+      dayjs(new Date()),
+    ]
+  }else if(key === 3) {
+    // 近一月
+    dateRange.value = [
+      dayjs(new Date()).subtract(1, 'month'),
+      dayjs(new Date()),
+    ]
+  }else if(key === 4) {
+    // 近一年
+    dateRange.value = [
+      dayjs(new Date()).subtract(1, 'year'),
+      dayjs(new Date()),
+    ]
+  }
+  flowLineXAxisData.value = [];
+  flowLineData.value = [];
+  getFlowDataByDateRange();
+
+}
+
+const formatOperator = (val:any) => {
+  // 1电信,2联通,3移动
+  if(val == 1) {
+    return "电信"
+  }else if(val == 2) {
+    return "联通"
+  }else if(val == 3) {
+    return "移动"
+  }
+}
+
+const formatType = (val:any) => {
+  // 1月卡,2季卡,3年卡,4其他
+  if(val == 1) {
+    return "月卡"
+  }else if(val == 2) {
+    return "季卡"
+  }else if(val == 3) {
+    return "年卡"
+  }else if(val == 4) {
+    return "其他"
+  }
+}
+
+const formatStatus = (val:any) => {
+  // 1:可激活 2:测试激活 3:测试去激活 4:在用5:停机6:运营商管理状态
+  if(val == 1) {
+    return "可激活"
+  }else if(val == 2) {
+    return "测试激活"
+  }else if(val == 3) {
+    return "测试去激活"
+  }else if(val == 4) {
+    return "在用"
+  }else if(val == 5) {
+    return "停机"
+  }else if(val == 6) {
+    return "运营商管理状态"
+  }
+}
+
+// 折线图 - 昨日流量消耗
+const initYesterdayLineChart = () => {
+  if (!state.global.dispose.some((b: any) => b === state.global.yesterdayLine)) state.global.yesterdayLine.dispose();
+	state.global.yesterdayLine = markRaw(echarts.init(yesterdayLine.value, state.charts.theme));
+  const option = {
+    backgroundColor: state.charts.bgColor,
+    xAxis: {
+      data: yesterdayLineXAxisData.value,
+      show: false,
+    },
+    yAxis: [
+      {
+        type: 'value',
+        name: '',
+        show: false,
+        splitLine: { show: false, lineStyle: { type: 'dashed', color: '#f5f5f5' } }
+      },
+    ],
+    tooltip: { 
+			trigger: 'axis',
+			formatter: function (params:any) {
+					var relVal = params[0].name
+					let circle = `<i style="margin-right:4px;display: inline-block;width: 10px;height: 10px;border-radius: 50%;background-color:${params[0].color}"></i>`
+					relVal += '<br/>' + circle + ' 流量: ' + formatSize(params[0].value*1024*1024)
+					return relVal;
+				}
+		},
+    grid: { top: 10, right: 10, bottom: 10, left: 10 },
+    series: [
+      {
+        name: '流量',
+        type: 'line',
+        smooth: true,
+        data: yesterdayLineData.value,
+        lineStyle: { color: '#fe9a8b' },
+        itemStyle: { color: '#fe9a8b', borderColor: '#fe9a8b' }
+      }
+    ],
+  };
+  (<any>state.global.yesterdayLine).setOption(option);
+  (<any>state.myCharts).push(state.global.yesterdayLine);
+};
+
+
+// 折线图 - 当月流量消耗
+const initMonthLineChart = () => {
+  if (!state.global.dispose.some((b: any) => b === state.global.monthLine)) state.global.monthLine.dispose();
+	state.global.monthLine = markRaw(echarts.init(monthLine.value, state.charts.theme));
+  const option = {
+    backgroundColor: state.charts.bgColor,
+    xAxis: {
+      data: monthLineXAxisData.value,
+      show: false,
+    },
+    yAxis: [
+      {
+        type: 'value',
+        name: '',
+        show: false,
+        splitLine: { show: false, lineStyle: { type: 'dashed', color: '#f5f5f5' } }
+      },
+    ],
+    tooltip: { 
+			trigger: 'axis',
+			formatter: function (params:any) {
+					var relVal = params[0].name
+					let circle = `<i style="margin-right:4px;display: inline-block;width: 10px;height: 10px;border-radius: 50%;background-color:${params[0].color}"></i>`
+					relVal += '<br/>' + circle + ' 流量: ' + formatSize(params[0].value*1024*1024)
+					return relVal;
+				}
+		},
+    grid: { top: 10, right: 10, bottom: 10, left: 10 },
+    series: [
+      {
+        name: '流量',
+        type: 'line',
+        smooth: true,
+        data: monthLineData.value,
+        lineStyle: { color: '#9E87FF' },
+        itemStyle: { color: '#9E87FF', borderColor: '#9E87FF' },
+      }
+    ],
+  };
+  (<any>state.global.monthLine).setOption(option);
+  (<any>state.myCharts).push(state.global.monthLine);
+};
+
+// 折线图 - 本年流量消耗
+const initYearLineChart = () => {
+  if (!state.global.dispose.some((b: any) => b === state.global.yearLine)) state.global.yearLine.dispose();
+	state.global.yearLine = markRaw(echarts.init(yearLine.value, state.charts.theme));
+  const option = {
+    backgroundColor: state.charts.bgColor,
+    xAxis: {
+      data: yearLineXAxisData.value,
+      show: false,
+    },
+    yAxis: [
+      {
+        type: 'value',
+        name: '',
+        show: false,
+        splitLine: { show: false, lineStyle: { type: 'dashed', color: '#f5f5f5' } }
+      },
+    ],
+    tooltip: { 
+			trigger: 'axis',
+			formatter: function (params:any) {
+					var relVal = params[0].name
+					let circle = `<i style="margin-right:4px;display: inline-block;width: 10px;height: 10px;border-radius: 50%;background-color:${params[0].color}"></i>`
+					relVal += '<br/>' + circle + ' 流量: ' + formatSize(params[0].value*1024*1024)
+					return relVal;
+				}
+		},
+    grid: { top: 10, right: 10, bottom: 10, left: 10 },
+    series: [
+      {
+        name: '流量',
+        type: 'line',
+        smooth: true,
+        data: yearLineData.value,
+        lineStyle: { color: '#fe9a8b' },
+        itemStyle: { color: '#fe9a8b', borderColor: '#fe9a8b' }
+      }
+    ],
+  };
+  (<any>state.global.yearLine).setOption(option);
+  (<any>state.myCharts).push(state.global.yearLine);
+};
+
+// 折线图 - 流量统计
+const iniFlowLineChart = async () => {
+  if (!state.global.dispose.some((b: any) => b === state.global.flowLine)) state.global.flowLine.dispose();
+	state.global.flowLine = markRaw(echarts.init(flowLine.value, state.charts.theme));
+  const option = {
+    backgroundColor: state.charts.bgColor,
+    grid: { top: 70, right: 20, bottom: 30, left: 30 },
+    tooltip: { 
+			trigger: 'axis',
+			formatter: function (params:any) {
+					var relVal = params[0].name
+					let circle = `<i style="margin-right:4px;display: inline-block;width: 10px;height: 10px;border-radius: 50%;background-color:${params[0].color}"></i>`
+					relVal += '<br/>' + circle + ' 流量: ' + params[0].value + 'MB'
+					return relVal;
+				}
+		},
+    xAxis: {
+      data: flowLineXAxisData.value,
+    },
+    yAxis: [
+      {
+        type: 'value',
+        name: '',
+        splitLine: { show: true, lineStyle: { type: 'dashed', color: '#f5f5f5' } },
+        axisLabel: {
+          margin: 2,
+          formatter: function (value:any, index:any) {
+            if (value >= 10000 && value < 10000000) {
+              value = value / 10000 + "W";
+            } else if (value >= 10000000) {
+              value = value / 10000000 + "KW";
+            }
+            return value;
+          }
+        },
+      },
+    ],
+    series: [
+      {
+        name: '流量',
+        type: 'line',
+        symbolSize: 6,
+        symbol: 'circle',
+        smooth: true,
+        data: flowLineData.value,
+        lineStyle: { color: '#9E87FF' },
+        itemStyle: { color: '#9E87FF', borderColor: '#9E87FF' },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#9E87FFb3' },
+            { offset: 1, color: '#9E87FF03' },
+          ]),
+        },
+        emphasis: {
+          itemStyle: {
+            color: {
+              type: 'radial',
+              x: 0.5,
+              y: 0.5,
+              r: 0.5,
+              colorStops: [
+                { offset: 0, color: '#9E87FF' },
+                { offset: 0.4, color: '#9E87FF' },
+                { offset: 0.5, color: '#fff' },
+                { offset: 0.7, color: '#fff' },
+                { offset: 0.8, color: '#fff' },
+                { offset: 1, color: '#fff' },
+              ],
+            },
+            borderColor: '#9E87FF',
+            borderWidth: 2,
+          },
+        },
+      },
+    ],
+  };
+  (<any>state.global.flowLine).setOption(option);
+  (<any>state.myCharts).push(state.global.flowLine);
+};
+
+// 监听 vuex 中是否开启深色主题
+watch(
+  () => store.state.themeConfig.themeConfig.isIsDark,
+  (isIsDark) => {
+    nextTick(() => {
+      state.charts.theme = isIsDark ? 'dark' : '';
+      state.charts.bgColor = isIsDark ? 'transparent' : '';
+      state.charts.color = isIsDark ? '#dadada' : '#303133';
+      setTimeout(() => {
+        iniFlowLineChart();
+        initYesterdayLineChart();
+        initMonthLineChart();
+        initYearLineChart();
+      }, 500);
+    });
+  },
+  {
+    deep: true,
+    immediate: true,
+  }
+);
+</script>
+
+<style lang="scss" scoped>
+.statistics-wrap {
+  display: flex;
+  justify-content: space-between;
+  align-items: normal;
+  margin-top: 20px;
+  .title {
+    color: var(--el-text-color-primary);
+    font-size: 16px;
+    font-weight: 700;
+  }
+  .left-wrap {
+    width: 66%;
+    .top-title-wrap {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 16px;
+      ::v-deep .el-date-editor.el-input__wrapper {
+        width: 360px!important;
+        margin-left: 12px;
+      }
+      .operate-wrap {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
+    }
+  }
+  .right-wrap {
+    width: 36%;
+    .line-wrap {
+      width: 100%;
+      background-color: #fff;
+      background: #fcfcfc;
+      border: 1px solid #e0e4e8;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 20px;
+      position: relative;
+      .text-wrap {
+        position: absolute;
+        left: 20px;
+        top: 40px;
+        .text {
+          font-size: 14px;
+          color: #000000a3;
+        }
+        div:nth-child(2) {
+          font-size: 32px;
+          font-weight: 700;
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+        }
+      }
+      .line-inner-wrap {
+        height: 100px;
+        width: calc(100% - 120px);
+        margin-left: 120px;
+      }
+    }
+    .line-wrap:not(:nth-child(1)) {
+      margin-top: 20px;
+    }
+  }
+}
+</style>

+ 146 - 0
src/views/iot/iotCard/index/index.vue

@@ -0,0 +1,146 @@
+<!-- 物联网卡列表 -->
+<template>
+  <div class="page-full">
+    <el-card shadow="nover" class="page-full-part">
+      <el-form :model="params" inline ref="queryRef">
+        <el-form-item prop="deptName" class="mr10">
+          <el-input @keyup.enter.native="getList" style="width: 240px;" v-model="params.keyWord" placeholder="请输入ICCID或卡号" clearable />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="getList">
+            <el-icon>
+              <ele-Search />
+            </el-icon>
+            查询
+          </el-button>
+          <el-button @click="resetQuery()">
+            <el-icon>
+              <ele-Refresh />
+            </el-icon>
+            重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+      <el-table
+        :data="tableData"
+        max-height="calc(100vh  - 210px);"
+        v-loading="loading"
+        style="width: 100%"
+      >
+        <el-table-column fixed="left" min-width="130" label="卡号" prop="accNumber" align="center" />
+        <el-table-column min-width="180" label="ICCID" prop="iccid" align="center" />
+        <el-table-column label="绑定设备" prop="bindDeviceName" align="center" />
+        <el-table-column label="平台对接" prop="platName" align="center" />
+        <el-table-column label="运营商" prop="types" align="center">
+        	<template #default="scope">{{ formatOperator(scope.row.types) }}</template>
+        </el-table-column> 
+        <el-table-column label="类型" prop="simTypes" align="center">
+          <template #default="scope">{{ formatType(scope.row.simTypes) }}</template>
+        </el-table-column>
+        <el-table-column label="总流量" prop="totalFlow" align="center" />   
+        <el-table-column label="使用流量" prop="usedFlow" align="center" />    
+        <el-table-column label="剩余流量" prop="leaveFlow" align="center" />    
+        <el-table-column width="160" label="激活日期" prop="activationTime" align="center" />     
+        <el-table-column width="160" label="更新时间" prop="updatedAt" align="center" />    
+        <el-table-column label="状态" prop="simStatus" align="center">
+        	<template #default="scope">{{ formatStatus(scope.row.simStatus) }}</template>
+        </el-table-column> 
+        <el-table-column width="110" label="操作" fixed="right" prop="handle" align="center">
+					<template #default="scope">
+						<el-button size="small" text type="primary" @click="onOpenDetail(scope.row)">详情</el-button>
+						<el-button size="small" text type="warning" @click="onDel(scope.row)">删除</el-button>
+					</template>
+        </el-table-column>
+      </el-table>
+      <pagination
+        v-if="params.total"
+        :total="params.total"
+        v-model:page="params.pageNum"
+        v-model:limit="params.pageSize"
+        @pagination="getList()"
+      />
+    </el-card>
+    <!-- <EditDept ref="editDeptRef" @deptList="deptList" /> -->
+  </div>
+</template>
+
+<script lang="ts" setup>
+import api from '/@/api/iotCard';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import { useSearch } from "/@/hooks/useCommon"
+import { useRouter } from 'vue-router';
+const { params, tableData, getList, loading } = useSearch<any[]>(
+  api.simCard.getList,
+  "Data"
+)
+
+getList();
+
+const router = useRouter();
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+	params.keyWord = ""
+  getList();
+};
+
+/**
+ * 单一删除
+ */
+const onDel = (row: any) => {
+	ElMessageBox.confirm(`此操作将卡号为:“${row.accNumber}”,是否继续?`, '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+	}).then(async () => {
+		await api.simCard.deleteItem({ ids: [row.id] });
+		ElMessage.success('删除成功');
+		getList();
+	});
+};
+
+const formatOperator = (val:any) => {
+  // 1电信,2联通,3移动
+  if(val == 1) {
+    return "电信"
+  }else if(val == 2) {
+    return "联通"
+  }else if(val == 3) {
+    return "移动"
+  }
+}
+
+const formatType = (val:any) => {
+  // 1月卡,2季卡,3年卡,4其他
+  if(val == 1) {
+    return "月卡"
+  }else if(val == 2) {
+    return "季卡"
+  }else if(val == 3) {
+    return "年卡"
+  }else if(val == 4) {
+    return "其他"
+  }
+}
+
+const formatStatus = (val:any) => {
+  // 1:可激活 2:测试激活 3:测试去激活 4:在用5:停机6:运营商管理状态
+  if(val == 1) {
+    return "可激活"
+  }else if(val == 2) {
+    return "测试激活"
+  }else if(val == 3) {
+    return "测试去激活"
+  }else if(val == 4) {
+    return "在用"
+  }else if(val == 5) {
+    return "停机"
+  }else if(val == 6) {
+    return "运营商管理状态"
+  }
+}
+
+const onOpenDetail = (item:any) => {
+  router.push('/iotmanager/iotCard/index/detail/'+item.id);
+}
+</script>

+ 137 - 0
src/views/iot/iotCard/platformManage/index.vue

@@ -0,0 +1,137 @@
+<!-- 平台接入列表 -->
+<template>
+  <div class="page-full">
+    <el-card shadow="nover" class="page-full-part">
+      <el-form :model="params" inline ref="queryRef">
+        <el-form-item prop="deptName" class="mr10">
+          <el-input @keyup.enter.native="getList" style="width: 240px;" v-model="params.keyWord" placeholder="请输入ICCID或卡号" clearable />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="getList">
+            <el-icon>
+              <ele-Search />
+            </el-icon>
+            查询
+          </el-button>
+          <el-button @click="resetQuery()">
+            <el-icon>
+              <ele-Refresh />
+            </el-icon>
+            重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+      <el-table
+        :data="tableData"
+        max-height="calc(100vh  - 210px);"
+        v-loading="loading"
+        style="width: 100%"
+      >
+        <el-table-column label="名称" prop="name" align="center" />
+        <el-table-column label="平台类型" prop="types" align="center">
+        	<template #default="scope">{{ formatOperator(scope.row.types) }}</template>
+        </el-table-column>
+        <el-table-column label="状态" prop="simStatus" align="center">
+        	<template #default="scope">{{ formatStatus(scope.row.simStatus) }}</template>
+        </el-table-column> 
+        <el-table-column label="说明" prop="remark" align="center" />       
+
+        <el-table-column width="110" label="操作" fixed="right" prop="handle" align="center">
+					<template #default="scope">
+						<el-button size="small" text type="primary" @click="onOpenDetail(scope.row)">详情</el-button>
+						<el-button size="small" text type="warning" @click="onDel(scope.row)">删除</el-button>
+					</template>
+        </el-table-column>
+      </el-table>
+      <pagination
+        v-if="params.total"
+        :total="params.total"
+        v-model:page="params.pageNum"
+        v-model:limit="params.pageSize"
+        @pagination="getList()"
+      />
+    </el-card>
+    <!-- <EditDept ref="editDeptRef" @deptList="deptList" /> -->
+  </div>
+</template>
+
+<script lang="ts" setup>
+import api from '/@/api/iotCard';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import { useSearch } from "/@/hooks/useCommon"
+import { useRouter } from 'vue-router';
+const { params, tableData, getList, loading } = useSearch<any[]>(
+  api.platform.getList,
+  "Data"
+)
+
+getList();
+
+const router = useRouter();
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+	params.keyWord = ""
+  getList();
+};
+
+/**
+ * 单一删除
+ */
+const onDel = (row: any) => {
+	ElMessageBox.confirm(`此操作将卡号为:“${row.accNumber}”,是否继续?`, '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+	}).then(async () => {
+		await api.simCard.deleteItem({ ids: [row.id] });
+		ElMessage.success('删除成功');
+		getList();
+	});
+};
+
+const formatOperator = (val:any) => {
+  // 1电信,2联通,3移动
+  if(val == 1) {
+    return "电信"
+  }else if(val == 2) {
+    return "联通"
+  }else if(val == 3) {
+    return "移动"
+  }
+}
+
+const formatType = (val:any) => {
+  // 1月卡,2季卡,3年卡,4其他
+  if(val == 1) {
+    return "月卡"
+  }else if(val == 2) {
+    return "季卡"
+  }else if(val == 3) {
+    return "年卡"
+  }else if(val == 4) {
+    return "其他"
+  }
+}
+
+const formatStatus = (val:any) => {
+  // 1:可激活 2:测试激活 3:测试去激活 4:在用5:停机6:运营商管理状态
+  if(val == 1) {
+    return "可激活"
+  }else if(val == 2) {
+    return "测试激活"
+  }else if(val == 3) {
+    return "测试去激活"
+  }else if(val == 4) {
+    return "在用"
+  }else if(val == 5) {
+    return "停机"
+  }else if(val == 6) {
+    return "运营商管理状态"
+  }
+}
+
+const onOpenDetail = (item:any) => {
+  router.push('/iotmanager/iotCard/index/detail/'+item.id);
+}
+</script>

+ 1 - 1
src/views/iot/ota-update/update/component/batch.vue

@@ -32,7 +32,7 @@
       <el-table-column label="类型" prop="types" width="120" align="center">
         <template #default="scope">
           <el-tag type="success" size="small" v-if="scope.row.types == 0">验证</el-tag>
-          <el-tag type="info" size="small" v-else>升级</el-tag>
+          <el-tag type="primary" size="small" v-else>升级</el-tag>
         </template>
       </el-table-column>
       <el-table-column label="状态" prop="active" width="120" align="center">

+ 6 - 10
src/views/iot/ota-update/update/component/check.vue

@@ -86,7 +86,7 @@ interface RuleFormState {
   name: string;
   waitVersion: string;
   method: string;
-  devices: Array;
+  devices: [];
   stratege: string;
   devOtaFirmwareId: number;
   push: string;
@@ -127,7 +127,7 @@ export default defineComponent({
         devOtaFirmwareId: 0,
         push: '2',
         pushDisabled: true,
-        types: '1',
+        types: "1",
         productId: '',
       },
       productData: [],
@@ -137,6 +137,7 @@ export default defineComponent({
         stratege: [{ required: true, message: '升级方式不能为空', trigger: 'blur' }],
         push: [{ required: true, message: '推送方式不能为空', trigger: 'blur' }],
         types: [{ required: true, message: '类型不能为空', trigger: 'blur' }],
+        devices: [{ required: true, message: '所属设备不能为空', trigger: 'blue'}],
       },
       deviceShow: false, // 所属设备是否显示
       deviceNameShow: false, // 回显设备名称状态
@@ -150,13 +151,8 @@ export default defineComponent({
     // 获取操作升级包类型
     const getFormType = () => {
       // 如果是验证类型,设备信息必填项
-      if (state.ruleForm.types === '1') {
+      if (state.ruleForm.types === "1") {
         state.deviceShow = true;
-        state.rules.devices = [{
-          required: true,
-          message: '所属设备不能为空',
-          trigger: 'blur'
-        }];
       } else { // 如果是升级类型,设备可选可不选
         delete (state.rules.devices);
         state.deviceShow = false;
@@ -194,7 +190,7 @@ export default defineComponent({
       }
       state.isShowDialog = true;
 
-      if (state.ruleForm.types == '1') {
+      if (state.ruleForm.types == "1") {
         state.deviceShow = true;
       }
     };
@@ -209,7 +205,7 @@ export default defineComponent({
         devOtaFirmwareId: 0,
         push: '2',
         pushDisabled: true,
-        types: '1',
+        types: "1",
         productId: '',
       };
     };

+ 20 - 4
src/views/iot/ota-update/update/component/deviceList.vue

@@ -6,9 +6,7 @@
           <el-form-item label="设备名称:" prop="name">
             <el-input v-model="tableData.param.deviceName" placeholder="请输入设备名称" clearable style="width: 240px" @submit.prevent />
           </el-form-item>
-
           <el-form-item>
-
             <el-button type="primary" class="ml10" @click="getDetail">
               <el-icon>
                 <ele-Search />
@@ -16,7 +14,6 @@
               查询
             </el-button>
           </el-form-item>
-
         </el-form>
       </div>
       <el-table :data="tableData.data" style="width: 100%" row-key="id" v-loading="tableData.loading">
@@ -47,6 +44,11 @@
           </template>
         </el-table-column>
         <el-table-column prop="createdAt" label="时间" min-width="100" align="center"></el-table-column>
+        <el-table-column label="操作" width="200" align="center">
+          <template #default="scope">
+            <el-button size="small" text type="primary" @click="distribute(scope.row)">手动下发</el-button>
+          </template>
+        </el-table-column>
       </el-table>
       <pagination v-show="tableData.total > 0" :total="tableData.total" v-model:page="tableData.param.pageNum" v-model:limit="tableData.param.pageSize" @pagination="getDetail" />
     </el-dialog>
@@ -56,6 +58,7 @@
 <script lang="ts">
 import api from '/@/api/ota';
 import {defineComponent, reactive, toRefs} from 'vue';
+import {ElMessage} from "element-plus";
 
 interface TableDataRow {
   id: number;
@@ -84,7 +87,7 @@ interface TableDataState {
 }
 
 export default defineComponent({
-  setup(prop) {
+  setup() {
     const state = reactive<TableDataState>({
       ids: [],
       tableData: {
@@ -125,11 +128,24 @@ export default defineComponent({
         state.tableData.total = res.Total;
       }).finally(() => (state.tableData.loading = false));
     };
+    // 手动下发
+    const distribute = (row: any) => {
+      const deviceKey = row.deviceKey;
+      const strategyId = row.strategyId;
+      api.batch.distribute({deviceKey: deviceKey, strategyId: strategyId}).then(() => {
+        ElMessage.success('操作成功');
+      })
+      // 因列表更新数据不是实时更新,需设置定时后在请求列表
+      setTimeout(() => {
+        getDetail();
+      }, 500);
+    }
     return {
       getDetail,
       openDialog,
       closeDialog,
       onCancel,
+      distribute,
       ...toRefs(state),
     };
   },

+ 4 - 3
src/views/iot/property/attribute/index.vue

@@ -33,9 +33,10 @@
 					<template #default="{ node, data }">
 						<div class="custom-tree-node">
 							<span class="tree-label">
-								<el-icon v-if="data.is_type == '2'">
-									<Expand />
+								<el-icon v-if="data.is_type != '2'">
+									<Folder />
 								</el-icon>
+                <SvgIcon name="iconfont icon-siweidaotu" v-if="data.is_type == '2'"></SvgIcon>
 								{{ node.label }}
 							</span>
 						</div>
@@ -69,7 +70,7 @@
 <script lang="ts" setup>
 import device from '/@/api/device'
 import { useSearch } from '/@/hooks/useCommon'
-import { Expand } from '@element-plus/icons-vue';
+import { Folder } from '@element-plus/icons-vue';
 
 import { ElMessageBox, ElMessage } from 'element-plus'
 import EditForm from './edit.vue'

+ 1 - 1
src/views/iot/property/dossier/component/from.vue

@@ -3,7 +3,7 @@
 	<div>
 		<div v-for="(item, index) in dataList" :key="index">
 
-      <el-form-item :label="item.title + ':'" :prop="item.name" class="form-item" v-if="item.types === 'input'">
+      <el-form-item :label="item.title + ''" :prop="item.name" class="form-item" v-if="item.types === 'input'">
         <el-input v-model="formData[item.name]" :placeholder="'请输入' + item.title" @input="saveData()" :readonly="disable" />
       </el-form-item>
 

+ 4 - 3
src/views/iot/property/dossier/index.vue

@@ -42,9 +42,10 @@
 					<template #default="{ node, data }">
 						<div class="custom-tree-node">
 							<span class="tree-label">
-								<el-icon v-if="data.is_type == '2'">
-									<Expand />
+								<el-icon v-if="data.is_type != '2'">
+									<Folder />
 								</el-icon>
+                <SvgIcon name="iconfont icon-siweidaotu" v-if="data.is_type == '2'"></SvgIcon>
 								{{ node.label }}
 							</span>
 						</div>
@@ -78,7 +79,7 @@
 <script lang="ts" setup>
 import device from '/@/api/device'
 import { useSearch } from '/@/hooks/useCommon'
-import { Expand } from '@element-plus/icons-vue'
+import { Folder } from '@element-plus/icons-vue'
 
 import { ElMessageBox, ElMessage } from 'element-plus'
 import EditForm from './edit.vue'