|
|
@@ -0,0 +1,339 @@
|
|
|
+<style scoped lang="scss">
|
|
|
+.system-container {
|
|
|
+ :deep(.el-card__body) {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: space-between;
|
|
|
+ flex: 1;
|
|
|
+ overflow: auto;
|
|
|
+
|
|
|
+ .el-table {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.page-content {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.page-pager {
|
|
|
+ background-color: var(--el-color-white);
|
|
|
+ height: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.qrcode-img {
|
|
|
+ width: 260px;
|
|
|
+ height: 260px;
|
|
|
+ margin: 20px auto;
|
|
|
+ display: block;
|
|
|
+ border: 1px solid #eee;
|
|
|
+}
|
|
|
+
|
|
|
+.qrcode-tip {
|
|
|
+ text-align: center;
|
|
|
+ color: #999;
|
|
|
+ font-size: 13px;
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.fault-tag {
|
|
|
+ margin-right: 8px;
|
|
|
+}
|
|
|
+</style>
|
|
|
+<template>
|
|
|
+ <div class="system-container layout-padding">
|
|
|
+ <el-card shadow="hover" class="layout-padding-auto">
|
|
|
+ <el-tabs v-model="state.activeTab" @tab-click="handleTabClick">
|
|
|
+ <!-- ==================== 订阅管理 ==================== -->
|
|
|
+ <el-tab-pane label="订阅管理" name="subscriber">
|
|
|
+ <el-form :model="state.formQuery" ref="queryRef" size="default" label-width="0px" class="mt5 mb5">
|
|
|
+ <ext-select
|
|
|
+ v-model="state.formQuery.stationId"
|
|
|
+ placeholder="请选择站点"
|
|
|
+ url="washStation/list"
|
|
|
+ url-method="post"
|
|
|
+ label-key="stationName"
|
|
|
+ value-key="stationId"
|
|
|
+ data-key="list"
|
|
|
+ clearable
|
|
|
+ class="wd200"
|
|
|
+ @change="loadSubscribers(true)"/>
|
|
|
+ <el-button class="ml10" plain size="default" type="success" @click="loadSubscribers(true)">
|
|
|
+ <SvgIcon name="ele-Search"/>
|
|
|
+ 查询
|
|
|
+ </el-button>
|
|
|
+ <el-button v-if="state.formQuery.stationId" class="ml10" plain size="default" type="primary" @click="handleGenerateQrcode">
|
|
|
+ <SvgIcon name="ele-Picture"/>
|
|
|
+ 生成绑定二维码
|
|
|
+ </el-button>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <el-table
|
|
|
+ border stripe
|
|
|
+ :height="state.tableHeight"
|
|
|
+ :data="state.subscriberData"
|
|
|
+ v-loading="state.subscriberLoading">
|
|
|
+ <template #empty>
|
|
|
+ <el-empty description="请选择站点后查询"/>
|
|
|
+ </template>
|
|
|
+ <el-table-column label="OpenID" prop="openid" width="260" show-overflow-tooltip/>
|
|
|
+ <el-table-column label="昵称" prop="nickname" width="150"/>
|
|
|
+ <el-table-column label="站点" prop="stationId" width="120"/>
|
|
|
+ <el-table-column label="绑定时间" prop="subscribeTime" width="170">
|
|
|
+ <template #default="{row}">{{ u.fmt.fmtDateTime(row.subscribeTime) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="状态" prop="status" width="90">
|
|
|
+ <template #default="{row}">
|
|
|
+ <el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
|
|
+ {{ row.status === 1 ? '已订阅' : '已解绑' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="100" align="center" fixed="right">
|
|
|
+ <template #default="{row}">
|
|
|
+ <el-button v-if="row.status === 1" type="danger" text size="small" @click="handleUnsubscribe(row)">解绑</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- ==================== 故障记录 ==================== -->
|
|
|
+ <el-tab-pane label="故障记录" name="faultRecord">
|
|
|
+ <el-form :model="state.faultQuery" size="default" label-width="0px" class="mt5 mb5">
|
|
|
+ <ext-select
|
|
|
+ v-model="state.faultQuery.stationId"
|
|
|
+ placeholder="站点(可选)"
|
|
|
+ url="washStation/list"
|
|
|
+ url-method="post"
|
|
|
+ label-key="stationName"
|
|
|
+ value-key="stationId"
|
|
|
+ data-key="list"
|
|
|
+ clearable
|
|
|
+ class="wd200"/>
|
|
|
+ <el-select v-model="state.faultQuery.faultType" placeholder="故障类型" clearable class="wd150 ml10">
|
|
|
+ <el-option label="设备离线" value="offline"/>
|
|
|
+ <el-option label="缺水" value="water_shortage"/>
|
|
|
+ <el-option label="缺泡沫" value="foam_shortage"/>
|
|
|
+ </el-select>
|
|
|
+ <el-select v-model="state.faultQuery.isRecovered" placeholder="恢复状态" clearable class="wd150 ml10">
|
|
|
+ <el-option label="未恢复" :value="0"/>
|
|
|
+ <el-option label="已恢复" :value="1"/>
|
|
|
+ </el-select>
|
|
|
+ <el-button class="ml10" plain size="default" type="success" @click="loadFaultRecords(true)">
|
|
|
+ <SvgIcon name="ele-Search"/>
|
|
|
+ 查询
|
|
|
+ </el-button>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <el-table
|
|
|
+ border stripe
|
|
|
+ :height="state.tableHeight"
|
|
|
+ :data="state.faultRecordData"
|
|
|
+ v-loading="state.faultRecordLoading">
|
|
|
+ <template #empty>
|
|
|
+ <el-empty description="暂无故障记录"/>
|
|
|
+ </template>
|
|
|
+ <el-table-column label="站点" prop="stationId" width="100"/>
|
|
|
+ <el-table-column label="设备" prop="deviceName" width="150"/>
|
|
|
+ <el-table-column label="故障类型" prop="faultType" width="110">
|
|
|
+ <template #default="{row}">
|
|
|
+ <el-tag class="fault-tag"
|
|
|
+ :type="row.faultType === 'offline' ? 'danger' : 'warning'"
|
|
|
+ size="small">
|
|
|
+ {{ faultTypeLabel(row.faultType) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="故障时间" prop="faultTime" width="170">
|
|
|
+ <template #default="{row}">{{ u.fmt.fmtDateTime(row.faultTime) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="是否通知" prop="isNotified" width="90">
|
|
|
+ <template #default="{row}">
|
|
|
+ <el-tag :type="row.isNotified === 1 ? 'success' : 'warning'" size="small">
|
|
|
+ {{ row.isNotified === 1 ? '已通知' : '未通知' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="通知时间" prop="notifyTime" width="170">
|
|
|
+ <template #default="{row}">{{ row.notifyTime ? u.fmt.fmtDateTime(row.notifyTime) : '-' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="是否恢复" prop="isRecovered" width="90">
|
|
|
+ <template #default="{row}">
|
|
|
+ <el-tag :type="row.isRecovered === 1 ? 'success' : 'danger'" size="small">
|
|
|
+ {{ row.isRecovered === 1 ? '已恢复' : '未恢复' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="恢复时间" prop="recoverTime" width="170">
|
|
|
+ <template #default="{row}">{{ row.recoverTime ? u.fmt.fmtDateTime(row.recoverTime) : '-' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <ext-page class="page-pager" v-model:value="state.faultPageQuery" @change="loadFaultRecords(false)"/>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 二维码弹窗 -->
|
|
|
+ <el-dialog v-model="state.qrcodeDialogVisible" title="故障通知绑定二维码" width="420px" center>
|
|
|
+ <div style="text-align: center">
|
|
|
+ <img v-if="state.qrcodeUrl" :src="state.qrcodeUrl" class="qrcode-img" alt="绑定二维码"/>
|
|
|
+ <div class="qrcode-tip">
|
|
|
+ 请使用微信扫描二维码<br/>
|
|
|
+ 扫描后关注公众号即可绑定,再次扫描可解绑<br/>
|
|
|
+ 站点:{{ state.formQuery.stationId }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts" name="adminStationFault">
|
|
|
+import {reactive, onMounted, onBeforeMount, ref, nextTick, onBeforeUnmount} from 'vue';
|
|
|
+import {$body, $get} from "/@/utils/request";
|
|
|
+import u from '/@/utils/u'
|
|
|
+import {Msg} from "/@/utils/message";
|
|
|
+
|
|
|
+import ExtPage from '/@/components/form/ExtPage.vue'
|
|
|
+import ExtSelect from "/@/components/form/ExtSelect.vue";
|
|
|
+import mittBus from '/@/utils/mitt';
|
|
|
+
|
|
|
+
|
|
|
+const state = reactive({
|
|
|
+ activeTab: 'subscriber',
|
|
|
+ tableHeight: 400,
|
|
|
+ formQuery: {
|
|
|
+ stationId: '' as string
|
|
|
+ },
|
|
|
+ subscriberData: [] as Array<any>,
|
|
|
+ subscriberLoading: false,
|
|
|
+
|
|
|
+ faultQuery: {
|
|
|
+ stationId: '' as string,
|
|
|
+ faultType: '' as string,
|
|
|
+ isRecovered: null as number | null
|
|
|
+ },
|
|
|
+ faultRecordData: [] as Array<any>,
|
|
|
+ faultRecordLoading: false,
|
|
|
+ faultPageQuery: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ total: 0
|
|
|
+ },
|
|
|
+
|
|
|
+ qrcodeDialogVisible: false,
|
|
|
+ qrcodeUrl: '',
|
|
|
+ qrcodeTicket: ''
|
|
|
+});
|
|
|
+
|
|
|
+const faultTypeLabel = (type: string) => {
|
|
|
+ switch (type) {
|
|
|
+ case 'offline': return '设备离线';
|
|
|
+ case 'water_shortage': return '缺水';
|
|
|
+ case 'foam_shortage': return '缺泡沫';
|
|
|
+ default: return type;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handleTabClick = () => {
|
|
|
+ if (state.activeTab === 'faultRecord') {
|
|
|
+ loadFaultRecords(true);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// ============ 订阅管理 ============
|
|
|
+
|
|
|
+const loadSubscribers = (refresh: boolean = false) => {
|
|
|
+ if (!state.formQuery.stationId) {
|
|
|
+ state.subscriberData = [];
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ state.subscriberLoading = true;
|
|
|
+ $get('/faultSubscriber/list', { stationId: state.formQuery.stationId }).then((res: any) => {
|
|
|
+ state.subscriberData = res.data || res || [];
|
|
|
+ state.subscriberLoading = false;
|
|
|
+ }).catch(() => {
|
|
|
+ state.subscriberLoading = false;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const handleGenerateQrcode = () => {
|
|
|
+ if (!state.formQuery.stationId) {
|
|
|
+ Msg.message('请先选择站点', 'warning');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Msg.showLoading('生成中...');
|
|
|
+ $body('/faultSubscriber/generateQrcode', { stationId: state.formQuery.stationId }).then((res: any) => {
|
|
|
+ Msg.hideLoading();
|
|
|
+ const data = res.data || res;
|
|
|
+ state.qrcodeUrl = data.url;
|
|
|
+ state.qrcodeTicket = data.ticket;
|
|
|
+ state.qrcodeDialogVisible = true;
|
|
|
+ }).catch(() => {
|
|
|
+ Msg.hideLoading();
|
|
|
+ Msg.message('生成二维码失败', 'error');
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const handleUnsubscribe = (row: any) => {
|
|
|
+ Msg.confirm(`确定要解绑 ${row.openid} 吗?解绑后该用户将不再接收故障通知。`).then(() => {
|
|
|
+ $body('/faultSubscriber/unsubscribe', { openid: row.openid, stationId: row.stationId }).then(() => {
|
|
|
+ Msg.message('解绑成功', 'success');
|
|
|
+ loadSubscribers(false);
|
|
|
+ }).catch(() => {
|
|
|
+ Msg.message('解绑失败', 'error');
|
|
|
+ });
|
|
|
+ }).catch(() => {});
|
|
|
+};
|
|
|
+
|
|
|
+// ============ 故障记录 ============
|
|
|
+
|
|
|
+const loadFaultRecords = (refresh: boolean = false) => {
|
|
|
+ if (refresh) {
|
|
|
+ state.faultPageQuery.pageNum = 1;
|
|
|
+ }
|
|
|
+ state.faultRecordLoading = true;
|
|
|
+ const params: any = { ...state.faultPageQuery };
|
|
|
+ if (state.faultQuery.stationId) params.stationId = state.faultQuery.stationId;
|
|
|
+ if (state.faultQuery.faultType) params.faultType = state.faultQuery.faultType;
|
|
|
+ if (state.faultQuery.isRecovered !== null && state.faultQuery.isRecovered !== undefined) {
|
|
|
+ params.isRecovered = state.faultQuery.isRecovered;
|
|
|
+ }
|
|
|
+ $get('/faultSubscriber/faultRecords', params).then((res: any) => {
|
|
|
+ const list = res.data || res || [];
|
|
|
+ if (Array.isArray(list)) {
|
|
|
+ state.faultRecordData = list;
|
|
|
+ state.faultPageQuery.total = list.length;
|
|
|
+ } else if (list.list) {
|
|
|
+ state.faultRecordData = list.list;
|
|
|
+ state.faultPageQuery.total = list.total || 0;
|
|
|
+ } else {
|
|
|
+ state.faultRecordData = list;
|
|
|
+ }
|
|
|
+ state.faultRecordLoading = false;
|
|
|
+ }).catch(() => {
|
|
|
+ state.faultRecordLoading = false;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// ============ 生命周期 ============
|
|
|
+
|
|
|
+onBeforeMount(() => {
|
|
|
+});
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ nextTick(() => {
|
|
|
+ let bodyHeight = document.body.clientHeight;
|
|
|
+ state.tableHeight = bodyHeight - 280;
|
|
|
+ });
|
|
|
+
|
|
|
+ mittBus.on("faultSubscriber.refresh", () => {
|
|
|
+ loadSubscribers(false);
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ mittBus.off("faultSubscriber.refresh");
|
|
|
+});
|
|
|
+</script>
|