index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <style scoped lang="scss">
  2. .system-container {
  3. :deep(.el-card__body) {
  4. display: flex;
  5. flex-direction: column;
  6. justify-content: space-between;
  7. flex: 1;
  8. overflow: auto;
  9. .el-table {
  10. flex: 1;
  11. }
  12. }
  13. }
  14. .page-content {
  15. margin-bottom: 20px;
  16. }
  17. .page-pager {
  18. background-color: var(--el-color-white);
  19. height: 24px;
  20. }
  21. .qrcode-img {
  22. width: 260px;
  23. height: 260px;
  24. margin: 20px auto;
  25. display: block;
  26. border: 1px solid #eee;
  27. }
  28. .qrcode-tip {
  29. text-align: center;
  30. color: #999;
  31. font-size: 13px;
  32. margin-top: 10px;
  33. }
  34. .fault-tag {
  35. margin-right: 8px;
  36. }
  37. </style>
  38. <template>
  39. <div class="system-container layout-padding">
  40. <el-card shadow="hover" class="layout-padding-auto">
  41. <el-tabs v-model="state.activeTab" @tab-click="handleTabClick">
  42. <!-- ==================== 订阅管理 ==================== -->
  43. <el-tab-pane label="订阅管理" name="subscriber">
  44. <el-form :model="state.formQuery" ref="queryRef" size="default" label-width="0px" class="mt5 mb5">
  45. <ext-select
  46. v-model="state.formQuery.stationId"
  47. placeholder="请选择站点"
  48. url="washStation/list"
  49. url-method="post"
  50. label-key="stationName"
  51. value-key="stationId"
  52. data-key="list"
  53. clearable
  54. class="wd200"
  55. @change="loadSubscribers(true)"/>
  56. <el-button class="ml10" plain size="default" type="success" @click="loadSubscribers(true)">
  57. <SvgIcon name="ele-Search"/>
  58. 查询
  59. </el-button>
  60. <el-button v-if="state.formQuery.stationId" class="ml10" plain size="default" type="primary" @click="handleGenerateQrcode">
  61. <SvgIcon name="ele-Picture"/>
  62. 生成绑定二维码
  63. </el-button>
  64. </el-form>
  65. <el-table
  66. border stripe
  67. :height="state.tableHeight"
  68. :data="state.subscriberData"
  69. v-loading="state.subscriberLoading">
  70. <template #empty>
  71. <el-empty description="请选择站点后查询"/>
  72. </template>
  73. <el-table-column label="OpenID" prop="openid" width="260" show-overflow-tooltip/>
  74. <el-table-column label="昵称" prop="nickname" width="150"/>
  75. <el-table-column label="站点" prop="stationId" width="120"/>
  76. <el-table-column label="绑定时间" prop="subscribeTime" width="170">
  77. <template #default="{row}">{{ u.fmt.fmtDateTime(row.subscribeTime) }}</template>
  78. </el-table-column>
  79. <el-table-column label="状态" prop="status" width="90">
  80. <template #default="{row}">
  81. <el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
  82. {{ row.status === 1 ? '已订阅' : '已解绑' }}
  83. </el-tag>
  84. </template>
  85. </el-table-column>
  86. <el-table-column label="操作" width="100" align="center" fixed="right">
  87. <template #default="{row}">
  88. <el-button v-if="row.status === 1" type="danger" text size="small" @click="handleUnsubscribe(row)">解绑</el-button>
  89. </template>
  90. </el-table-column>
  91. </el-table>
  92. </el-tab-pane>
  93. <!-- ==================== 故障记录 ==================== -->
  94. <el-tab-pane label="故障记录" name="faultRecord">
  95. <el-form :model="state.faultQuery" size="default" label-width="0px" class="mt5 mb5">
  96. <ext-select
  97. v-model="state.faultQuery.stationId"
  98. placeholder="站点(可选)"
  99. url="washStation/list"
  100. url-method="post"
  101. label-key="stationName"
  102. value-key="stationId"
  103. data-key="list"
  104. clearable
  105. class="wd200"/>
  106. <el-select v-model="state.faultQuery.faultType" placeholder="故障类型" clearable class="wd150 ml10">
  107. <el-option label="设备离线" value="offline"/>
  108. <el-option label="缺水" value="water_shortage"/>
  109. <el-option label="缺泡沫" value="foam_shortage"/>
  110. </el-select>
  111. <el-select v-model="state.faultQuery.isRecovered" placeholder="恢复状态" clearable class="wd150 ml10">
  112. <el-option label="未恢复" :value="0"/>
  113. <el-option label="已恢复" :value="1"/>
  114. </el-select>
  115. <el-button class="ml10" plain size="default" type="success" @click="loadFaultRecords(true)">
  116. <SvgIcon name="ele-Search"/>
  117. 查询
  118. </el-button>
  119. </el-form>
  120. <el-table
  121. border stripe
  122. :height="state.tableHeight"
  123. :data="state.faultRecordData"
  124. v-loading="state.faultRecordLoading">
  125. <template #empty>
  126. <el-empty description="暂无故障记录"/>
  127. </template>
  128. <el-table-column label="站点" prop="stationId" width="100"/>
  129. <el-table-column label="设备" prop="deviceName" width="150"/>
  130. <el-table-column label="故障类型" prop="faultType" width="110">
  131. <template #default="{row}">
  132. <el-tag class="fault-tag"
  133. :type="row.faultType === 'offline' ? 'danger' : 'warning'"
  134. size="small">
  135. {{ faultTypeLabel(row.faultType) }}
  136. </el-tag>
  137. </template>
  138. </el-table-column>
  139. <el-table-column label="故障时间" prop="faultTime" width="170">
  140. <template #default="{row}">{{ u.fmt.fmtDateTime(row.faultTime) }}</template>
  141. </el-table-column>
  142. <el-table-column label="是否通知" prop="isNotified" width="90">
  143. <template #default="{row}">
  144. <el-tag :type="row.isNotified === 1 ? 'success' : 'warning'" size="small">
  145. {{ row.isNotified === 1 ? '已通知' : '未通知' }}
  146. </el-tag>
  147. </template>
  148. </el-table-column>
  149. <el-table-column label="通知时间" prop="notifyTime" width="170">
  150. <template #default="{row}">{{ row.notifyTime ? u.fmt.fmtDateTime(row.notifyTime) : '-' }}</template>
  151. </el-table-column>
  152. <el-table-column label="是否恢复" prop="isRecovered" width="90">
  153. <template #default="{row}">
  154. <el-tag :type="row.isRecovered === 1 ? 'success' : 'danger'" size="small">
  155. {{ row.isRecovered === 1 ? '已恢复' : '未恢复' }}
  156. </el-tag>
  157. </template>
  158. </el-table-column>
  159. <el-table-column label="恢复时间" prop="recoverTime" width="170">
  160. <template #default="{row}">{{ row.recoverTime ? u.fmt.fmtDateTime(row.recoverTime) : '-' }}</template>
  161. </el-table-column>
  162. </el-table>
  163. <ext-page class="page-pager" v-model:value="state.faultPageQuery" @change="loadFaultRecords(false)"/>
  164. </el-tab-pane>
  165. </el-tabs>
  166. </el-card>
  167. <!-- 二维码弹窗 -->
  168. <el-dialog v-model="state.qrcodeDialogVisible" title="故障通知绑定二维码" width="420px" center>
  169. <div style="text-align: center">
  170. <img v-if="state.qrcodeUrl" :src="state.qrcodeUrl" class="qrcode-img" alt="绑定二维码"/>
  171. <div class="qrcode-tip">
  172. 请使用微信扫描二维码<br/>
  173. 扫描后关注公众号即可绑定,再次扫描可解绑<br/>
  174. 站点:{{ state.formQuery.stationId }}
  175. </div>
  176. </div>
  177. </el-dialog>
  178. </div>
  179. </template>
  180. <script setup lang="ts" name="adminStationFault">
  181. import {reactive, onMounted, onBeforeMount, ref, nextTick, onBeforeUnmount} from 'vue';
  182. import {$body, $get} from "/@/utils/request";
  183. import u from '/@/utils/u'
  184. import {Msg} from "/@/utils/message";
  185. import ExtPage from '/@/components/form/ExtPage.vue'
  186. import ExtSelect from "/@/components/form/ExtSelect.vue";
  187. import mittBus from '/@/utils/mitt';
  188. const state = reactive({
  189. activeTab: 'subscriber',
  190. tableHeight: 400,
  191. formQuery: {
  192. stationId: '' as string
  193. },
  194. subscriberData: [] as Array<any>,
  195. subscriberLoading: false,
  196. faultQuery: {
  197. stationId: '' as string,
  198. faultType: '' as string,
  199. isRecovered: null as number | null
  200. },
  201. faultRecordData: [] as Array<any>,
  202. faultRecordLoading: false,
  203. faultPageQuery: {
  204. pageNum: 1,
  205. pageSize: 10,
  206. total: 0
  207. },
  208. qrcodeDialogVisible: false,
  209. qrcodeUrl: '',
  210. qrcodeTicket: ''
  211. });
  212. const faultTypeLabel = (type: string) => {
  213. switch (type) {
  214. case 'offline': return '设备离线';
  215. case 'water_shortage': return '缺水';
  216. case 'foam_shortage': return '缺泡沫';
  217. default: return type;
  218. }
  219. };
  220. const handleTabClick = () => {
  221. if (state.activeTab === 'faultRecord') {
  222. loadFaultRecords(true);
  223. }
  224. };
  225. // ============ 订阅管理 ============
  226. const loadSubscribers = (refresh: boolean = false) => {
  227. if (!state.formQuery.stationId) {
  228. state.subscriberData = [];
  229. return;
  230. }
  231. state.subscriberLoading = true;
  232. $get('/faultSubscriber/list', { stationId: state.formQuery.stationId }).then((res: any) => {
  233. state.subscriberData = res.data || res || [];
  234. state.subscriberLoading = false;
  235. }).catch(() => {
  236. state.subscriberLoading = false;
  237. });
  238. };
  239. const handleGenerateQrcode = () => {
  240. if (!state.formQuery.stationId) {
  241. Msg.message('请先选择站点', 'warning');
  242. return;
  243. }
  244. Msg.showLoading('生成中...');
  245. $body('/faultSubscriber/generateQrcode', { stationId: state.formQuery.stationId }).then((res: any) => {
  246. Msg.hideLoading();
  247. const data = res.data || res;
  248. state.qrcodeUrl = data.url;
  249. state.qrcodeTicket = data.ticket;
  250. state.qrcodeDialogVisible = true;
  251. }).catch(() => {
  252. Msg.hideLoading();
  253. Msg.message('生成二维码失败', 'error');
  254. });
  255. };
  256. const handleUnsubscribe = (row: any) => {
  257. Msg.confirm(`确定要解绑 ${row.openid} 吗?解绑后该用户将不再接收故障通知。`).then(() => {
  258. $body('/faultSubscriber/unsubscribe', { openid: row.openid, stationId: row.stationId }).then(() => {
  259. Msg.message('解绑成功', 'success');
  260. loadSubscribers(false);
  261. }).catch(() => {
  262. Msg.message('解绑失败', 'error');
  263. });
  264. }).catch(() => {});
  265. };
  266. // ============ 故障记录 ============
  267. const loadFaultRecords = (refresh: boolean = false) => {
  268. if (refresh) {
  269. state.faultPageQuery.pageNum = 1;
  270. }
  271. state.faultRecordLoading = true;
  272. const params: any = { ...state.faultPageQuery };
  273. if (state.faultQuery.stationId) params.stationId = state.faultQuery.stationId;
  274. if (state.faultQuery.faultType) params.faultType = state.faultQuery.faultType;
  275. if (state.faultQuery.isRecovered !== null && state.faultQuery.isRecovered !== undefined) {
  276. params.isRecovered = state.faultQuery.isRecovered;
  277. }
  278. $get('/faultSubscriber/faultRecords', params).then((res: any) => {
  279. const list = res.data || res || [];
  280. if (Array.isArray(list)) {
  281. state.faultRecordData = list;
  282. state.faultPageQuery.total = list.length;
  283. } else if (list.list) {
  284. state.faultRecordData = list.list;
  285. state.faultPageQuery.total = list.total || 0;
  286. } else {
  287. state.faultRecordData = list;
  288. }
  289. state.faultRecordLoading = false;
  290. }).catch(() => {
  291. state.faultRecordLoading = false;
  292. });
  293. };
  294. // ============ 生命周期 ============
  295. onBeforeMount(() => {
  296. });
  297. onMounted(() => {
  298. nextTick(() => {
  299. let bodyHeight = document.body.clientHeight;
  300. state.tableHeight = bodyHeight - 280;
  301. });
  302. mittBus.on("faultSubscriber.refresh", () => {
  303. loadSubscribers(false);
  304. });
  305. });
  306. onBeforeUnmount(() => {
  307. mittBus.off("faultSubscriber.refresh");
  308. });
  309. </script>