index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. <template>
  2. <a-card title="设备统计" >
  3. <a-row justify="center">
  4. <a-col
  5. v-for="item in state.deviceCount"
  6. :key="item.key"
  7. :xs="12" :sm="12" :md="6" :lg="8" :xl="4"
  8. >
  9. <div class="statistic" >
  10. <span class="count" :style="{color: item.color}" >{{item.value}}</span>
  11. <span>台</span>
  12. </div>
  13. <div class="label" >
  14. {{item.label}}
  15. </div>
  16. </a-col>
  17. </a-row>
  18. </a-card>
  19. <a-card style="margin-top: 20px;">
  20. <a-row justify="space-between" >
  21. <a-col :span="20" >
  22. <a-form layout="inline" >
  23. <a-form-item label="设备状态" >
  24. <a-select allowClear style="width: 176px;" v-model:value="searchState.deviceStatus" placeholder="请输入设备状态">
  25. <a-select-option
  26. v-for="item in DeviceContriller.deviceStatus"
  27. :key="item.key"
  28. :value="item.key"
  29. >{{item.name}}</a-select-option>
  30. </a-select>
  31. <!-- <a-input v-model:value="searchState.deviceStatus" placeholder="请输入设备状态"/> -->
  32. </a-form-item>
  33. <a-form-item style="width: 400px;">
  34. <a-input-group compact >
  35. <a-select v-model:value="searchState.searchKey" style="width: 150px;" >
  36. <a-select-option v-for="item in searchList" :key="item.key" :value="item.key">{{item.name}}</a-select-option>
  37. </a-select>
  38. <a-input v-model:value="searchState.searchValue" style="width: 176px" placeholder="请输入查询值"/>
  39. </a-input-group>
  40. </a-form-item>
  41. <a-form-item><a-button @click="resetFields([])">重置</a-button></a-form-item>
  42. <a-form-item><a-button type="primary" @click="getDevicePage">搜索</a-button> </a-form-item>
  43. </a-form>
  44. </a-col>
  45. <a-col :span="2" >
  46. <a-button type="primary" @click="openModal('add', {})"> + 新增设备</a-button>
  47. </a-col>
  48. </a-row>
  49. <a-table
  50. style="margin-top: 10px;"
  51. :columns="columns"
  52. :data-source="state.dataSource"
  53. :loading="state.loading"
  54. :pagination="searchState"
  55. @change="changePage"
  56. >
  57. <template #bodyCell="{column, record}">
  58. <template v-if="column.key === 'id'" >
  59. <a @click="goDetailPage(record.id)">{{record.id}}</a>
  60. </template>
  61. <template v-if="column.key === 'deviceStatus'" >
  62. <a-tag :color="record.deviceStatus.color" >{{record.deviceStatus.name}}</a-tag>
  63. </template>
  64. <template v-if="column.key === 'deviceNodeType'" >
  65. <a-tag>{{record.deviceNodeType == 'GATEWAY'?'直连类型':'非直连类型'}}</a-tag>
  66. </template>
  67. <template v-if="column.key === 'action'">
  68. <a-space>
  69. <a @click="goDetailPage(record.id)">查看</a>
  70. <a @click="goDebugPage(record.id)" >调试</a>
  71. <a-popconfirm
  72. title="确实要删除吗?"
  73. ok-text="确定"
  74. cancel-text="取消"
  75. @confirm="delDevice(record.id)"
  76. >
  77. <a href="#">删除</a>
  78. </a-popconfirm>
  79. <a-dropdown>
  80. <a class="ant-dropdown-link" @click.prevent>分析 <DownOutlined /> </a>
  81. <template #overlay>
  82. <a-menu>
  83. <a-menu-item
  84. v-for="item in analysisList"
  85. :key="item.key"
  86. @click="openAnalysisModal(item.key, record.id)"
  87. >
  88. <a href="javascript:;">{{item.label}}</a>
  89. </a-menu-item>
  90. </a-menu>
  91. </template>
  92. </a-dropdown>
  93. </a-space>
  94. </template>
  95. </template>
  96. </a-table>
  97. </a-card>
  98. <a-modal
  99. title="单设备注册"
  100. :open="state.visible"
  101. @cancel="state.visible = false"
  102. @ok="ok"
  103. >
  104. <a-form :labelCol="{span: 6}" :wrapperCol="{span: 14}" >
  105. <a-form-item label="所属产品" v-bind="validateInfosDeviceState.modelId" >
  106. <a-select v-model:value="deviceState.modelId" >
  107. <a-select-option
  108. v-for="item in state.modelList"
  109. :key="item.id"
  110. :value="item.id"
  111. >
  112. {{item.modelLabel}}
  113. </a-select-option>
  114. </a-select>
  115. </a-form-item>
  116. <a-form-item label="设备标识码" v-bind="validateInfosDeviceState.deviceCode" >
  117. <a-input v-model:value="deviceState.deviceCode" />
  118. </a-form-item>
  119. <a-form-item label="设备名称" >
  120. <a-input v-model:value="deviceState.deviceLabel" />
  121. </a-form-item>
  122. <a-form-item label="设备认证类型" >
  123. <a-radio-group v-model:value="deviceState.deviceAuthType">
  124. <a-radio value="SECRET">密钥认证</a-radio>
  125. <a-radio value="X509CERT">x509证书</a-radio>
  126. </a-radio-group>
  127. </a-form-item>
  128. <a-form-item label="密匙" v-if="deviceState.deviceAuthType === DeviceAuthTypeEnum.SECRET " >
  129. <a-input-password v-model:value="deviceState.deviceSecret" />
  130. </a-form-item>
  131. <a-form-item label="确认密匙" v-if="deviceState.deviceAuthType === DeviceAuthTypeEnum.SECRET ">
  132. <a-input-password v-model:value="deviceState.confirmSecret" />
  133. </a-form-item>
  134. </a-form>
  135. </a-modal>
  136. <modal-pro
  137. :label="analysisTitle"
  138. :open="state.analysisVisible"
  139. @cancel="state.analysisVisible = false"
  140. @ok="state.analysisVisible = false"
  141. style="width: 700px;"
  142. >
  143. <a-row :gutter="[8, 8]" style="margin-top: 20px;" >
  144. <a-col :span="24" >
  145. <a-range-picker @change="changeRangePicker" format="YYYY/MM/DD" />
  146. </a-col>
  147. <a-spin :spinning="analysisState.loading" >
  148. <a-col :span="24" v-if="state.analysisType === 'session'">
  149. <div id="device-session" style="width: 600px;height: 400px;" ></div>
  150. </a-col>
  151. <a-col :span="24" v-else>
  152. <div id="device-attr" style="width: 600px;height: 400px;" ></div>
  153. </a-col>
  154. </a-spin>
  155. </a-row>
  156. </modal-pro>
  157. </template>
  158. <script lang="ts" setup >
  159. import { DeviceContriller, ModelController } from '@/controller/index'
  160. import { onMounted, reactive, ref, toRefs, nextTick, computed, watch } from 'vue'
  161. import { Form, message } from 'ant-design-vue'
  162. import { DeviceAuthTypeEnum } from '@/enum/common'
  163. import { useRouter } from 'vue-router'
  164. import { DownOutlined } from '@ant-design/icons-vue'
  165. import * as echarts from 'echarts'
  166. import { sessionEchartsJson, attrEchartsJson } from './json/echartsJson'
  167. import dayjs from 'dayjs'
  168. const useForm = Form.useForm
  169. const router = useRouter()
  170. const columns = [
  171. {
  172. title: '设备ID',
  173. dataIndex: 'id',
  174. key: 'id'
  175. },
  176. {
  177. title: '设备名称',
  178. dataIndex: 'deviceLabel'
  179. },
  180. {
  181. title: '设备标识码',
  182. dataIndex: 'deviceCode'
  183. },
  184. {
  185. title: '设备描述',
  186. dataIndex: 'deviceDescription'
  187. },
  188. {
  189. title: '节点类型',
  190. dataIndex: 'deviceNodeType',
  191. key: 'deviceNodeType'
  192. },
  193. {
  194. title: '所属产品',
  195. dataIndex: 'modelLabel'
  196. },
  197. {
  198. title: '状态',
  199. dataIndex: 'deviceStatus',
  200. key: 'deviceStatus'
  201. },
  202. {
  203. title: '操作',
  204. key: 'action'
  205. }
  206. ]
  207. const searchList = [
  208. { name: '设备id', key: 'deviceId' },
  209. { name: '设备名称', key: 'deviceLabel' },
  210. { name: '设备标识码', key: 'deviceCode' }
  211. ]
  212. const analysisList = [
  213. { label: '上下线分析', key: 'session' },
  214. { label: '属性分析', key: 'attr' }
  215. ]
  216. const analysisTitle = computed(() => state.analysisType === 'session' ? '上下线分析' : '属性分析')
  217. const state = reactive<{
  218. modelId: string,
  219. deviceCount: any,
  220. dataSource: IOT.API.DEVICE.Device[],
  221. loading: boolean,
  222. visible: boolean,
  223. opraState: 'add' | 'update',
  224. modelList: IOT.API.MODEL.ModelDot[]
  225. analysisVisible: boolean
  226. analysisType: 'session' | 'attr' | ''
  227. }>({
  228. modelId: '',
  229. deviceCount: [],
  230. dataSource: [],
  231. modelList: [],
  232. loading: false,
  233. visible: false,
  234. analysisVisible: false,
  235. analysisType: '',
  236. opraState: 'add'
  237. })
  238. const searchState = reactive({
  239. page: 1,
  240. pageSize: 10,
  241. total: 0,
  242. deviceStatus: '',
  243. modelId: '',
  244. searchKey: 'deviceId',
  245. searchValue: ''
  246. })
  247. const deviceState = reactive({
  248. id: '',
  249. modelId: '',
  250. deviceLabel: '',
  251. deviceCode: '',
  252. deviceDescription: '',
  253. deviceAuthType: DeviceAuthTypeEnum.SECRET,
  254. modelLabel: '',
  255. deviceSecret: '',
  256. confirmSecret: ''
  257. })
  258. const analysisState = reactive({
  259. deviceId: '',
  260. start: 0,
  261. end: '',
  262. dataSource: [],
  263. loading: false
  264. })
  265. watch(
  266. () => analysisState.dataSource,
  267. () => {
  268. if (state.analysisType === 'session') {
  269. const chartDom = document.getElementById('device-session')
  270. const myChart = echarts.init(chartDom!)
  271. sessionEchartsJson.xAxis.data = analysisState.dataSource.map(item => item.createAt)
  272. myChart.setOption(sessionEchartsJson)
  273. analysisState.loading = false
  274. } else {
  275. const chartDom = document.getElementById('device-attr')
  276. const myChart = echarts.init(chartDom!)
  277. attrEchartsJson.xAxis.data = analysisState.dataSource.map(item => item.keyLabel) || []
  278. attrEchartsJson.series[0].data = analysisState.dataSource.map(item => item.longValue) || []
  279. console.log('attrEchartsJson:', attrEchartsJson)
  280. myChart.setOption(attrEchartsJson)
  281. analysisState.loading = false
  282. }
  283. }
  284. )
  285. const { resetFields, validate, validateInfos } = useForm(searchState, {})
  286. const { resetFields: resetFieldsDevice, validate: validateDevice, validateInfos: validateInfosDeviceState } = useForm(deviceState, reactive({
  287. modelId: [{ required: true, message: '请选择所属产品' }],
  288. deviceCode: [{ required: true, message: '请填写设备码' }]
  289. }))
  290. const changeRangePicker = (time) => {
  291. const [startTime, endTime] = time
  292. console.log(startTime, endTime)
  293. analysisState.start = new Date(startTime).getTime()
  294. analysisState.end = new Date(endTime).getTime()
  295. state.analysisType === 'session' ? getDeviceSession() : getDeviceAttr()
  296. }
  297. const openAnalysisModal = (key: 'session' | 'attr', id: string) => {
  298. state.analysisVisible = true
  299. state.analysisType = key
  300. analysisState.deviceId = id
  301. key === 'session' ? getDeviceSession() : getDeviceAttr()
  302. }
  303. const changePage = ({ current }) => {
  304. searchState.page = current
  305. getDevicePage()
  306. }
  307. const goDebugPage = (id: string) => {
  308. router.push({ path: '/devOps/onlineTest', query: { id } })
  309. }
  310. const goDetailPage = (id: string) => {
  311. router.push({ path: '/device/detail', query: { id } })
  312. }
  313. const getDeviceSession = async () => {
  314. analysisState.loading = true
  315. const { data } = await DeviceContriller.getSession({ deviceId: analysisState.deviceId, start: analysisState.start, end: analysisState.end })
  316. analysisState.dataSource = data
  317. }
  318. const getDeviceAttr = async () => {
  319. analysisState.loading = true
  320. const { data } = await DeviceContriller.getAttr({ deviceId: analysisState.deviceId, start: analysisState.start, end: analysisState.end })
  321. analysisState.dataSource = data.hum
  322. }
  323. const delDevice = async (id: string) => {
  324. await DeviceContriller.del(id)
  325. getDevicePage()
  326. }
  327. const ok = () => {
  328. if (deviceState.deviceAuthType === DeviceAuthTypeEnum.SECRET && deviceState.deviceSecret !== deviceState.confirmSecret) {
  329. message.warn('两次密匙输入不同')
  330. return
  331. }
  332. validateDevice().then(async (r) => {
  333. await DeviceContriller.post(deviceState)
  334. state.visible = false
  335. getDevicePage()
  336. })
  337. }
  338. const getModelList = async () => {
  339. const { data } = await ModelController.list()
  340. state.modelList = data
  341. }
  342. const openModal = (opraState: 'add' | 'update', record:Partial<IOT.API.DEVICE.Device> = {}) => {
  343. state.visible = true
  344. state.opraState = opraState
  345. getModelList()
  346. }
  347. const getDevicePage = async () => {
  348. state.loading = true
  349. const { data, sum } = await DeviceContriller.page(searchState)
  350. state.loading = false
  351. state.dataSource = data
  352. searchState.total = sum
  353. }
  354. const getDeviceStatistics = async () => {
  355. const data = await DeviceContriller.statistics({ modelId: state.modelId })
  356. state.deviceCount = data as any
  357. }
  358. onMounted(() => {
  359. getDeviceStatistics()
  360. getDevicePage()
  361. })
  362. </script>
  363. <style lang="less" scoped >
  364. @import '~@/styles/theme.less';
  365. .statistic {
  366. color: @sublabel-color;
  367. .count {
  368. font-size: 48px;
  369. margin-right: 10px;
  370. }
  371. }
  372. .label {
  373. font-size: 18px;
  374. color: @sublabel-color;
  375. }
  376. </style>