|
|
@@ -4,9 +4,8 @@
|
|
|
<a-tabs v-model="activeTab" @change="handleTabChange">
|
|
|
<a-tab-pane tab="按维修人统计" key="user"></a-tab-pane>
|
|
|
<a-tab-pane tab="按设备统计" key="sb"></a-tab-pane>
|
|
|
- <!-- 按月度统计功能暂不正确,先隐藏页签,逻辑代码保留待修复后恢复
|
|
|
- <a-tab-pane tab="按月度统计(设备平均维修工时)" key="month"></a-tab-pane>
|
|
|
- -->
|
|
|
+ <a-tab-pane tab="月度MTTR统计" key="mttr"></a-tab-pane>
|
|
|
+ <a-tab-pane tab="月度MTBF统计" key="mtbf"></a-tab-pane>
|
|
|
</a-tabs>
|
|
|
|
|
|
<div class="table-page-search-wrapper" @keyup.enter="handleSearch">
|
|
|
@@ -17,12 +16,13 @@
|
|
|
<a-input v-model="queryParam.repairUserName" placeholder="请输入维修人姓名" allow-clear/>
|
|
|
</a-form-item>
|
|
|
</a-col>
|
|
|
- <a-col :md="8" :sm="24" v-else>
|
|
|
+ <a-col :md="6" :sm="24" v-if="activeTab === 'sb'">
|
|
|
<a-form-item label="设备">
|
|
|
<a-input v-model="queryParam.keyword" placeholder="请输入设备名称/编号" allow-clear/>
|
|
|
</a-form-item>
|
|
|
</a-col>
|
|
|
- <a-col :md="10" :sm="24" v-if="activeTab !== 'month'">
|
|
|
+ <!-- user/sb 页签:按维修开始时间范围过滤 -->
|
|
|
+ <a-col :md="10" :sm="24" v-if="!isMonth">
|
|
|
<a-form-item label="维修开始时间">
|
|
|
<a-range-picker
|
|
|
v-model="dateRange"
|
|
|
@@ -31,15 +31,48 @@
|
|
|
:placeholder="['开始日期', '结束日期']"/>
|
|
|
</a-form-item>
|
|
|
</a-col>
|
|
|
- <a-col :md="10" :sm="24" v-else>
|
|
|
- <a-form-item label="统计月份">
|
|
|
- <a-month-picker
|
|
|
- v-model="queryParam.yearMonth"
|
|
|
- format="YYYY-MM"
|
|
|
- placeholder="选择月份"
|
|
|
- style="width: 100%"/>
|
|
|
- </a-form-item>
|
|
|
- </a-col>
|
|
|
+ <!-- month 页签:统计时间段(年月) + 设备等级(A/B/C) + 设备类别 + 设备(置底) -->
|
|
|
+ <template v-else-if="isMonth">
|
|
|
+ <a-col :md="8" :sm="24">
|
|
|
+ <a-form-item label="统计时间段">
|
|
|
+ <a-month-picker
|
|
|
+ v-model="mttrStartMonth"
|
|
|
+ format="YYYY-MM"
|
|
|
+ placeholder="开始月份"
|
|
|
+ style="width: 46%"/>
|
|
|
+ <span style="margin: 0 2%">至</span>
|
|
|
+ <a-month-picker
|
|
|
+ v-model="mttrEndMonth"
|
|
|
+ format="YYYY-MM"
|
|
|
+ placeholder="结束月份"
|
|
|
+ style="width: 46%"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ <a-col :md="5" :sm="24">
|
|
|
+ <a-form-item label="设备等级">
|
|
|
+ <a-select mode="multiple" v-model="queryParam.levelList" placeholder="全部" allowClear
|
|
|
+ :maxTagCount="1" style="width: 100%">
|
|
|
+ <a-select-option v-for="(label, value) in sbLevelMap" :key="value" :label="label"
|
|
|
+ :value="parseInt(value)">{{ label }}
|
|
|
+ </a-select-option>
|
|
|
+ </a-select>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ <a-col :md="6" :sm="24">
|
|
|
+ <a-form-item label="设备类别">
|
|
|
+ <a-tree-select style="width: 100%" :dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
|
|
|
+ :treeData="sbTypeTreeData" :treeNodeFilterProp="'title'" :showSearch="true"
|
|
|
+ v-model="queryParam.typeIdList" multiple allowClear :maxTagCount="1"
|
|
|
+ placeholder="全部">
|
|
|
+ </a-tree-select>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ <a-col :md="6" :sm="24">
|
|
|
+ <a-form-item label="设备">
|
|
|
+ <a-input v-model="queryParam.keyword" placeholder="请输入设备名称/编号" allow-clear/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ </template>
|
|
|
<a-col :md="6" :sm="24">
|
|
|
<span class="table-page-search-submitButtons">
|
|
|
<a-button type="primary" @click="handleSearch">查询</a-button>
|
|
|
@@ -54,22 +87,44 @@
|
|
|
<a-button type="primary" icon="download" @click="doExport">导出</a-button>
|
|
|
</div>
|
|
|
|
|
|
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px">
|
|
|
- <span style="font-weight: 600">{{ chartTitle }}(按平均工时降序)</span>
|
|
|
- <span>
|
|
|
- <a-radio-group v-if="activeTab === 'sb'" v-model="chartDim" size="small" style="margin-right: 8px" @change="doRenderChart">
|
|
|
- <a-radio-button value="sb">按设备</a-radio-button>
|
|
|
- <a-radio-button value="type">按设备类型</a-radio-button>
|
|
|
- </a-radio-group>
|
|
|
- <a-radio-group v-model="chartTop" size="small" @change="doRenderChart">
|
|
|
+ <!-- 月度MTTR/MTBF页签:专属图表区(已拆为两个独立页签) -->
|
|
|
+ <template v-if="isMonth">
|
|
|
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px">
|
|
|
+ <span style="font-weight: 600">{{ monthChartTitle }}</span>
|
|
|
+ <a-radio-group v-if="monthMetric === 'mtbf'" v-model="mtbfChartTop" size="small" @change="renderMonthCharts">
|
|
|
<a-radio-button value="top20">前20名</a-radio-button>
|
|
|
<a-radio-button value="all">全部</a-radio-button>
|
|
|
</a-radio-group>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <a-spin :spinning="chartLoading">
|
|
|
- <div :id="chartContainerId" style="min-height: 400px"></div>
|
|
|
- </a-spin>
|
|
|
+ </div>
|
|
|
+ <a-spin :spinning="monthLoading">
|
|
|
+ <!-- MTTR:月度趋势折线 -->
|
|
|
+ <div v-if="monthMetric === 'mttr'" id="monthTrendChartContainer" key="monthTrendChartContainer" style="min-height: 360px"></div>
|
|
|
+ <!-- MTBF:仅按设备统计柱状图(不需要月度趋势) -->
|
|
|
+ <template v-else>
|
|
|
+ <div id="monthMtbfSbChartContainer" key="monthMtbfSbChartContainer" style="min-height: 360px"></div>
|
|
|
+ </template>
|
|
|
+ </a-spin>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- user/sb 页签图表区 -->
|
|
|
+ <template v-else>
|
|
|
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px">
|
|
|
+ <span style="font-weight: 600">{{ chartTitle }}(按平均工时降序)</span>
|
|
|
+ <span>
|
|
|
+ <a-radio-group v-if="activeTab === 'sb'" v-model="chartDim" size="small" style="margin-right: 8px" @change="doRenderChart">
|
|
|
+ <a-radio-button value="sb">按设备</a-radio-button>
|
|
|
+ <a-radio-button value="type">按设备类型</a-radio-button>
|
|
|
+ </a-radio-group>
|
|
|
+ <a-radio-group v-model="chartTop" size="small" @change="doRenderChart">
|
|
|
+ <a-radio-button value="top20">前20名</a-radio-button>
|
|
|
+ <a-radio-button value="all">全部</a-radio-button>
|
|
|
+ </a-radio-group>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <a-spin :spinning="chartLoading">
|
|
|
+ <div :id="chartContainerId" key="userSbChartContainer" style="min-height: 400px"></div>
|
|
|
+ </a-spin>
|
|
|
+ </template>
|
|
|
|
|
|
<s-table
|
|
|
ref="table"
|
|
|
@@ -99,17 +154,24 @@ import { STable } from '@/components'
|
|
|
import { Chart } from '@antv/g2'
|
|
|
import moment from 'moment'
|
|
|
import DetailModal from './modules/DetailModal'
|
|
|
+import { fetchSbTypeTree } from '@/api/sb/type'
|
|
|
import {
|
|
|
getWorkHourByUser,
|
|
|
getWorkHourBySb,
|
|
|
- getWorkHourByMonth,
|
|
|
exportWorkHourUser,
|
|
|
exportWorkHourSb,
|
|
|
- exportWorkHourMonth,
|
|
|
- exportWorkHourMonthDetail,
|
|
|
- exportWorkHourDetail
|
|
|
+ exportWorkHourDetail,
|
|
|
+ getMttrMonth,
|
|
|
+ getMtbfReport,
|
|
|
+ exportMttrMonth,
|
|
|
+ exportMtbfReport
|
|
|
} from '@/api/report/application-form'
|
|
|
|
|
|
+// 非计划性维修类别值(后端 RepairApplicationFormCategoryEnum.PLAN_NOT),MTTR/MTBF 明细过滤用
|
|
|
+const CATEGORY_NOT_PLAN = 2
|
|
|
+// MTTR/MTBF 统计时间段跨度上限(月),与后端 MTTR_MAX_MONTH_SPAN 保持一致
|
|
|
+const MTTR_MAX_MONTH_SPAN = 36
|
|
|
+
|
|
|
export default {
|
|
|
name: 'RepairWorkHourReport',
|
|
|
components: {
|
|
|
@@ -118,18 +180,24 @@ export default {
|
|
|
},
|
|
|
data () {
|
|
|
return {
|
|
|
- // 当前统计维度:user-按维修人,sb-按设备,month-按月度统计(月度页签已临时隐藏,逻辑保留)
|
|
|
+ // 当前统计维度:user-按维修人,sb-按设备,mttr-月度MTTR统计,mtbf-月度MTBF统计
|
|
|
activeTab: 'user',
|
|
|
+ // MTTR 目标值(分钟),会议确认候选值 90,趋势图上画目标横线
|
|
|
+ MTTR_TARGET_MINUTES: 90,
|
|
|
// 查询参数
|
|
|
queryParam: {
|
|
|
repairUserName: null,
|
|
|
keyword: null,
|
|
|
- // 月度统计 Tab 使用:a-month-picker 的 moment 值
|
|
|
- yearMonth: moment()
|
|
|
+ // 月度页签:设备筛选
|
|
|
+ levelList: [],
|
|
|
+ typeIdList: []
|
|
|
},
|
|
|
+ // 月度页签统计时间段(moment 对象,查询时转 yyyy-MM),默认本年 1-12 月
|
|
|
+ mttrStartMonth: moment().startOf('year'),
|
|
|
+ mttrEndMonth: moment().month(11).startOf('month'),
|
|
|
// 维修开始时间范围 默认本月,避免一次性拉取全部历史数据
|
|
|
dateRange: [moment().startOf('month'), moment().endOf('month')],
|
|
|
- // 图表
|
|
|
+ // user/sb 图表
|
|
|
chart: null,
|
|
|
chartLoading: false,
|
|
|
chartContainerId: 'workHourChartContainer',
|
|
|
@@ -139,14 +207,36 @@ export default {
|
|
|
chartDim: 'sb',
|
|
|
// 图表全量数据缓存,切换前20/全部时不重复请求
|
|
|
chartRows: [],
|
|
|
+ // 月度页签图表与数据缓存(MTTR / MTBF 分属不同页签,各自拉取)
|
|
|
+ monthChart: null,
|
|
|
+ monthSbChart: null,
|
|
|
+ // MTBF 按设备柱状图展示范围:top20-前20名,all-全部
|
|
|
+ mtbfChartTop: 'top20',
|
|
|
+ monthLoading: false,
|
|
|
+ mttrRows: [],
|
|
|
+ mtbfSbRows: [],
|
|
|
+ mtbfMonthRows: [],
|
|
|
+ // 设备等级字典(SBINFO_LEVEL:A/B/C类)与设备类别树
|
|
|
+ sbLevelMap: {},
|
|
|
+ sbTypeTreeData: [],
|
|
|
// 加载数据方法 必须为 Promise 对象
|
|
|
loadData: parameter => {
|
|
|
+ // 月度页签:数据已一次拉全,前端切片分页
|
|
|
+ if (this.isMonth) {
|
|
|
+ const list = this.monthMetric === 'mttr' ? this.mttrRows : this.mtbfSbRows
|
|
|
+ const pageNo = parameter.pageNo || parameter.pageNum || 1
|
|
|
+ const pageSize = parameter.pageSize || 20
|
|
|
+ return Promise.resolve({
|
|
|
+ rows: list.slice((pageNo - 1) * pageSize, pageNo * pageSize),
|
|
|
+ total: list.length
|
|
|
+ })
|
|
|
+ }
|
|
|
const query = this.buildQuery()
|
|
|
parameter = {
|
|
|
...parameter,
|
|
|
...query
|
|
|
}
|
|
|
- const fetchFn = this.activeTab === 'user' ? getWorkHourByUser : (this.activeTab === 'month' ? getWorkHourByMonth : getWorkHourBySb)
|
|
|
+ const fetchFn = this.activeTab === 'user' ? getWorkHourByUser : getWorkHourBySb
|
|
|
return fetchFn(parameter)
|
|
|
.then(res => {
|
|
|
return res.data
|
|
|
@@ -155,17 +245,42 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
+ // 是否为月度统计页签(MTTR 或 MTBF)
|
|
|
+ isMonth () {
|
|
|
+ return this.activeTab === 'mttr' || this.activeTab === 'mtbf'
|
|
|
+ },
|
|
|
+ // 月度页签当前指标:由所在页签决定
|
|
|
+ monthMetric () {
|
|
|
+ return this.activeTab === 'mtbf' ? 'mtbf' : 'mttr'
|
|
|
+ },
|
|
|
+ // 统计时间段展示文案:yyyy-MM ~ yyyy-MM
|
|
|
+ mttrPeriodLabel () {
|
|
|
+ if (!this.mttrStartMonth || !this.mttrEndMonth) {
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+ return this.mttrStartMonth.format('YYYY-MM') + ' ~ ' + this.mttrEndMonth.format('YYYY-MM')
|
|
|
+ },
|
|
|
chartTitle () {
|
|
|
if (this.activeTab === 'user') {
|
|
|
return '维修人平均工时统计图'
|
|
|
}
|
|
|
- if (this.activeTab === 'month') {
|
|
|
- return this.chartDim === 'type' ? '设备类型月度平均工时统计图' : '设备月度平均工时统计图'
|
|
|
- }
|
|
|
return this.chartDim === 'type' ? '设备类型平均工时统计图' : '设备平均工时统计图'
|
|
|
},
|
|
|
+ monthChartTitle () {
|
|
|
+ const period = this.mttrPeriodLabel
|
|
|
+ if (this.monthMetric === 'mttr') {
|
|
|
+ return period + ' MTTR月度趋势(平均故障修复时间,分钟)'
|
|
|
+ }
|
|
|
+ return period + ' 设备MTBF统计(平均故障间隔时间,小时,MTBF升序)'
|
|
|
+ },
|
|
|
rowKey () {
|
|
|
- return this.activeTab === 'user' ? 'repairUserId' : 'sbId'
|
|
|
+ if (this.activeTab === 'user') {
|
|
|
+ return 'repairUserId'
|
|
|
+ }
|
|
|
+ if (this.activeTab === 'mttr') {
|
|
|
+ return 'month'
|
|
|
+ }
|
|
|
+ return 'sbId'
|
|
|
},
|
|
|
columns () {
|
|
|
const indexColumn = {
|
|
|
@@ -184,6 +299,95 @@ export default {
|
|
|
const hourRender = (text) => {
|
|
|
return this.BaseTool.Object.isBlank(text) ? '0.00' : Number(text).toFixed(2)
|
|
|
}
|
|
|
+ const actionColumn = {
|
|
|
+ title: '操作',
|
|
|
+ checked: true,
|
|
|
+ key: 'action',
|
|
|
+ width: '180px',
|
|
|
+ align: 'center',
|
|
|
+ scopedSlots: { customRender: 'action' }
|
|
|
+ }
|
|
|
+ // 月度页签:MTTR/MTBF 两套表头
|
|
|
+ if (this.isMonth) {
|
|
|
+ if (this.monthMetric === 'mttr') {
|
|
|
+ return [
|
|
|
+ indexColumn,
|
|
|
+ {
|
|
|
+ title: '月份',
|
|
|
+ checked: true,
|
|
|
+ dataIndex: 'month'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '故障次数',
|
|
|
+ checked: true,
|
|
|
+ dataIndex: 'num'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '总维修工时(小时)',
|
|
|
+ checked: true,
|
|
|
+ dataIndex: 'totalRepairHours',
|
|
|
+ customRender: hourRender
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: 'MTTR(分钟)',
|
|
|
+ checked: true,
|
|
|
+ dataIndex: 'mttrMinutes',
|
|
|
+ customRender: hourRender
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '目标达成',
|
|
|
+ checked: true,
|
|
|
+ key: 'target',
|
|
|
+ customRender: (text, record) => {
|
|
|
+ if (!record.num) {
|
|
|
+ return '-'
|
|
|
+ }
|
|
|
+ const over = Number(record.mttrMinutes) > this.MTTR_TARGET_MINUTES
|
|
|
+ return this.$createElement('span', {
|
|
|
+ style: { color: over ? '#f5222d' : '#52c41a', fontWeight: 600 }
|
|
|
+ }, over ? '超标' : '达标')
|
|
|
+ }
|
|
|
+ },
|
|
|
+ actionColumn
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ return [
|
|
|
+ indexColumn,
|
|
|
+ {
|
|
|
+ title: '设备编号',
|
|
|
+ checked: true,
|
|
|
+ dataIndex: 'sbNo'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '设备名称',
|
|
|
+ checked: true,
|
|
|
+ dataIndex: 'sbName'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '设备等级',
|
|
|
+ checked: true,
|
|
|
+ dataIndex: 'level',
|
|
|
+ customRender: (text) => this.sbLevelMap[text] || '-'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '设备类别',
|
|
|
+ checked: true,
|
|
|
+ dataIndex: 'sbTypeName'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '故障次数',
|
|
|
+ checked: true,
|
|
|
+ dataIndex: 'faultNum'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: 'MTBF(小时)',
|
|
|
+ checked: true,
|
|
|
+ dataIndex: 'mtbfHours',
|
|
|
+ customRender: (text) => this.BaseTool.Object.isBlank(text) ? '-' : Number(text).toFixed(2)
|
|
|
+ },
|
|
|
+ actionColumn
|
|
|
+ ]
|
|
|
+ }
|
|
|
const commonColumns = [
|
|
|
{
|
|
|
title: '维修次数',
|
|
|
@@ -208,14 +412,7 @@ export default {
|
|
|
dataIndex: 'totalDealHours',
|
|
|
customRender: hourRender
|
|
|
},
|
|
|
- {
|
|
|
- title: '操作',
|
|
|
- checked: true,
|
|
|
- key: 'action',
|
|
|
- width: '180px',
|
|
|
- align: 'center',
|
|
|
- scopedSlots: { customRender: 'action' }
|
|
|
- }
|
|
|
+ actionColumn
|
|
|
]
|
|
|
if (this.activeTab === 'user') {
|
|
|
return [
|
|
|
@@ -228,7 +425,7 @@ export default {
|
|
|
...commonColumns
|
|
|
]
|
|
|
}
|
|
|
- const sbColumns = [
|
|
|
+ return [
|
|
|
indexColumn,
|
|
|
{
|
|
|
title: '设备编号',
|
|
|
@@ -247,13 +444,14 @@ export default {
|
|
|
},
|
|
|
...commonColumns
|
|
|
]
|
|
|
- // 月度统计不展示总停机修复时长
|
|
|
- if (this.activeTab === 'month') {
|
|
|
- return sbColumns.filter(c => c.dataIndex !== 'totalDealHours')
|
|
|
- }
|
|
|
- return sbColumns
|
|
|
}
|
|
|
},
|
|
|
+ created () {
|
|
|
+ this.sbLevelMap = this.DictCache.getLabelByValueMapByType(this.DictCache.TYPE.SBINFO_LEVEL)
|
|
|
+ fetchSbTypeTree().then(res => {
|
|
|
+ this.sbTypeTreeData = res.data || []
|
|
|
+ })
|
|
|
+ },
|
|
|
mounted () {
|
|
|
this.$nextTick(() => {
|
|
|
this.loadChartData()
|
|
|
@@ -262,11 +460,12 @@ export default {
|
|
|
beforeDestroy () {
|
|
|
this.chart && this.chart.destroy()
|
|
|
this.chart = null
|
|
|
+ this.destroyMonthCharts()
|
|
|
},
|
|
|
methods: {
|
|
|
loadChartData () {
|
|
|
this.chartLoading = true
|
|
|
- const fetchFn = this.activeTab === 'user' ? getWorkHourByUser : (this.activeTab === 'month' ? getWorkHourByMonth : getWorkHourBySb)
|
|
|
+ const fetchFn = this.activeTab === 'user' ? getWorkHourByUser : getWorkHourBySb
|
|
|
const parameter = {
|
|
|
pageNum: 1,
|
|
|
pageSize: 500,
|
|
|
@@ -284,6 +483,10 @@ export default {
|
|
|
})
|
|
|
},
|
|
|
doRenderChart () {
|
|
|
+ // 已切到月度页签时,异步回调不再渲染 user/sb 图表
|
|
|
+ if (this.isMonth) {
|
|
|
+ return
|
|
|
+ }
|
|
|
let data = []
|
|
|
if (this.activeTab === 'user') {
|
|
|
data = this.chartRows.map(r => ({
|
|
|
@@ -348,26 +551,158 @@ export default {
|
|
|
this.chart.render()
|
|
|
})
|
|
|
},
|
|
|
+ /**
|
|
|
+ * 月度页签:按当前页签(MTTR / MTBF)只拉取对应统计数据
|
|
|
+ */
|
|
|
+ loadMonthData () {
|
|
|
+ this.monthLoading = true
|
|
|
+ const query = this.buildQuery()
|
|
|
+ const fetch = this.monthMetric === 'mttr'
|
|
|
+ ? getMttrMonth(query).then(res => {
|
|
|
+ this.mttrRows = (res && res.data) || []
|
|
|
+ })
|
|
|
+ : getMtbfReport(query).then(res => {
|
|
|
+ const mtbf = (res && res.data) || {}
|
|
|
+ this.mtbfSbRows = mtbf.sbList || []
|
|
|
+ this.mtbfMonthRows = mtbf.monthList || []
|
|
|
+ })
|
|
|
+ fetch
|
|
|
+ .then(() => {
|
|
|
+ this.$refs.table.refresh(true)
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.renderMonthCharts()
|
|
|
+ })
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ this.monthLoading = false
|
|
|
+ })
|
|
|
+ },
|
|
|
+ renderMonthCharts () {
|
|
|
+ this.destroyMonthCharts()
|
|
|
+ // 已切离月度页签时不再渲染
|
|
|
+ if (!this.isMonth) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (this.monthMetric === 'mttr') {
|
|
|
+ // MTTR 月度趋势折线 + 目标值横线(会议确认候选值 90 分钟)
|
|
|
+ const data = this.mttrRows.map(r => ({
|
|
|
+ month: this.formatMonthLabel(r.month),
|
|
|
+ mttrMinutes: Number(r.mttrMinutes || 0)
|
|
|
+ }))
|
|
|
+ this.monthChart = new Chart({
|
|
|
+ container: 'monthTrendChartContainer',
|
|
|
+ autoFit: true,
|
|
|
+ height: 360
|
|
|
+ })
|
|
|
+ this.monthChart.data(data)
|
|
|
+ this.monthChart.scale({
|
|
|
+ mttrMinutes: { alias: 'MTTR(分钟)', nice: true, min: 0 }
|
|
|
+ })
|
|
|
+ this.monthChart.axis('mttrMinutes', { title: { offset: 40 } })
|
|
|
+ this.monthChart.line().position('month*mttrMinutes').color('#1890ff')
|
|
|
+ this.monthChart.point().position('month*mttrMinutes').color('#1890ff')
|
|
|
+ this.monthChart.annotation().line({
|
|
|
+ start: ['min', this.MTTR_TARGET_MINUTES],
|
|
|
+ end: ['max', this.MTTR_TARGET_MINUTES],
|
|
|
+ style: { stroke: '#f5222d', lineDash: [4, 4], lineWidth: 1 },
|
|
|
+ text: {
|
|
|
+ content: '目标值 ' + this.MTTR_TARGET_MINUTES + ' 分钟',
|
|
|
+ position: 'end',
|
|
|
+ style: { fill: '#f5222d', fontSize: 12, textAlign: 'end' }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ this.monthChart.tooltip({ shared: true, showCrosshairs: true })
|
|
|
+ this.monthChart.render()
|
|
|
+ } else {
|
|
|
+ // MTBF:仅按设备柱状图(MTBF 升序,故障不足2次无 MTBF 值的设备不展示;前20/全部可切换)
|
|
|
+ let sbData = this.mtbfSbRows
|
|
|
+ .filter(r => !this.BaseTool.Object.isBlank(r.mtbfHours))
|
|
|
+ .map(r => ({
|
|
|
+ name: (r.sbName || '-') + '(' + (r.sbNo || '-') + ')',
|
|
|
+ mtbfHours: Number(r.mtbfHours)
|
|
|
+ }))
|
|
|
+ if (this.mtbfChartTop === 'top20' && sbData.length > 20) {
|
|
|
+ sbData = sbData.slice(0, 20)
|
|
|
+ }
|
|
|
+ if (sbData.length) {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.monthSbChart = new Chart({
|
|
|
+ container: 'monthMtbfSbChartContainer',
|
|
|
+ autoFit: true,
|
|
|
+ height: 360
|
|
|
+ })
|
|
|
+ this.monthSbChart.data(sbData)
|
|
|
+ this.monthSbChart.scale({
|
|
|
+ mtbfHours: { alias: 'MTBF(小时)', nice: true, min: 0 }
|
|
|
+ })
|
|
|
+ // 纵轴标题(与 MTTR 趋势图一致,显示 scale alias)
|
|
|
+ this.monthSbChart.axis('mtbfHours', { title: { offset: 40 } })
|
|
|
+ this.monthSbChart.axis('name', { label: { autoRotate: true, autoHide: false } })
|
|
|
+ this.monthSbChart.interval().position('name*mtbfHours').color('#722ed1')
|
|
|
+ this.monthSbChart.tooltip({ shared: true, showCrosshairs: false })
|
|
|
+ this.monthSbChart.option('scrollbar', { type: 'horizontal' })
|
|
|
+ this.monthSbChart.render()
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ destroyMonthCharts () {
|
|
|
+ this.monthChart && this.monthChart.destroy()
|
|
|
+ this.monthChart = null
|
|
|
+ this.monthSbChart && this.monthSbChart.destroy()
|
|
|
+ this.monthSbChart = null
|
|
|
+ },
|
|
|
+ // 销毁所有图表实例(user/sb 柱状图 + 月度趋势/设备图),切换页签时调用防止 canvas 残留
|
|
|
+ destroyAllCharts () {
|
|
|
+ this.chart && this.chart.destroy()
|
|
|
+ this.chart = null
|
|
|
+ this.destroyMonthCharts()
|
|
|
+ },
|
|
|
+ formatMonthLabel (month) {
|
|
|
+ if (!month) {
|
|
|
+ return '-'
|
|
|
+ }
|
|
|
+ // 跨年时间段保留年份(如 2025-11),避免不同年份同月标签重复导致图表数据点被合并
|
|
|
+ if (this.mttrStartMonth && this.mttrEndMonth && !this.mttrStartMonth.isSame(this.mttrEndMonth, 'year')) {
|
|
|
+ return month
|
|
|
+ }
|
|
|
+ return parseInt(month.split('-')[1], 10) + '月'
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * yyyy-MM -> [当月第一天 00:00:00, 当月最后一天 23:59:59]
|
|
|
+ */
|
|
|
+ monthRange (month) {
|
|
|
+ const m = moment(month + '-01')
|
|
|
+ return [m.clone().startOf('month').format('YYYY-MM-DD HH:mm:ss'), m.clone().endOf('month').format('YYYY-MM-DD HH:mm:ss')]
|
|
|
+ },
|
|
|
buildQuery () {
|
|
|
const query = {}
|
|
|
if (this.activeTab === 'user') {
|
|
|
if (this.BaseTool.String.isNotBlank(this.queryParam.repairUserName)) {
|
|
|
query.repairUserName = this.queryParam.repairUserName
|
|
|
}
|
|
|
- } else if (this.activeTab === 'month') {
|
|
|
- // 月度统计:后端按 yearMonth(YYYY-MM) 转当月起止时间
|
|
|
- if (this.queryParam.yearMonth) {
|
|
|
- query.yearMonth = this.queryParam.yearMonth.format('YYYY-MM')
|
|
|
+ } else if (this.isMonth) {
|
|
|
+ // 月度MTTR/MTBF:统计时间段 + 设备等级/类别/关键字筛选
|
|
|
+ if (this.mttrStartMonth) {
|
|
|
+ query.mttrStartMonth = this.mttrStartMonth.format('YYYY-MM')
|
|
|
}
|
|
|
- if (this.BaseTool.String.isNotBlank(this.queryParam.keyword)) {
|
|
|
- query.keyword = this.queryParam.keyword
|
|
|
+ if (this.mttrEndMonth) {
|
|
|
+ query.mttrEndMonth = this.mttrEndMonth.format('YYYY-MM')
|
|
|
+ }
|
|
|
+ if (this.queryParam.levelList && this.queryParam.levelList.length) {
|
|
|
+ query.levelList = this.queryParam.levelList
|
|
|
+ }
|
|
|
+ if (this.queryParam.typeIdList && this.queryParam.typeIdList.length) {
|
|
|
+ query.typeIdList = this.queryParam.typeIdList
|
|
|
}
|
|
|
- } else {
|
|
|
if (this.BaseTool.String.isNotBlank(this.queryParam.keyword)) {
|
|
|
query.keyword = this.queryParam.keyword
|
|
|
}
|
|
|
+ return query
|
|
|
+ } else if (this.BaseTool.String.isNotBlank(this.queryParam.keyword)) {
|
|
|
+ query.keyword = this.queryParam.keyword
|
|
|
}
|
|
|
- if (this.activeTab !== 'month' && this.dateRange && this.dateRange.length === 2) {
|
|
|
+ if (this.dateRange && this.dateRange.length === 2) {
|
|
|
query.searchRepairStartTime = this.dateRange[0].format('YYYY-MM-DD') + ' 00:00:00'
|
|
|
query.searchRepairEndTime = this.dateRange[1].format('YYYY-MM-DD') + ' 23:59:59'
|
|
|
}
|
|
|
@@ -376,66 +711,163 @@ export default {
|
|
|
handleTabChange () {
|
|
|
// 切换维度后图表聚合维度恢复按设备
|
|
|
this.chartDim = 'sb'
|
|
|
+ // 切换页签先销毁全部图表实例,避免上个页签的 canvas 残留叠加到新页签
|
|
|
+ this.destroyAllCharts()
|
|
|
this.$nextTick(() => {
|
|
|
- this.$refs.table.refresh(true)
|
|
|
- this.loadChartData()
|
|
|
+ if (this.isMonth) {
|
|
|
+ this.loadMonthData()
|
|
|
+ } else {
|
|
|
+ this.$refs.table.refresh(true)
|
|
|
+ this.loadChartData()
|
|
|
+ }
|
|
|
})
|
|
|
},
|
|
|
handleSearch () {
|
|
|
+ if (this.isMonth) {
|
|
|
+ if (!this.validateMonthRange()) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.loadMonthData()
|
|
|
+ return
|
|
|
+ }
|
|
|
this.$refs.table.refresh(true)
|
|
|
this.loadChartData()
|
|
|
},
|
|
|
+ // 月度页签统计时间段校验,非法时提示并返回 false
|
|
|
+ validateMonthRange () {
|
|
|
+ if (!this.mttrStartMonth || !this.mttrEndMonth) {
|
|
|
+ this.$message.error('请选择统计开始月份和结束月份')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if (this.mttrStartMonth.isAfter(this.mttrEndMonth, 'month')) {
|
|
|
+ this.$message.error('开始月份不能晚于结束月份')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ // 与后端 MTTR_MAX_MONTH_SPAN 保持一致,提前拦截避免大区间慢查询
|
|
|
+ if (this.mttrEndMonth.diff(this.mttrStartMonth, 'month') + 1 > MTTR_MAX_MONTH_SPAN) {
|
|
|
+ this.$message.error('统计时间段跨度不能超过' + MTTR_MAX_MONTH_SPAN + '个月')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return true
|
|
|
+ },
|
|
|
resetSearchForm () {
|
|
|
this.queryParam = {
|
|
|
repairUserName: null,
|
|
|
keyword: null,
|
|
|
- yearMonth: moment()
|
|
|
+ levelList: [],
|
|
|
+ typeIdList: []
|
|
|
}
|
|
|
+ // 月度页签统计时间段恢复本年 1-12 月
|
|
|
+ this.mttrStartMonth = moment().startOf('year')
|
|
|
+ this.mttrEndMonth = moment().month(11).startOf('month')
|
|
|
// 重置后仍默认统计本月
|
|
|
this.dateRange = [moment().startOf('month'), moment().endOf('month')]
|
|
|
- this.$refs.table.refresh(true)
|
|
|
- this.loadChartData()
|
|
|
+ this.handleSearch()
|
|
|
},
|
|
|
doExport () {
|
|
|
const parameter = this.buildQuery()
|
|
|
- const exportFn = this.activeTab === 'user' ? exportWorkHourUser : (this.activeTab === 'month' ? exportWorkHourMonth : exportWorkHourSb)
|
|
|
+ // 当前页签图表截图(PNG dataURL)随查询条件一并提交,后端嵌入 Excel 的“统计图表”sheet
|
|
|
+ const chartBase64 = this.getChartBase64()
|
|
|
+ if (chartBase64) {
|
|
|
+ parameter.chartBase64 = chartBase64
|
|
|
+ }
|
|
|
+ if (this.isMonth) {
|
|
|
+ // 未点查询直接导出时同样校验时间段,避免下载到后端报错的 JSON 文件
|
|
|
+ if (!this.validateMonthRange()) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const monthExportFn = this.monthMetric === 'mttr' ? exportMttrMonth : exportMtbfReport
|
|
|
+ monthExportFn(parameter).then(file => {
|
|
|
+ this.BaseTool.UPLOAD.downLoadExportExcel(file)
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const exportFn = this.activeTab === 'user' ? exportWorkHourUser : exportWorkHourSb
|
|
|
exportFn(parameter).then(file => {
|
|
|
this.BaseTool.UPLOAD.downLoadExportExcel(file)
|
|
|
})
|
|
|
},
|
|
|
+ // 取当前页签已渲染图表的 PNG base64;图表未渲染(无数据)时返回 null,仅导出数据
|
|
|
+ getChartBase64 () {
|
|
|
+ // G2 4.0.15 的 Chart 没有 toDataURL,改为直接取画布容器内的原生 <canvas> 截图
|
|
|
+ let containerId
|
|
|
+ if (this.isMonth) {
|
|
|
+ containerId = this.monthMetric === 'mttr' ? 'monthTrendChartContainer' : 'monthMtbfSbChartContainer'
|
|
|
+ } else {
|
|
|
+ containerId = this.chartContainerId
|
|
|
+ }
|
|
|
+ const container = document.getElementById(containerId)
|
|
|
+ const canvas = container && container.querySelector('canvas')
|
|
|
+ if (!canvas) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return canvas.toDataURL('image/png')
|
|
|
+ } catch (e) {
|
|
|
+ // 截图失败不阻断导出,降级为仅导出数据
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ },
|
|
|
handleDetail (record) {
|
|
|
const modal = this.$refs.detailModal
|
|
|
+ if (this.isMonth) {
|
|
|
+ if (this.monthMetric === 'mttr') {
|
|
|
+ // MTTR 月度明细:该月全部非计划已完成工单,叠加当前设备筛选条件
|
|
|
+ const range = this.monthRange(record.month)
|
|
|
+ modal.base({
|
|
|
+ type: 'detail',
|
|
|
+ record: {},
|
|
|
+ title: 'MTTR明细 - ' + record.month,
|
|
|
+ category: CATEGORY_NOT_PLAN,
|
|
|
+ extraQuery: this.buildMonthFilter(),
|
|
|
+ searchRepairStartTime: range[0],
|
|
|
+ searchRepairEndTime: range[1]
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // MTBF 明细:该设备全部非计划已完成工单(按报修时间排序,可核对故障间隔)
|
|
|
+ modal.base({
|
|
|
+ type: 'sb',
|
|
|
+ record: record,
|
|
|
+ title: 'MTBF明细 - ' + (record.sbName || record.sbNo || ''),
|
|
|
+ category: CATEGORY_NOT_PLAN
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
const query = this.buildQuery()
|
|
|
modal.base({
|
|
|
type: this.activeTab,
|
|
|
record: record,
|
|
|
- yearMonth: query.yearMonth,
|
|
|
searchRepairStartTime: query.searchRepairStartTime,
|
|
|
searchRepairEndTime: query.searchRepairEndTime
|
|
|
})
|
|
|
},
|
|
|
doExportDetail (record) {
|
|
|
const parameter = this.buildDetailQuery(record)
|
|
|
- const exportFn = this.activeTab === 'month' ? exportWorkHourMonthDetail : exportWorkHourDetail
|
|
|
- exportFn(parameter).then(file => {
|
|
|
+ exportWorkHourDetail(parameter).then(file => {
|
|
|
this.BaseTool.UPLOAD.downLoadExportExcel(file)
|
|
|
})
|
|
|
},
|
|
|
buildDetailQuery (record) {
|
|
|
const parameter = {}
|
|
|
+ if (this.isMonth) {
|
|
|
+ parameter.category = CATEGORY_NOT_PLAN
|
|
|
+ if (this.monthMetric === 'mttr') {
|
|
|
+ const range = this.monthRange(record.month)
|
|
|
+ parameter.searchRepairStartTime = range[0]
|
|
|
+ parameter.searchRepairEndTime = range[1]
|
|
|
+ Object.assign(parameter, this.buildMonthFilter())
|
|
|
+ } else {
|
|
|
+ parameter.sbId = record.sbId
|
|
|
+ }
|
|
|
+ return parameter
|
|
|
+ }
|
|
|
if (this.activeTab === 'user') {
|
|
|
parameter.repairUserId = record.repairUserId
|
|
|
} else {
|
|
|
parameter.sbId = record.sbId
|
|
|
}
|
|
|
const timeQuery = this.buildQuery()
|
|
|
- if (this.activeTab === 'month') {
|
|
|
- // 月度明细导出:传 yearMonth(YYYY-MM),后端转当月起止时间
|
|
|
- if (timeQuery.yearMonth) {
|
|
|
- parameter.yearMonth = timeQuery.yearMonth
|
|
|
- }
|
|
|
- return parameter
|
|
|
- }
|
|
|
if (timeQuery.searchRepairStartTime) {
|
|
|
parameter.searchRepairStartTime = timeQuery.searchRepairStartTime
|
|
|
}
|
|
|
@@ -443,6 +875,20 @@ export default {
|
|
|
parameter.searchRepairEndTime = timeQuery.searchRepairEndTime
|
|
|
}
|
|
|
return parameter
|
|
|
+ },
|
|
|
+ // 月度页签的设备筛选条件(与统计口径保持一致)
|
|
|
+ buildMonthFilter () {
|
|
|
+ const filter = {}
|
|
|
+ if (this.queryParam.levelList && this.queryParam.levelList.length) {
|
|
|
+ filter.levelList = this.queryParam.levelList
|
|
|
+ }
|
|
|
+ if (this.queryParam.typeIdList && this.queryParam.typeIdList.length) {
|
|
|
+ filter.typeIdList = this.queryParam.typeIdList
|
|
|
+ }
|
|
|
+ if (this.BaseTool.String.isNotBlank(this.queryParam.keyword)) {
|
|
|
+ filter.keyword = this.queryParam.keyword
|
|
|
+ }
|
|
|
+ return filter
|
|
|
}
|
|
|
}
|
|
|
}
|