list.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. <template>
  2. <div>
  3. <!-- <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">-->
  4. <!-- <div class="search" v-show="showSearch">-->
  5. <!-- <el-form ref="queryFormRef" :model="state.queryParams" :inline="true" label-width="68px">-->
  6. <!-- <el-form-item label="部门名称" prop="menuName">-->
  7. <!-- <el-input v-model="state.queryParams.deptName" placeholder="请输入部门名称" clearable @keyup.enter="handleQuery" />-->
  8. <!-- </el-form-item>-->
  9. <!-- <el-form-item label="状态" prop="status">-->
  10. <!-- <el-select v-model="state.queryParams.status" placeholder="部门状态" clearable>-->
  11. <!-- <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />-->
  12. <!-- </el-select>-->
  13. <!-- </el-form-item>-->
  14. <!-- <el-form-item>-->
  15. <!-- <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>-->
  16. <!-- <el-button icon="Refresh" @click="resetQuery">重置</el-button>-->
  17. <!-- </el-form-item>-->
  18. <!-- </el-form>-->
  19. <!-- </div>-->
  20. <!-- </transition>-->
  21. <yt-crud
  22. v-bind="options"
  23. ref="crudRef"
  24. @onLoad="getData"
  25. :fun-props="{
  26. exportBtn: false,
  27. delBtn: layoutType !== 'card',
  28. }"
  29. :table-props="{
  30. selection: true,
  31. delBtn: false,
  32. viewBtn: false,
  33. editBtn: true,
  34. customTable: layoutType === 'card',
  35. menuSlot: true,
  36. menuWidth: 300,
  37. }"
  38. :loading="state.loading"
  39. :total="state.total"
  40. v-model:page="state.page"
  41. v-model:query="state.query"
  42. @delFun="handleDelete"
  43. @saveFun="onSave"
  44. @openBeforeFun="openBeforeFun"
  45. :addBtn = "hasPermission('iot:device:add')"
  46. >
  47. <template #rightToolbar>
  48. <el-radio-group v-model="layoutType">
  49. <el-radio-button label="table">
  50. <svg-icon icon-class="table2" />
  51. </el-radio-button>
  52. <el-radio-button label="card">
  53. <svg-icon icon-class="card" />
  54. </el-radio-button>
  55. </el-radio-group>
  56. </template>
  57. <template #customTable>
  58. <el-row class="card-list flex">
  59. <el-col class="card-item" v-for="(item, index) in data" :key="index" :class="item.state.online ? 'success-box' : 'error-box'">
  60. <div class="text-box">
  61. <div class="title flex align-center">
  62. <div class="title-l">
  63. <div class="icon">
  64. <svg-icon icon-class="card2" />
  65. </div>
  66. {{ item.deviceName }}
  67. </div>
  68. <div class="title-r">
  69. <status-tag :type="item.state.online ? 'success' : 'danger'" :text="item.state.online ? '在线' : '离线'" />
  70. </div>
  71. </div>
  72. <div class="text flex">
  73. <div class="txt">
  74. <div class="txt-item">
  75. <div class="label">所属产品</div>
  76. <div class="value active">{{ getProductName(item.productKey) }}</div>
  77. </div>
  78. <div class="txt-item">
  79. <div class="label">设备类型</div>
  80. <div class="value">{{ getNodeTypeName(item.productKey) }}</div>
  81. </div>
  82. <div class="txt-item">
  83. <div class="copy-tag" v-copyText="item.deviceId" v-copyText:callback="copyIdSuccess">
  84. <svg-icon icon-class="copy" />
  85. 设备ID
  86. </div>
  87. </div>
  88. </div>
  89. <div class="img">
  90. <img :src="defaultImg" alt="" />
  91. </div>
  92. </div>
  93. </div>
  94. <div class="btn-group">
  95. <el-button
  96. v-if="item.productKey === 'openiitagateway01'"
  97. class="cu-btn"
  98. type="success"
  99. icon="Box"
  100. plain
  101. @click="showChidrenDevices(item)"
  102. >子设备</el-button
  103. >
  104. <el-button class="cu-btn" type="primary" icon="EditPen" plain @click="crudRef.handleUpdate(item)" >编辑</el-button>
  105. <el-button class="cu-btn" type="warning" icon="View" plain @click="handleView(item)">详情</el-button>
  106. <el-divider direction="vertical" />
  107. <el-popconfirm title="是否确认删除?" @confirm="handleDelete(item)">
  108. <template #reference>
  109. <el-button type="danger" icon="Delete" plain />
  110. </template>
  111. </el-popconfirm>
  112. </div>
  113. </el-col>
  114. </el-row>
  115. </template>
  116. <template #state="scope">
  117. <el-tag v-if="scope.row.state.online" type="success" size="small">在线</el-tag>
  118. <el-tag v-else type="danger" size="small">离线</el-tag>
  119. </template>
  120. <template #menuSlot="scope">
  121. <!-- TODO: 没接口,nodeType无法获取,得改成 != 0 -->
  122. <el-tooltip class="box-item" effect="dark" content="子设备" placement="top">
  123. <el-button link icon="Box" :disabled="scope.row.nodeType == 0" @click="showChidrenDevices(scope.row)" />
  124. </el-tooltip>
  125. <el-divider direction="vertical" />
  126. <el-tooltip class="box-item" effect="dark" content="详情" placement="top">
  127. <el-button link type="primary" icon="View" @click="handleView(scope.row)" />
  128. </el-tooltip>
  129. <el-divider direction="vertical" />
  130. <el-tooltip class="box-item" effect="dark" content="删除" placement="top">
  131. <el-popconfirm title="是否确认删除该数据" @confirm="handleDelete(scope.row)">
  132. <template #reference>
  133. <el-button link type="danger" icon="Delete" />
  134. </template>
  135. </el-popconfirm>
  136. </el-tooltip>
  137. </template>
  138. <template #type="{ row }">{{ getNodeTypeName(row.productKey) }}</template>
  139. <template #deviceMapFormItem="{ row }">
  140. <div v-if="state.showDeviceMap">
  141. <Map :clickMap="true" @locateChange="(lnglat) => locateChange(lnglat, row)" :isWrite="true" v-model:center="state.mapLnglat" />
  142. </div>
  143. </template>
  144. </yt-crud>
  145. <children-dialog ref="childrenDialogRef" />
  146. </div>
  147. </template>
  148. <script lang="ts" setup>
  149. import defaultImg from '@/assets/images/pic_device.png'
  150. import { IColumn } from '@/components/common/types/tableCommon'
  151. import { ComponentInternalInstance } from 'vue'
  152. import { getDevicesList, deleteDevices, saveDevices,getParentDevices, deleteBatchDevices } from '../api/devices.api'
  153. import { getProductsList,IProductsVO } from '../api/products.api'
  154. import Map from '@/components/Map/index.vue'
  155. import ChildrenDialog from './modules/childrenDialog.vue'
  156. import YtCrud from '@/components/common/yt-crud.vue'
  157. import { ElPopconfirm } from 'element-plus'
  158. import StatusTag from '@/components/StatusTag/index.vue'
  159. import { hasPermission } from '@/utils/auth'
  160. import { listDept} from '@/api/system/dept'
  161. const { proxy } = getCurrentInstance() as ComponentInternalInstance
  162. interface DeptOptionsType {
  163. id: number | string
  164. deptName: string
  165. children: DeptOptionsType[]
  166. }
  167. const state = reactive({
  168. queryParams: {
  169. pageNum: 1,
  170. pageSize: 10,
  171. deptName: undefined,
  172. status: undefined,
  173. },
  174. page: {
  175. pageSize: 12,
  176. pageNum: 1,
  177. },
  178. total: 0,
  179. loading: false,
  180. showDeviceMap: false,
  181. mapLnglat: '' as any,
  182. query: {},
  183. })
  184. const layoutType = ref('card')
  185. // 查看详情
  186. const router = useRouter()
  187. const handleView = (row: any) => {
  188. if (!row.id) return
  189. let showMap=false
  190. productOptions.value.forEach((p) => {
  191. if (p.productKey == row.productKey ) {
  192. showMap=p.isOpenLocate
  193. }
  194. })
  195. router.push(`devicesDetail/${row.id}?showMap=${showMap}`)
  196. }
  197. const nodeTypeOptions = [
  198. {
  199. value: 0,
  200. label: '网关设备',
  201. }, {
  202. value: 1,
  203. label: '网关子设备',
  204. }, {
  205. value: 2,
  206. label: '直连设备',
  207. },
  208. ]
  209. // const { proxy } = getCurrentInstance() as ComponentInternalInstance
  210. // 打开子设备
  211. const childrenDialogRef = ref()
  212. const showChidrenDevices = (row: any) => {
  213. childrenDialogRef.value.openDialog(row)
  214. }
  215. // 复制ID
  216. const copyIdSuccess = () => {
  217. proxy?.$modal.msgSuccess('复制成功')
  218. }
  219. // 产品字典
  220. const productOptions = ref<IProductsVO[]>([])
  221. const deptOptions = ref<DeptOptionsType[]>([])
  222. // 组列表
  223. const groupOptions = [
  224. {
  225. 'id': 'g3',
  226. 'name': '组3',
  227. 'uid': 'fa1c5eaa-de6e-48b6-805e-8f091c7bb831',
  228. 'remark': '2223333',
  229. 'deviceQty': 17,
  230. 'createAt': 1659872082792
  231. },
  232. {
  233. 'id': 'g2',
  234. 'name': '组2',
  235. 'uid': 'fa1c5eaa-de6e-48b6-805e-8f091c7bb831',
  236. 'remark': '222',
  237. 'deviceQty': 21,
  238. 'createAt': 1659872082803
  239. },
  240. {
  241. 'id': 'g1',
  242. 'name': '分组1',
  243. 'uid': 'fa1c5eaa-de6e-48b6-805e-8f091c7bb831',
  244. 'remark': '1111',
  245. 'deviceQty': 10,
  246. 'createAt': 1659872082805
  247. }
  248. ]
  249. const column = ref<IColumn[]>([{
  250. label: '设备ID',
  251. key: 'deviceId',
  252. formHide: true,
  253. rules: [{ required: true, message: 'ProductKey不能为空' }],
  254. }, {
  255. label: '产品',
  256. key: 'productKey',
  257. type: 'select',
  258. search: true,
  259. colSpan:12,
  260. tableWidth: 120,
  261. editDisabled: true,
  262. componentProps: {
  263. labelAlias: 'name',
  264. valueAlias: 'productKey',
  265. options: [],
  266. },
  267. rules: [{ required: true, message: '产品名称不能为空' }],
  268. formWatch: (scope) => {
  269. scope.column.forEach((f: IColumn) => {
  270. if (['parentId', 'longitude', 'latitude'].includes(f.key)) {
  271. if (!scope.value) {
  272. f.formHide = true
  273. state.showDeviceMap = false
  274. return
  275. }
  276. productOptions.value.forEach((p)=>{
  277. if (p.productKey == scope.value ) {
  278. if (f.key === 'parentId') {
  279. f.formHide = p.nodeType !== 1
  280. } else {
  281. const flag = p.isOpenLocate && 'manual' == p.locateUpdateType
  282. state.showDeviceMap = flag
  283. f.formHide = !flag
  284. }
  285. }
  286. })
  287. }
  288. })
  289. column.value = scope.column
  290. }
  291. }, {
  292. label: '设备类型',
  293. key: 'type',
  294. slot: true,
  295. formHide: true,
  296. }, {
  297. label: '网关设备',
  298. key: 'parentId',
  299. type: 'select',
  300. colSpan: 12,
  301. tableWidth: 120,
  302. hide: true,
  303. formHide: true,
  304. componentProps: {
  305. labelAlias: 'deviceName',
  306. valueAlias: 'id',
  307. options: [],
  308. placeholder: '子设备可选择父设备'
  309. },
  310. }, {
  311. label: '设备DN',
  312. key: 'deviceName',
  313. tableWidth: 240,
  314. componentProps: {
  315. placeholder: '一般为设备mac'
  316. },
  317. rules: [{ required: true, message: '设备DN不能为空' }],
  318. },
  319. {
  320. label: '设备经度',
  321. key: 'longitude',
  322. hide: true,
  323. formHide: true,
  324. colSpan: 12,
  325. },
  326. {
  327. label: '设备纬度',
  328. key: 'latitude',
  329. hide: true,
  330. formHide: true,
  331. colSpan: 12,
  332. },
  333. {
  334. label: '设备地图',
  335. key: 'deviceMap',
  336. hide: true,
  337. formItemSlot: true,
  338. },
  339. // , {
  340. // label: '分组',
  341. // key: 'group',
  342. // type: 'select',
  343. // componentProps: {
  344. // labelAlias: 'name',
  345. // valueAlias: 'id',
  346. // options: groupOptions,
  347. // },
  348. // }
  349. {
  350. label: '状态',
  351. key: 'state',
  352. type: 'select',
  353. componentProps: {
  354. options: [
  355. {
  356. label: '在线',
  357. value: 'online',
  358. },
  359. {
  360. label: '离线',
  361. value: 'offline',
  362. }
  363. ]
  364. },
  365. search: true,
  366. formHide: true,
  367. tableWidth: 80,
  368. slot: true,
  369. }, {
  370. label: '关键字',
  371. key: 'keyword',
  372. search: true,
  373. hide: true,
  374. formHide: true,
  375. },
  376. {
  377. label: '所属部门',
  378. key: 'createDept',
  379. search: true,
  380. type: 'select',
  381. hide: true,
  382. formHide: true,
  383. componentProps: {
  384. labelAlias: 'deptName',
  385. valueAlias: 'id',
  386. options: [],
  387. }
  388. },
  389. {
  390. label: '创建时间',
  391. key: 'createAt',
  392. tableWidth: 180,
  393. sortable: true,
  394. type: 'date',
  395. formHide: true,
  396. }])
  397. const crudRef = ref()
  398. const data = ref<any[]>([])
  399. const getData = () => {
  400. state.loading = true
  401. getDevicesList({
  402. ...state.page,
  403. ...state.query,
  404. }).then((res) => {
  405. data.value = res.data.rows
  406. state.total = res.data.total
  407. }).finally(() => {
  408. state.loading = false
  409. })
  410. }
  411. const getDict = () => {
  412. getProductsList({
  413. pageNum: 1,
  414. pageSize: 99999,
  415. }).then(res => {
  416. productOptions.value = res.data.rows || []
  417. column.value.forEach(item => {
  418. if (item.key === 'productKey') {
  419. item.componentProps.options = productOptions.value
  420. }
  421. })
  422. })
  423. }
  424. getDict()
  425. const getDept = () => {
  426. listDept().then((res) => {
  427. // const data = proxy?.handleTree<DeptOptionsType>(res.data, 'id')
  428. deptOptions.value = res.data || []
  429. // console.log("################"+JSON.stringify(deptOptions.value))
  430. column.value.forEach(item => {
  431. if (item.key === 'createDept') {
  432. item.componentProps.options = deptOptions.value
  433. }
  434. })
  435. // const data = proxy?.handleTree<DeptOptionsType>(res.data, 'id')
  436. // console.log("####################"+JSON.stringify(data))
  437. // if (data) {
  438. // deptOptions.value = data
  439. // }
  440. })
  441. }
  442. getDept()
  443. const getProductName = (key: string) => {
  444. return productOptions.value.find(f => f.productKey === key)?.name || ''
  445. }
  446. const getNodeTypeName = (key) => {
  447. const type = productOptions.value.find(f => f.productKey === key)?.nodeType
  448. return nodeTypeOptions.find(f => f.value === type)?.label || ''
  449. }
  450. // 保存数据
  451. const onSave = async ({type, data, cancel}: any) => {
  452. state.loading = true
  453. await saveDevices(toRaw(data))
  454. state.loading = false
  455. cancel()
  456. getData()
  457. }
  458. // 弹窗前置操作
  459. const openBeforeFun = ({type, data}) => {
  460. if (type === 'add') {
  461. state.mapLnglat=''
  462. } else if (type === 'update') {
  463. const latitude = data?.locate?.latitude || ''
  464. const longitude = data?.locate?.longitude || ''
  465. state.mapLnglat = longitude + ',' + latitude
  466. }
  467. }
  468. const parentDevices = async () => {
  469. let data = await getParentDevices()
  470. column.value.forEach(item => {
  471. if (item.key === 'parentId') {
  472. item.componentProps.options = data.data
  473. }
  474. })
  475. }
  476. parentDevices()
  477. // 删除
  478. const handleDelete = async (row: any) => {
  479. state.loading = true
  480. if (row instanceof Array) {
  481. await deleteBatchDevices(row.map(m => m.id))
  482. } else {
  483. await deleteDevices(row.id)
  484. }
  485. ElMessage.success('删除成功!')
  486. state.loading = false
  487. getData()
  488. }
  489. const locateChange=(e, row)=> {
  490. if (!e) return
  491. row.longitude = e[0] || ''
  492. row.latitude = e[1] || ''
  493. }
  494. const options = reactive({
  495. ref: 'crudRef',
  496. data,
  497. column,
  498. })
  499. </script>
  500. <style lang="scss" scoped>
  501. ::v-deep(.el-radio-button__inner) {
  502. padding: 8px;
  503. }
  504. ::v-deep(.el-radio-button__original-radio:checked+.el-radio-button__inner) {
  505. border: 1px solid #0070ffff;
  506. background: #0070ff1a;
  507. box-shadow: none;
  508. svg {
  509. path {
  510. fill: #0070ffff;
  511. }
  512. }
  513. }
  514. .card-list {
  515. .card-item {
  516. border: 1px solid #d8dee5;
  517. border-radius: 3px;
  518. margin-right: 16px;
  519. margin-bottom: 16px;
  520. flex: 0 0 calc(25% - 12px);
  521. width: calc(25% - 12px);
  522. &.success-box {
  523. .text-box {
  524. background: linear-gradient(141.6deg, rgb(238, 250, 255) 0%, rgba(255, 255, 255, 0) 80%);
  525. }
  526. }
  527. &.error-box {
  528. .text-box {
  529. background: linear-gradient(141.6deg, rgb(255, 241, 241) 0%, rgba(255, 255, 255, 0) 80%);
  530. }
  531. }
  532. &:nth-child(4n) {
  533. margin-right: 0;
  534. }
  535. .text-box {
  536. padding: 16px;
  537. .title {
  538. font-size: 16px;
  539. font-weight: 600;
  540. align-items: center;
  541. margin-bottom: 12px;
  542. width: 100%;
  543. display: flex;
  544. align-items: center;
  545. justify-content: space-between;
  546. .title-l {
  547. display: flex;
  548. align-items: center;
  549. }
  550. .icon {
  551. margin-right: 10px;
  552. display: flex;
  553. align-items: center;
  554. }
  555. }
  556. .text {
  557. align-items: center;
  558. font-size: 14px;
  559. .txt {
  560. flex: 1;
  561. .txt-item {
  562. margin-bottom: 10px;
  563. &:last-child {
  564. margin-bottom: 0;
  565. }
  566. .copy-tag {
  567. padding: 4px 8px;
  568. background-color: #FFF7EF;
  569. color: #FF7D00;
  570. display: inline-flex;
  571. align-items: center;
  572. transition: 0.3s ease;
  573. cursor: pointer;
  574. &:hover {
  575. opacity: 0.8;
  576. transform: translateY(-2px);
  577. }
  578. svg {
  579. margin-right: 8px;
  580. }
  581. }
  582. }
  583. border-radius: 2px;
  584. .label {
  585. display: inline-block;
  586. margin-right: 10px;
  587. color: #717C8E;
  588. }
  589. .value {
  590. display: inline-block;
  591. color: #0B1D30;
  592. &.active {
  593. color: #0070FF;
  594. }
  595. }
  596. }
  597. .img {
  598. width: 100px;
  599. height: 100px;
  600. img {
  601. width: 100%;
  602. height: auto;
  603. }
  604. }
  605. }
  606. }
  607. .btn-group {
  608. padding: 12px 16px;
  609. border-top: 1px solid #DCDFE1;
  610. .cu-btn {
  611. width: calc((100% - 73px) / 3);
  612. }
  613. .el-button {
  614. padding: 8px;
  615. }
  616. display: flex;
  617. justify-content: flex-end;
  618. align-items: center;
  619. }
  620. }
  621. }
  622. @media screen and (max-width: 1560px) {
  623. .card-list .card-item .btn-group {
  624. padding: 12px;
  625. .el-button {
  626. font-size: 12px;
  627. }
  628. .cu-btn {
  629. width: calc((100% - 59px) / 3);
  630. }
  631. .el-button+.el-button {
  632. margin-left: 6px;
  633. }
  634. }
  635. }
  636. @media screen and (max-width: 1400px) {
  637. .card-list .card-item {
  638. width: calc(100% / 3 - 8px);
  639. flex: 0 0 calc(100% / 3 - 8px);
  640. margin-right: 12px;
  641. &:nth-child(4n) {
  642. margin-right: 12px;
  643. }
  644. &:nth-child(3n) {
  645. margin-right: 0;
  646. }
  647. }
  648. }
  649. </style>