cloudview.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. <template>
  2. <a-card
  3. style="width: 100%"
  4. :tab-list="tabListNoTitle"
  5. :active-tab-key="state.tasActive"
  6. @tabChange="onTabChange"
  7. >
  8. <template v-if="state.tasActive == 'msg'" >
  9. <a-alert :message="tipMessage" type="info" show-icon />
  10. <div class="subtitle" >注意:平台为每个设备默认最多保存20条消息,超过20条后,后续的消息会替换下发最早的消息。</div>
  11. <a-row justify="space-between" >
  12. <a-col></a-col>
  13. <a-col> <a-button type="primary" @click="state.visible = true">下发消息</a-button> </a-col>
  14. </a-row>
  15. </template>
  16. <template v-else >
  17. <a-alert :message="tipMessage" type="info" show-icon style="margin-bottom: 20px;" />
  18. <a-row justify="space-between" >
  19. <a-col class="msg-title" >
  20. <div class="title" >同步命令下发</div>
  21. <div class="subtitle" >同步命令暂不支持历史记录查看。</div>
  22. </a-col>
  23. <a-col> <a-button type="primary" @click="state.visible = true">命令下发</a-button> </a-col>
  24. </a-row>
  25. </template>
  26. <a-table
  27. style="margin-top: 20px;"
  28. :columns="state.tasActive === 'msg' ? columns :cmdColumns"
  29. :dataSource="state.msgDataSource"
  30. :loading="state.loading"
  31. >
  32. <template #bodyCell="{ column, record }">
  33. <template v-if="column.key === 'status'">
  34. <span>{{ DeviceContriller.deviceMag.get(record.status)?.name }}</span>
  35. </template>
  36. <template v-if="column.key === 'createAt'">
  37. {{dayjs(record.createAt).format('YYYY-MM-DD HH:mm:ss')}}
  38. </template>
  39. <template v-if="column.key === 'action'">
  40. <a @click="openDetailModal(record)" >详情</a>
  41. </template>
  42. </template>
  43. </a-table>
  44. </a-card>
  45. <modal-pro
  46. style="width: 700px;"
  47. :title="modalTitle"
  48. :visible="state.visible"
  49. @cancel="state.visible = false"
  50. @ok="ok"
  51. >
  52. <a-form
  53. v-if="state.tasActive === 'msg'"
  54. :labelCol="{span: 4}"
  55. :wrapperCol="{span: 14}"
  56. >
  57. <a-form-item
  58. label="消息名称"
  59. v-bind="validateInfos.msgLabel"
  60. >
  61. <a-input v-model:value="msgState.msgLabel" > </a-input>
  62. </a-form-item>
  63. <a-form-item
  64. label="Topic"
  65. >
  66. <a-input v-model:value="msgState.topic" > </a-input>
  67. </a-form-item>
  68. <a-form-item
  69. label="消息内容"
  70. v-bind="validateInfos.msgPayload"
  71. >
  72. <a-textarea v-model:value="msgState.msgPayload" > </a-textarea>
  73. </a-form-item>
  74. </a-form>
  75. <template v-else >
  76. <a-alert message="同步命令成功下发后,设备需要在20秒内向平台回复响应,否则会认为命令请求超时。" type="info" show-icon style="margin-bottom: 20px;" />
  77. <a-form
  78. :labelCol="{span: 4}"
  79. :wrapperCol="{span: 14}"
  80. >
  81. <a-form-item
  82. label="命令名称"
  83. v-bind="validateInfosCmd.cmdId"
  84. >
  85. <a-select v-model:value="cmdState.cmdId" >
  86. <a-select-option
  87. v-for="item in state.cmdList"
  88. :key="item.id"
  89. :value="item.id"
  90. >
  91. {{item.cmdLabel}}
  92. </a-select-option>
  93. </a-select>
  94. </a-form-item>
  95. <a-form-item
  96. label="设置命令参数"
  97. v-bind="validateInfos.validateInfosCmd"
  98. v-if="cmdDetail?.cmdParams"
  99. >
  100. <a-row>
  101. <a-col v-for="(item, index) in cmdDetail?.cmdParams" :key="index" style="margin: 10px 0px;" >
  102. <a-input-group size="large" >
  103. <a-row :gutter="8" align="middle" >
  104. <a-col :span="8">
  105. <a-input placeholder="key" v-model:value="item.paramLabel" disabled />
  106. </a-col>
  107. <a-col :span="8">
  108. <a-input placeholder="value" v-model:value="item.dataUnit" />
  109. </a-col>
  110. </a-row>
  111. </a-input-group>
  112. </a-col>
  113. </a-row>
  114. </a-form-item>
  115. <a-form-item label="设置命令参数" >
  116. 产品尚未配置命令,请先去 <a @click="pushProductDetail" >产品详情</a> 定义命令。
  117. </a-form-item>
  118. </a-form>
  119. </template>
  120. </modal-pro>
  121. <modal-pro
  122. title="下发消息"
  123. layout="vertical"
  124. :visible="state.detailVisible"
  125. @cancel="state.detailVisible = false"
  126. >
  127. <a-descriptions
  128. :column="1"
  129. :labelStyle="{width: '100px', textAlign: 'left', display: 'block'}"
  130. v-if="state.tasActive === 'msg'"
  131. >
  132. <a-descriptions-item :span="24" label="状态">{{ DeviceContriller.deviceMag.get(state.msgDetail!.status! )?.name }}</a-descriptions-item>
  133. <a-descriptions-item :span="24" label="消息 ID">{{state.msgDetail.msgId}}</a-descriptions-item>
  134. <a-descriptions-item :span="24" label="消息名称">{{state.msgDetail.msgLabel}}</a-descriptions-item>
  135. <a-descriptions-item :span="24" label="Topic">{{state.msgDetail.topic}}</a-descriptions-item>
  136. <a-descriptions-item :span="24" label="消息创建时间">
  137. {{dayjs(state.msgDetail.createAt).format('YYYY-MM-DD HH:mm:ss')}}
  138. </a-descriptions-item>
  139. </a-descriptions>
  140. <a-descriptions
  141. :column="1"
  142. :labelStyle="{width: '100px', textAlign: 'left', display: 'block'}"
  143. v-else
  144. >
  145. <a-descriptions-item :span="24" label="状态">{{ DeviceContriller.deviceMag.get(state.cmdDetail!.status! )?.name }}</a-descriptions-item>
  146. <a-descriptions-item :span="24" label="命令名称">{{state.cmdDetail.cmdLabel}}</a-descriptions-item>
  147. <a-descriptions-item :span="24" label="参数">{{state.cmdDetail.cmdPayload}}</a-descriptions-item>
  148. <!-- <a-descriptions-item :span="24" label="下发参数">{{state.cmdDetail.msgLabel}}</a-descriptions-item> -->
  149. <a-descriptions-item :span="24" label="消息创建时间">
  150. {{dayjs(state.cmdDetail.createAt).format('YYYY-MM-DD HH:mm:ss')}}
  151. </a-descriptions-item>
  152. </a-descriptions>
  153. </modal-pro>
  154. </template>
  155. <script lang="ts" setup >
  156. import { DeviceContriller, ModelCmdController } from '@/controller'
  157. import { computed, onMounted, reactive, ref } from 'vue'
  158. import { useRoute, useRouter } from 'vue-router'
  159. import { Form, message } from 'ant-design-vue'
  160. import dayjs from 'dayjs'
  161. const msg = '消息下发不依赖产品模型,平台会以异步方式(消息下发后无需等待设备侧回复响应)下发消息给设备。当前仅MQTT设备支持消息下发。'
  162. const cmdMsg = '如果设备所属产品定义了命令功能,则您可以通过应用调用平台接口或者操作下面的“下发命令”按钮下发命令。当前MQTT设备仅支持同步命令下发,NB设备仅支持异步命令下发 。'
  163. const modalTitle = '新增下发消息'
  164. const tabListNoTitle = [
  165. {
  166. key: 'msg',
  167. tab: '消息下发'
  168. },
  169. {
  170. key: 'cmd',
  171. tab: '命令下发'
  172. }
  173. ]
  174. const columns = [
  175. {
  176. title: '状态',
  177. dataIndex: 'status',
  178. key: 'status'
  179. },
  180. {
  181. title: '消息名称',
  182. dataIndex: 'msgLabel'
  183. },
  184. {
  185. title: '消息id',
  186. dataIndex: 'msgId'
  187. },
  188. {
  189. title: '消息内容',
  190. dataIndex: 'msgPayload'
  191. },
  192. {
  193. title: '消息创建时间',
  194. dataIndex: 'createAt',
  195. key: 'createAt'
  196. },
  197. {
  198. title: '操作',
  199. key: 'action'
  200. }
  201. ]
  202. const cmdColumns = [
  203. {
  204. title: '状态',
  205. dataIndex: 'status',
  206. key: 'status'
  207. },
  208. {
  209. title: '消息名称',
  210. dataIndex: 'cmdLabel'
  211. },
  212. {
  213. title: '消息id',
  214. dataIndex: 'msgId'
  215. },
  216. {
  217. title: '消息内容',
  218. dataIndex: 'cmdPayload'
  219. },
  220. {
  221. title: '消息创建时间',
  222. dataIndex: 'createAt',
  223. key: 'createAt'
  224. },
  225. {
  226. title: '操作',
  227. key: 'action'
  228. }
  229. ]
  230. const useForm = Form.useForm
  231. const route = useRoute()
  232. const router = useRouter()
  233. const deviceId = route.query.id as string
  234. const tipMessage = computed(() => state.tasActive === 'msg' ? msg : cmdMsg)
  235. const cmdDetail = computed(() => state.cmdList.find(item => item.id === cmdState.cmdId))
  236. const cmdParametersTrans = computed(() => {
  237. return Object.keys(cmdState.cmdParameters).map(key => {
  238. return { key: key, value: cmdState.cmdParameters[key] }
  239. })
  240. })
  241. const state = reactive<{
  242. tasActive: 'msg' | 'cmd',
  243. msgDataSource: (IOT.API.DEVICE.Msg | IOT.API.DEVICE.Cmd)[],
  244. visible: boolean,
  245. loading: boolean,
  246. detailVisible: boolean,
  247. msgDetail: Partial<IOT.API.DEVICE.Msg >
  248. cmdDetail: Partial<IOT.API.DEVICE.Cmd>
  249. cmdList: IOT.API.CMD.Cmd[],
  250. deviceDetail: IOT.API.DEVICE.Device | null
  251. }>({
  252. tasActive: 'msg',
  253. msgDataSource: [],
  254. visible: false,
  255. loading: false,
  256. detailVisible: false,
  257. msgDetail: {},
  258. cmdDetail: {},
  259. cmdList: [],
  260. deviceDetail: null
  261. })
  262. const msgState = reactive({
  263. deviceId: deviceId,
  264. msgPayload: '',
  265. msgLabel: '',
  266. topic: ''
  267. })
  268. const cmdState = reactive<{
  269. cmdId: string
  270. deviceId: string,
  271. cmdLabel: string,
  272. cmdParameters: {key: string, value: string}[]
  273. }>({
  274. cmdId: '',
  275. deviceId: deviceId,
  276. cmdLabel: '',
  277. cmdParameters: []
  278. })
  279. const { resetFields, validate, validateInfos } = useForm(msgState, reactive({
  280. msgPayload: [{ required: true, message: '请填写消息内容' }],
  281. msgLabel: [{ required: true, message: '请填写消息标题' }]
  282. }))
  283. const { resetFields: resetFieldsCmd, validate: validateCmd, validateInfos: validateInfosCmd } = useForm(cmdState, reactive({
  284. cmdId: [{ required: true, message: '请填写命令名称' }]
  285. }))
  286. const getModelCmdList = async () => {
  287. const { data } = await ModelCmdController.list({ modelId: state.deviceDetail!.modelId })
  288. state.cmdList = data
  289. }
  290. const pushProductDetail = () => {
  291. router.push({ path: '/product/detail', query: { id: state.deviceDetail!.modelId } })
  292. }
  293. const ok = () => {
  294. if (state.tasActive === 'msg') {
  295. validate().then(async () => {
  296. await DeviceContriller.addDeviceMsg(msgState)
  297. state.visible = false
  298. getDeviceMsgList()
  299. })
  300. } else {
  301. validateCmd().then(async () => {
  302. const _cmdParameters: Record<string, string> = {}
  303. cmdDetail.value?.cmdParams.forEach(item => {
  304. _cmdParameters[item.paramLabel] = item.dataUnit
  305. })
  306. const $params = {
  307. ...cmdState,
  308. cmdLabel: cmdDetail.value!.cmdLabel,
  309. cmdParameters: _cmdParameters
  310. }
  311. await DeviceContriller.addDeviceCmd($params)
  312. state.visible = false
  313. getDeviceMsgList()
  314. })
  315. }
  316. }
  317. const openDetailModal = (record: IOT.API.DEVICE.Msg) => {
  318. const key = state.tasActive === 'cmd' ? 'cmdDetail' : 'msgDetail'
  319. state.detailVisible = true
  320. state[key] = record
  321. }
  322. const getDeviceMsgList = async () => {
  323. state.loading = true
  324. state.msgDataSource = state.tasActive === 'msg' ? await DeviceContriller.listDeviceMsg({ deviceId }) : await DeviceContriller.listDeviceCmd({ deviceId })
  325. state.loading = false
  326. }
  327. const getDeviceById = async () => {
  328. state.deviceDetail = await DeviceContriller.byId(deviceId)
  329. getModelCmdList()
  330. }
  331. const onTabChange = (value: 'msg' | 'cmd') => {
  332. state.tasActive = value
  333. getDeviceMsgList()
  334. }
  335. onMounted(() => {
  336. getDeviceMsgList()
  337. getDeviceById()
  338. })
  339. </script>
  340. <style lang="less" scoped >
  341. @import '~@/styles/theme.less';
  342. .subtitle {
  343. color: @sublabel-color;
  344. margin: 20px 0px;
  345. }
  346. .msg-title {
  347. .title {
  348. font-size: 20px;
  349. }
  350. .subtitle {
  351. color: @sublabel-color;
  352. font-size: 14px;
  353. }
  354. }
  355. </style>