fastbee/yack-web/src/views/dataCenter/reportHistory.vue
2026-09-22 09:54:51 +08:00

289 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="data-center-report-history">
<el-card>
<!-- 筛选栏 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" size="mini" label-width="80px" @submit.native.prevent>
<el-form-item label="关键字" prop="keyword">
<el-input
v-model="queryParams.keyword"
placeholder="报告编号 / 设备名称"
clearable
style="width: 220px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="周期" prop="period">
<el-select v-model="queryParams.period" placeholder="全部" clearable style="width: 130px">
<el-option v-for="item in periodOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 130px">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item style="float: right">
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 报表列表 -->
<el-table v-loading="loading" :data="reportList" style="width: 100%">
<el-table-column label="报告编号" prop="reportNo" min-width="220" show-overflow-tooltip />
<el-table-column label="设备名称" prop="deviceName" min-width="140" show-overflow-tooltip />
<el-table-column label="监测分类" min-width="110" align="center">
<template slot-scope="scope">{{ categoryText(scope.row.monitorCategoryCode) }}</template>
</el-table-column>
<el-table-column label="周期" min-width="90" align="center">
<template slot-scope="scope">{{ periodText(scope.row.period) }}</template>
</el-table-column>
<el-table-column label="时间范围" min-width="240" align="center">
<template slot-scope="scope">{{ scope.row.startTime }} ~ {{ scope.row.endTime }}</template>
</el-table-column>
<el-table-column label="生成时间" prop="createTime" width="160" align="center" />
<el-table-column label="状态" min-width="90" align="center">
<template slot-scope="scope">
<el-tag :type="statusType(scope.row.status)" size="mini">{{ statusText(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template slot-scope="scope">
<el-button
v-if="scope.row.status === 'SUCCESS'"
type="success"
icon="el-icon-download"
size="mini"
@click="handleDownload(scope.row)"
>下载</el-button>
<el-button
v-if="scope.row.status === 'FAILED'"
type="danger"
plain
icon="el-icon-warning-outline"
size="mini"
@click="handleShowError(scope.row)"
>失败原因</el-button>
<el-button
v-if="scope.row.status === 'CREATING'"
plain
icon="el-icon-refresh"
size="mini"
@click="getList()"
>刷新</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
<!-- 失败原因 -->
<el-dialog title="报告生成失败原因" :visible.sync="errorVisible" width="520px" append-to-body>
<div class="report-error-detail">
<div class="detail-item">
<span class="label">报告编号:</span>{{ errorRow.reportNo || '-' }}
</div>
<div class="detail-item">
<span class="label">设备名称:</span>{{ errorRow.deviceName || '-' }}
</div>
<div class="detail-item">
<span class="label">时间范围:</span>{{ errorRow.startTime || '-' }} ~ {{ errorRow.endTime || '-' }}
</div>
<div class="detail-item">
<span class="label">失败原因:</span>
</div>
<pre class="error-message">{{ errorRow.errorMessage || '未记录失败原因,请联系管理员查看服务端日志' }}</pre>
</div>
<span slot="footer">
<el-button size="mini" type="primary" @click="errorVisible = false">关闭</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { saveAs } from 'file-saver';
import moment from 'moment';
import { listReportRecords, downloadReportRecord } from '@/api/iot/report';
import { blobValidate } from '@/utils/ruoyi';
/** 报告保留天数,与后端 LogCleanTask#cleanLogs 默认保留天数一致 */
const REPORT_RETENTION_DAYS = 15;
export default {
name: 'ReportHistory',
data() {
return {
loading: false,
reportList: [],
total: 0,
/** 生成中记录轮询定时器 */
pollTimer: null,
errorVisible: false,
errorRow: {},
queryParams: {
pageNum: 1,
pageSize: 10,
keyword: null,
period: null,
status: null,
},
periodOptions: [
{ value: 'DAY', label: '日报' },
{ value: 'WEEK', label: '周报' },
{ value: 'HALF_MONTH', label: '半月报' },
{ value: 'MONTH', label: '月报' },
{ value: 'CUSTOM', label: '自定义' },
],
statusOptions: [
{ value: 'CREATING', label: '生成中' },
{ value: 'SUCCESS', label: '成功' },
{ value: 'FAILED', label: '失败' },
],
};
},
mounted() {
this.getList();
},
beforeDestroy() {
this.stopPolling();
},
methods: {
/** silent=true 时不展示 loading,用于轮询刷新 */
getList(silent) {
if (!silent) {
this.loading = true;
}
listReportRecords(this.queryParams)
.then((res) => {
this.reportList = res.rows || [];
this.total = res.total || 0;
})
.finally(() => {
if (!silent) {
this.loading = false;
}
this.schedulePolling();
});
},
/** 存在生成中记录时每 10s 轮询,全部转为终态后自动停止 */
schedulePolling() {
this.stopPolling();
const hasCreating = this.reportList.some((item) => item.status === 'CREATING');
if (!hasCreating) {
return;
}
this.pollTimer = setTimeout(() => {
this.getList(true);
}, 10000);
},
stopPolling() {
if (this.pollTimer) {
clearTimeout(this.pollTimer);
this.pollTimer = null;
}
},
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
handleResetQuery() {
this.resetForm('queryForm');
this.handleQuery();
},
/** 报告无 expire_time 字段,按 create_time + 保留天数判断是否已过期 */
isExpired(row) {
if (!row || !row.createTime) {
return false;
}
const created = moment(row.createTime, 'YYYY-MM-DD HH:mm:ss', true);
if (!created.isValid()) {
return false;
}
return moment().diff(created, 'days') >= REPORT_RETENTION_DAYS;
},
handleDownload(row) {
if (this.isExpired(row)) {
this.$message.warning('报告已过期');
return;
}
downloadReportRecord(row.reportNo)
.then(async (data) => {
const isBlob = await blobValidate(data);
if (isBlob) {
const blob = new Blob([data]);
saveAs(blob, row.fileName || row.reportNo + '.docx');
} else {
const resText = await data.text();
const rspObj = JSON.parse(resText);
this.$message.error((rspObj && rspObj.msg) || '下载失败');
}
})
.catch(() => {
this.$message.error('下载失败,请稍后重试');
});
},
handleShowError(row) {
this.errorRow = row || {};
this.errorVisible = true;
},
categoryText(code) {
const map = {
CRACK_METER: '裂缝计',
DEEP_DISPLACEMENT: '深部位移',
AXIAL_FORCE: '轴力/应力',
STATIC_LEVEL: '静力水准',
VIBRATING_WIRE: '振弦',
};
return map[code] || code || '-';
},
periodText(code) {
const item = this.periodOptions.find((i) => i.value === code);
return item ? item.label : code || '-';
},
statusText(code) {
const item = this.statusOptions.find((i) => i.value === code);
return item ? item.label : code || '-';
},
statusType(code) {
if (code === 'SUCCESS') return 'success';
if (code === 'FAILED') return 'danger';
return 'warning';
},
},
};
</script>
<style scoped>
.data-center-report-history .el-card {
min-height: calc(100vh - 110px);
}
.report-error-detail .detail-item {
margin-bottom: 6px;
font-size: 13px;
}
.report-error-detail .label {
color: #909399;
}
.report-error-detail .error-message {
margin: 0;
padding: 8px 10px;
background: #f5f7fa;
border-radius: 4px;
color: #f56c6c;
font-size: 13px;
font-family: inherit;
white-space: pre-wrap;
word-break: break-all;
max-height: 240px;
overflow: auto;
}
</style>