refactor(auth): 增加锌线系统token管理功能 feat(api): 添加锌线停机记录、生产报表和设备快照API feat(views): 实现锌线实时监控、生产统计和停机统计页面 feat(components): 开发锌线生产报表、停机统计和班组绩效组件 feat(utils): 新增锌线专用请求工具zinc1Request chore(vue.config): 配置锌线API代理
908 lines
27 KiB
Vue
908 lines
27 KiB
Vue
<template>
|
||
<div class="page-container">
|
||
<div style="display: flex; align-items: center;">
|
||
<!-- 时间维度切换 -->
|
||
<div class="time-tab-bar">
|
||
<div v-for="item in timeTabs" :key="item.value" class="time-tab-item"
|
||
:class="{ 'time-tab-active': activeTab === item.value }" @click="handleTabChange(item.value)">
|
||
{{ item.label }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 日期选择区 -->
|
||
<div class="date-selector">
|
||
<!-- 日模式 -->
|
||
<el-date-picker v-if="activeTab === 'day'" v-model="startDate" type="date" value-format="yyyy-MM-dd"
|
||
placeholder="选择日期" @change="handleDateChange" class="single-date-picker" />
|
||
|
||
<!-- 月模式 -->
|
||
<div v-else-if="activeTab === 'month'" class="date-range-group">
|
||
<el-date-picker v-model="startDate" type="month" value-format="yyyy-MM" placeholder="选择开始月份"
|
||
@change="handleStartMonthChange" class="range-date-picker" />
|
||
<span class="date-separator">至</span>
|
||
<el-date-picker v-model="endDate" type="month" value-format="yyyy-MM" placeholder="选择结束月份"
|
||
:picker-options="monthPickerOptions" @change="handleEndMonthChange" class="range-date-picker" />
|
||
</div>
|
||
|
||
<!-- 年模式 -->
|
||
<div v-else class="date-range-group">
|
||
<el-date-picker v-model="startDate" type="year" value-format="yyyy" placeholder="选择开始年份"
|
||
@change="handleStartYearChange" class="range-date-picker" />
|
||
<span class="date-separator">至</span>
|
||
<el-date-picker v-model="endDate" type="year" value-format="yyyy" placeholder="选择结束年份"
|
||
@change="handleEndYearChange" class="range-date-picker" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 停机汇总 -->
|
||
<div class="summary-section">
|
||
<div class="section-header">
|
||
<span class="section-title">停机汇总</span>
|
||
<span class="section-date">{{ displayDateRange }}</span>
|
||
</div>
|
||
<div class="summary-grid">
|
||
<div class="summary-card" v-for="(item, index) in summaryData" :key="index">
|
||
<span class="summary-label">{{ item.label }}</span>
|
||
<div class="summary-value-box">
|
||
<span class="summary-value">{{ item.value }}</span>
|
||
<span v-if="item.unit" class="summary-unit">{{ item.unit }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<el-row :gutter="10">
|
||
<el-col :span="12">
|
||
<!-- 停机分布 - 班组 -->
|
||
<div class="chart-section">
|
||
<div class="section-header">
|
||
<span class="section-title">班组停机分布</span>
|
||
</div>
|
||
<div v-if="crewPieData.length > 0" class="pie-chart-single" ref="crewPie" id="crewPie"></div>
|
||
<div class="empty-chart" v-else>
|
||
<span class="empty-icon">📊</span>
|
||
<span class="empty-text">此时间段未发生停机</span>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
|
||
<el-col :span="12">
|
||
<!-- 停机分布 - 类型 -->
|
||
<div class="chart-section">
|
||
<div class="section-header">
|
||
<span class="section-title">停机类型分布</span>
|
||
</div>
|
||
<div v-if="typePieData.length > 0" class="pie-chart-single" ref="typePie" id="typePie"></div>
|
||
<div class="empty-chart" v-else>
|
||
<span class="empty-icon">📊</span>
|
||
<span class="empty-text">此时间段未发生停机</span>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 停机详细列表(日视图) -->
|
||
<div class="detail-section" v-if="activeTab === 'day'">
|
||
<div class="section-header">
|
||
<span class="section-title">停机详情</span>
|
||
</div>
|
||
<div class="detail-list">
|
||
<div v-if="tableData.length === 0" class="empty-state">
|
||
<span class="empty-text">暂无停机记录</span>
|
||
</div>
|
||
<el-table v-else :data="tableData">
|
||
<el-table-column label="时间范围" prop="time"></el-table-column>
|
||
<el-table-column label="持续时间" prop="duration"></el-table-column>
|
||
<el-table-column label="机组" prop="machine"></el-table-column>
|
||
<el-table-column label="备注" prop="remark" show-overflow-tooltip></el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
<!-- 停机趋势图(月/年视图) -->
|
||
<div class="chart-section" v-else>
|
||
<div class="section-header">
|
||
<span class="section-title">停机趋势</span>
|
||
</div>
|
||
<div class="chart-wrapper trend-chart" ref="trendChart" id="trendChart"></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
// ✅【完全未改动】原API导入
|
||
import { listStoppage } from '@/api/pocket/plantState'
|
||
// 引入Echarts5.x 兼容Vue2
|
||
import * as echarts from 'echarts'
|
||
|
||
// ✅【完全未改动】原独立工具函数
|
||
function getDefaultDate(type = "day") {
|
||
const date = new Date();
|
||
const year = date.getFullYear();
|
||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||
const day = date.getDate().toString().padStart(2, "0");
|
||
|
||
switch (type) {
|
||
case "day":
|
||
return `${year}-${month}-${day}`;
|
||
case "month":
|
||
return `${year}-${month}`;
|
||
case "year":
|
||
return `${year}`;
|
||
default:
|
||
return `${year}-${month}-${day}`;
|
||
}
|
||
}
|
||
|
||
function getLastMonth() {
|
||
const date = new Date();
|
||
date.setMonth(date.getMonth() - 1);
|
||
const year = date.getFullYear();
|
||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||
return `${year}-${month}`;
|
||
}
|
||
|
||
function formatDate(date, type) {
|
||
const year = date.getFullYear();
|
||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||
const day = date.getDate().toString().padStart(2, "0");
|
||
|
||
switch (type) {
|
||
case "day":
|
||
return `${year}-${month}-${day}`;
|
||
case "month":
|
||
return `${year}-${month}`;
|
||
case "year":
|
||
return `${year}`;
|
||
default:
|
||
return `${year}-${month}-${day}`;
|
||
}
|
||
}
|
||
|
||
export default {
|
||
name: 'PlantStoppage',
|
||
data() {
|
||
return {
|
||
// ✅【完全未改动】业务数据
|
||
activeTab: "day",
|
||
startDate: getDefaultDate(),
|
||
endDate: getDefaultDate(),
|
||
timeTabs: [
|
||
{ label: "日", value: "day" },
|
||
{ label: "月", value: "month" },
|
||
{ label: "年", value: "year" }
|
||
],
|
||
summaryData: [
|
||
{ label: "停机时间", value: 0, unit: "min" },
|
||
{ label: "停机次数", value: 0, unit: "次" },
|
||
{ label: "作业率", value: 0, unit: "%" }
|
||
],
|
||
// Echarts图表实例
|
||
trendChart: null,
|
||
crewPie: null,
|
||
typePie: null,
|
||
// 月份选择器限制条件
|
||
monthPickerOptions: {},
|
||
// 趋势图临时存储X轴数据
|
||
trendXData: [],
|
||
// ✅【完全未改动】原图表配色
|
||
mainColor: ["#0066cc", "#f56c6c"],
|
||
pieColor: ["#0066cc", "#409eff", "#66b1ff", "#a0cfff", "#d9ecff"],
|
||
crewPieData: [],
|
||
typePieData: [],
|
||
tableData: [],
|
||
// ✅【新增】resize防抖定时器,解决窗口缩放卡顿
|
||
resizeTimer: null
|
||
};
|
||
},
|
||
// ✅【完全未改动】计算属性
|
||
computed: {
|
||
maxMonthEnd() {
|
||
if (!this.startDate) return "";
|
||
const date = new Date(this.startDate);
|
||
date.setFullYear(date.getFullYear() + 1);
|
||
return formatDate(date, "month");
|
||
},
|
||
displayDateRange() {
|
||
switch (this.activeTab) {
|
||
case "day":
|
||
return this.startDate;
|
||
case "month":
|
||
return `${this.startDate} 至 ${this.endDate}`;
|
||
case "year":
|
||
return `${this.startDate} 至 ${this.endDate}`;
|
||
default:
|
||
return "";
|
||
}
|
||
}
|
||
},
|
||
watch: {
|
||
// 监听开始月份,更新结束月份可选范围
|
||
startDate(val) {
|
||
if (this.activeTab === 'month' && val) {
|
||
this.monthPickerOptions = {
|
||
disabledDate: (time) => {
|
||
const maxDate = new Date(this.maxMonthEnd + '-01')
|
||
return time.getTime() > maxDate.getTime()
|
||
}
|
||
}
|
||
}
|
||
},
|
||
// ✅【新增】监听tab切换,自动触发图表自适应
|
||
activeTab() {
|
||
this.$nextTick(() => {
|
||
this.resizeEcharts()
|
||
})
|
||
}
|
||
},
|
||
mounted() {
|
||
// 初始化Echarts图表
|
||
this.initEcharts()
|
||
// 加载业务数据
|
||
this.loadStoppageData()
|
||
// 窗口自适应监听
|
||
window.addEventListener('resize', this.resizeEcharts)
|
||
},
|
||
beforeDestroy() {
|
||
// ✅【修复】安全销毁图表实例+清除监听+清除定时器,防止内存泄漏
|
||
window.removeEventListener('resize', this.resizeEcharts)
|
||
clearTimeout(this.resizeTimer)
|
||
if (this.trendChart && !this.trendChart.isDisposed()) {
|
||
this.trendChart.dispose()
|
||
this.trendChart = null
|
||
}
|
||
if (this.crewPie && !this.crewPie.isDisposed()) {
|
||
this.crewPie.dispose()
|
||
this.crewPie = null
|
||
}
|
||
if (this.typePie && !this.typePie.isDisposed()) {
|
||
this.typePie.dispose()
|
||
this.typePie = null
|
||
}
|
||
},
|
||
methods: {
|
||
// ✅【逻辑未改动】仅适配Element传参
|
||
handleTabChange(tab) {
|
||
this.activeTab = tab;
|
||
if (tab === "day") {
|
||
const today = getDefaultDate();
|
||
this.startDate = today;
|
||
this.endDate = today;
|
||
} else if (tab === "month") {
|
||
this.startDate = getLastMonth();
|
||
this.endDate = getDefaultDate("month");
|
||
} else {
|
||
const currentYear = getDefaultDate("year");
|
||
this.startDate = currentYear;
|
||
this.endDate = currentYear;
|
||
}
|
||
this.loadStoppageData();
|
||
},
|
||
handleDateChange(val) {
|
||
this.startDate = val;
|
||
this.endDate = val;
|
||
this.loadStoppageData();
|
||
},
|
||
handleStartMonthChange(val) {
|
||
this.startDate = val;
|
||
const maxEndDate = new Date(this.startDate);
|
||
maxEndDate.setFullYear(maxEndDate.getFullYear() + 1);
|
||
const maxEndStr = formatDate(maxEndDate, "month");
|
||
if (new Date(this.endDate) > maxEndDate) {
|
||
this.endDate = maxEndStr;
|
||
}
|
||
this.loadStoppageData();
|
||
},
|
||
handleEndMonthChange(val) {
|
||
this.endDate = val;
|
||
this.loadStoppageData();
|
||
},
|
||
handleStartYearChange(val) {
|
||
this.startDate = val;
|
||
this.loadStoppageData();
|
||
},
|
||
handleEndYearChange(val) {
|
||
this.endDate = val;
|
||
this.loadStoppageData();
|
||
},
|
||
|
||
// ✅【核心逻辑完全未改动】仅替换uni加载提示为Element + 新增清空图表逻辑
|
||
loadStoppageData() {
|
||
const loading = this.$loading({
|
||
lock: true,
|
||
text: '加载中',
|
||
spinner: 'el-icon-loading',
|
||
background: 'rgba(0, 0, 0, 0.7)'
|
||
});
|
||
|
||
const start = this.formatFullDate(this.startDate, true)
|
||
let end = this.formatFullDate(this.endDate, false)
|
||
|
||
if (this.activeTab === 'month' && this.endDate && this.endDate.length === 7) {
|
||
const today = new Date()
|
||
const todayYear = today.getFullYear()
|
||
const todayMonth = today.getMonth() + 1
|
||
const [endYear, endMonth] = this.endDate.split('-').map(Number)
|
||
if (endYear === todayYear && endMonth === todayMonth) {
|
||
const todayDay = today.getDate()
|
||
end = `${this.endDate}-${String(todayDay).padStart(2, '0')}`
|
||
}
|
||
}
|
||
|
||
const queryParams = {
|
||
pageNum: 1,
|
||
pageSize: 9999,
|
||
startDate: start,
|
||
endDate: end
|
||
}
|
||
|
||
console.log('停机查询参数:', queryParams)
|
||
|
||
listStoppage(queryParams).then(response => {
|
||
loading.close()
|
||
console.log('停机统计响应:', response)
|
||
|
||
if (response.code === 200 && response.rows && response.rows.length > 0) {
|
||
this.tableData = response.rows.map(item => ({
|
||
time: this.formatDateTime(item.startDate) + ' - ' + this.formatDateTime(item.endDate),
|
||
duration: this.secondsToMinutes(item.duration) + 'min',
|
||
remark: item.remark || '-',
|
||
machine: item.unit || '-'
|
||
}))
|
||
|
||
const totalDurationSeconds = response.rows.reduce((sum, item) => sum + (Number(item.duration) || 0), 0)
|
||
const totalDurationMinutes = this.secondsToMinutes(totalDurationSeconds)
|
||
const totalCount = response.rows.length
|
||
const totalAvailableMinutes = this.getTotalAvailableMinutes()
|
||
const workRate = this.calculateWorkRate(totalDurationMinutes, totalAvailableMinutes)
|
||
|
||
this.summaryData = [
|
||
{ label: '停机时间', value: totalDurationMinutes, unit: 'min' },
|
||
{ label: '停机次数', value: totalCount, unit: '次' },
|
||
{ label: '作业率', value: workRate, unit: '%' }
|
||
]
|
||
|
||
const crewMap = {}
|
||
const typeMap = {}
|
||
response.rows.forEach(item => {
|
||
const crew = item.crew || '未知班组'
|
||
const type = item.stopType || '未知类型'
|
||
const durationMinutes = this.secondsToMinutes(item.duration)
|
||
crewMap[crew] = (crewMap[crew] || 0) + durationMinutes
|
||
typeMap[type] = (typeMap[type] || 0) + durationMinutes
|
||
})
|
||
|
||
this.crewPieData = Object.keys(crewMap).map(crew => ({ name: crew, value: crewMap[crew] }))
|
||
this.typePieData = Object.keys(typeMap).map(type => ({ name: type, value: typeMap[type] }))
|
||
|
||
// 渲染饼图
|
||
this.$nextTick(() => {
|
||
this.renderPieChart('crew', this.crewPieData)
|
||
this.renderPieChart('type', this.typePieData)
|
||
})
|
||
|
||
if (this.activeTab !== 'day') {
|
||
if (response.rows.length > 0) {
|
||
this.buildTrendChart(response.rows)
|
||
} else {
|
||
this.trendXData = []
|
||
}
|
||
}
|
||
} else {
|
||
console.log('暂无停机数据')
|
||
this.tableData = []
|
||
this.summaryData = [
|
||
{ label: '停机时间', value: 0, unit: 'min' },
|
||
{ label: '停机次数', value: 0, unit: '次' },
|
||
{ label: '作业率', value: 100, unit: '%' }
|
||
]
|
||
this.crewPieData = []
|
||
this.typePieData = []
|
||
this.trendXData = []
|
||
// ✅【修复】清空所有图表数据
|
||
this.clearChart(this.trendChart)
|
||
this.renderPieChart('crew', [])
|
||
this.renderPieChart('type', [])
|
||
}
|
||
}).catch(error => {
|
||
loading.close()
|
||
console.error('加载停机数据失败:', error)
|
||
this.$message.error('加载失败,请稍后重试')
|
||
})
|
||
},
|
||
|
||
// ✅【完全未改动】原所有工具方法
|
||
formatDateTime(dateStr) {
|
||
if (!dateStr) return '-'
|
||
const date = new Date(dateStr)
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
const hours = String(date.getHours()).padStart(2, '0')
|
||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||
},
|
||
getTotalAvailableMinutes() {
|
||
const start = new Date(this.formatFullDate(this.startDate, true))
|
||
const end = new Date(this.formatFullDate(this.endDate, false))
|
||
const diffTime = end - start
|
||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1
|
||
return diffDays * 1440
|
||
},
|
||
calculateWorkRate(stopDuration, totalAvailableMinutes) {
|
||
if (!totalAvailableMinutes || totalAvailableMinutes === 0) {
|
||
return 100
|
||
}
|
||
const workRate = ((totalAvailableMinutes - stopDuration) / totalAvailableMinutes) * 100
|
||
return Math.max(0, Math.min(100, workRate)).toFixed(2)
|
||
},
|
||
formatDate(dateStr) {
|
||
if (!dateStr) return ''
|
||
const date = new Date(dateStr)
|
||
const month = date.getMonth() + 1
|
||
const day = date.getDate()
|
||
return `${month}/${day}`
|
||
},
|
||
formatDateByMonth(dateStr) {
|
||
if (!dateStr) return ''
|
||
const date = new Date(dateStr)
|
||
const month = date.getMonth() + 1
|
||
return `${month}月`
|
||
},
|
||
secondsToMinutes(seconds) {
|
||
if (!seconds || seconds === 0) return 0
|
||
return Math.round(Number(seconds) / 60)
|
||
},
|
||
formatFullDate(dateStr, isStart) {
|
||
if (!dateStr) return ''
|
||
if (dateStr.length === 10) {
|
||
return dateStr
|
||
}
|
||
if (dateStr.length === 7) {
|
||
if (isStart) {
|
||
return `${dateStr}-01`
|
||
} else {
|
||
const [year, month] = dateStr.split('-')
|
||
const lastDay = new Date(year, month, 0).getDate()
|
||
return `${dateStr}-${String(lastDay).padStart(2, '0')}`
|
||
}
|
||
}
|
||
if (dateStr.length === 4) {
|
||
if (isStart) {
|
||
return `${dateStr}-01-01`
|
||
} else {
|
||
return `${dateStr}-12-31`
|
||
}
|
||
}
|
||
return dateStr
|
||
},
|
||
|
||
// ✅【逻辑未改动】仅修改图表渲染方式
|
||
buildTrendChart(stoppageData) {
|
||
if (!stoppageData || stoppageData.length === 0) {
|
||
console.log('无法构建趋势图:数据为空')
|
||
this.trendXData = []
|
||
return
|
||
}
|
||
const dateMap = {}
|
||
const isYearView = this.activeTab === 'year'
|
||
stoppageData.forEach(item => {
|
||
if (!item.startDate) return
|
||
let key
|
||
if (isYearView) {
|
||
key = this.formatDateByMonth(item.startDate)
|
||
} else {
|
||
key = this.formatDate(item.startDate)
|
||
}
|
||
if (!dateMap[key]) {
|
||
dateMap[key] = { duration: 0, count: 0 }
|
||
}
|
||
const durationMinutes = this.secondsToMinutes(item.duration)
|
||
dateMap[key].duration += durationMinutes
|
||
dateMap[key].count += 1
|
||
})
|
||
|
||
let categories = []
|
||
if (isYearView) {
|
||
categories = Array.from({ length: 12 }, (_, i) => `${i + 1}月`)
|
||
} else {
|
||
const startStr = this.formatFullDate(this.startDate, true)
|
||
let endStr = this.formatFullDate(this.endDate, false)
|
||
const today = new Date()
|
||
const todayYear = today.getFullYear()
|
||
const todayMonth = today.getMonth() + 1
|
||
if (this.endDate && this.endDate.length === 7) {
|
||
const [endYear, endMonth] = this.endDate.split('-').map(Number)
|
||
if (endYear === todayYear && endMonth === todayMonth) {
|
||
const todayDay = today.getDate()
|
||
endStr = `${this.endDate}-${String(todayDay).padStart(2, '0')}`
|
||
}
|
||
}
|
||
const start = new Date(startStr)
|
||
const end = new Date(endStr)
|
||
const dateList = []
|
||
const currentDate = new Date(start)
|
||
while (currentDate <= end) {
|
||
const month = currentDate.getMonth() + 1
|
||
const day = currentDate.getDate()
|
||
const dateKey = `${month}/${day}`
|
||
dateList.push(dateKey)
|
||
currentDate.setDate(currentDate.getDate() + 1)
|
||
}
|
||
categories = dateList
|
||
}
|
||
|
||
if (categories.length === 0) {
|
||
console.log('无法构建趋势图:无有效日期')
|
||
this.trendXData = []
|
||
return
|
||
}
|
||
|
||
this.trendXData = categories
|
||
const durationData = []
|
||
const rateData = []
|
||
categories.forEach(key => {
|
||
const data = dateMap[key] || { duration: 0, count: 0 }
|
||
durationData.push(data.duration)
|
||
let totalMinutes
|
||
if (isYearView) {
|
||
const year = parseInt(this.startDate) || new Date().getFullYear()
|
||
const monthIndex = parseInt(key.replace('月', '')) - 1
|
||
const daysInMonth = new Date(year, monthIndex + 1, 0).getDate()
|
||
totalMinutes = daysInMonth * 1440
|
||
} else {
|
||
totalMinutes = 1440
|
||
}
|
||
const rate = this.calculateWorkRate(data.duration, totalMinutes)
|
||
rateData.push(Number(rate))
|
||
})
|
||
// 渲染趋势混合图
|
||
this.renderTrendChart(categories, durationData, rateData)
|
||
},
|
||
|
||
// ===== ✅【全部修复】Echarts 图表初始化/渲染/自适应 核心方法 =====
|
||
// 安全初始化单个图表实例【新增核心方法】
|
||
initSingleChart(chartId) {
|
||
const dom = document.getElementById(chartId)
|
||
console.log('获取dom', dom)
|
||
if (!dom) return null
|
||
const chartInstance = echarts.init(dom)
|
||
return chartInstance
|
||
},
|
||
// 安全清空图表内容【新增核心方法】
|
||
clearChart(chartInstance) {
|
||
if (chartInstance && !chartInstance.isDisposed()) {
|
||
chartInstance.clear()
|
||
}
|
||
},
|
||
// 初始化图表(懒初始化占位)
|
||
initEcharts() {
|
||
this.trendChart = this.initSingleChart('trendChart')
|
||
this.crewPie = this.initSingleChart('crewPie')
|
||
this.typePie = this.initSingleChart('typePie')
|
||
},
|
||
// 渲染停机趋势图 (柱状+折线 双Y轴)【修复完整版】
|
||
renderTrendChart(xData, durData, rateData) {
|
||
this.trendChart = this.initSingleChart('trendChart')
|
||
if (!this.trendChart) return
|
||
if (xData.length === 0) {
|
||
this.clearChart(this.trendChart)
|
||
return
|
||
}
|
||
const option = {
|
||
color: this.mainColor,
|
||
grid: { left: 30, right: 30, top: 40, bottom: 60 },
|
||
legend: { top: 0, left: 'center' },
|
||
tooltip: { trigger: 'axis' },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: xData,
|
||
axisLine: { show: false },
|
||
axisLabel: { rotate: 60, fontSize: 12 }
|
||
},
|
||
yAxis: [
|
||
{ type: 'value', name: '停机时间(min)', position: 'left', splitLine: { type: 'dashed', color: '#e4e7ed' } },
|
||
{ type: 'value', name: '作业率(%)', position: 'right', splitLine: { show: false } }
|
||
],
|
||
series: [
|
||
{ name: '停机时间', type: 'bar', data: durData, barWidth: 40 },
|
||
{ name: '作业率', type: 'line', yAxisIndex: 1, data: rateData, smooth: true }
|
||
]
|
||
}
|
||
this.trendChart.setOption(option, true)
|
||
},
|
||
// 渲染饼图 (班组/类型 通用)【修复完整版】
|
||
renderPieChart(type, data) {
|
||
const chartId = type === 'crew' ? 'crewPie' : 'typePie'
|
||
if (type === 'crew') {
|
||
this.crewPie = this.initSingleChart(chartId)
|
||
} else {
|
||
this.typePie = this.initSingleChart(chartId)
|
||
}
|
||
console.log('渲染饼图数据:', type, data, chartId)
|
||
const chart = type === 'crew' ? this.crewPie : this.typePie
|
||
console.log('获取到的图表实例:', chart)
|
||
if (!chart) return
|
||
if (data.length === 0) {
|
||
this.clearChart(chart)
|
||
return
|
||
}
|
||
const option = {
|
||
color: this.pieColor,
|
||
tooltip: { trigger: 'item', formatter: '{b}: {c}min ({d}%)' },
|
||
legend: { bottom: 0, left: 'center', textStyle: { fontSize: 12, color: '#666' } },
|
||
series: [{
|
||
name: type === 'crew' ? '班组停机' : '类型停机',
|
||
type: 'pie',
|
||
radius: ['40%', '70%'],
|
||
data: data,
|
||
label: { show: true, fontSize: 12 }
|
||
}]
|
||
}
|
||
chart.setOption(option, true)
|
||
},
|
||
// 图表自适应 - 防抖+安全校验【修复完整版】
|
||
resizeEcharts: function () {
|
||
clearTimeout(this.resizeTimer)
|
||
this.resizeTimer = setTimeout(() => {
|
||
if (this.trendChart && !this.trendChart.isDisposed()) this.trendChart.resize()
|
||
if (this.crewPie && !this.crewPie.isDisposed()) this.crewPie.resize()
|
||
if (this.typePie && !this.typePie.isDisposed()) this.typePie.resize()
|
||
}, 200)
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
// ✅ PC端适配:rpx转px(1rpx=0.5px) 保留原布局+样式+间距 + 修复图表高度问题
|
||
.page-container {
|
||
background: #f5f7fa;
|
||
padding: 12px;
|
||
min-height: calc(100vh - 20px);
|
||
}
|
||
|
||
.time-tab-bar {
|
||
display: flex;
|
||
width: 240px;
|
||
background: #fff;
|
||
border-radius: 4px;
|
||
padding: 4px;
|
||
margin-bottom: 12px;
|
||
border: 1px solid #e4e7ed;
|
||
}
|
||
|
||
.time-tab-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 8px 0;
|
||
font-size: 13px;
|
||
color: #606266;
|
||
border-radius: 3px;
|
||
transition: all 0.2s;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.time-tab-active {
|
||
background: #0066cc;
|
||
color: #fff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.date-selector {
|
||
background: #fff;
|
||
border-radius: 4px;
|
||
padding: 12px;
|
||
margin-bottom: 12px;
|
||
margin-left: 20px;
|
||
border: 1px solid #e4e7ed;
|
||
}
|
||
|
||
.single-date-picker {
|
||
width: 100%;
|
||
}
|
||
|
||
.date-range-group {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: stretch;
|
||
gap: 0;
|
||
width: 100%;
|
||
}
|
||
|
||
.range-date-picker {
|
||
flex: 1;
|
||
}
|
||
|
||
.date-separator {
|
||
font-size: 14px;
|
||
color: #000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 40px;
|
||
}
|
||
|
||
.summary-section,
|
||
.chart-section,
|
||
.detail-section {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
padding-left: 8px;
|
||
border-left: 2px solid #0066cc;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: #303133;
|
||
}
|
||
|
||
.section-date {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.summary-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 8px;
|
||
}
|
||
|
||
.summary-card {
|
||
background: #fff;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
padding: 14px 10px;
|
||
text-align: center;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||
}
|
||
}
|
||
|
||
.summary-label {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.summary-value-box {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: center;
|
||
}
|
||
|
||
.summary-value {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #0066cc;
|
||
line-height: 1;
|
||
}
|
||
|
||
.summary-unit {
|
||
font-size: 11px;
|
||
color: #909399;
|
||
margin-left: 3px;
|
||
}
|
||
|
||
.chart-wrapper {
|
||
background: #fff;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
padding: 12px 8px;
|
||
min-height: 225px;
|
||
width: 100%;
|
||
}
|
||
|
||
// ✅【修复】趋势图高度强制生效
|
||
.trend-chart {
|
||
height: 250px !important;
|
||
min-height: 250px;
|
||
}
|
||
|
||
// ✅【修复】饼图容器增加固定高度,解决高度塌陷
|
||
.pie-chart-single {
|
||
background: #fff;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
padding: 16px 0;
|
||
height: 260px !important;
|
||
min-height: 240px;
|
||
width: 100%;
|
||
}
|
||
|
||
// ✅【修复】空图表容器高度和饼图一致
|
||
.empty-chart {
|
||
background: #fff;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
padding: 50px 0;
|
||
height: 260px !important;
|
||
min-height: 240px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 40px;
|
||
opacity: 0.3;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
}
|
||
|
||
.detail-list {
|
||
background: #fff;
|
||
border-radius: 4px;
|
||
border: 1px solid #e4e7ed;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.detail-item {
|
||
padding: 12px;
|
||
border-bottom: 1px solid #f5f7fa;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
.detail-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.detail-time {
|
||
font-size: 13px;
|
||
color: #606266;
|
||
}
|
||
|
||
.detail-duration {
|
||
font-size: 14px;
|
||
color: #0066cc;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.detail-info {
|
||
display: flex;
|
||
align-items: baseline;
|
||
margin-bottom: 4px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.detail-label {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
min-width: 60px;
|
||
}
|
||
|
||
.detail-text {
|
||
font-size: 13px;
|
||
color: #303133;
|
||
flex: 1;
|
||
}
|
||
|
||
.empty-state {
|
||
padding: 50px 0;
|
||
text-align: center;
|
||
}
|
||
</style> |