Files
klp-mono/apps/hand-factory/components/klp-shutdown-statistic/klp-shutdown-statistic.vue

912 lines
24 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>
<view class="page-container">
<!-- 时间维度切换 -->
<view class="time-tab-bar">
<view
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 }}
</view>
</view>
<!-- 日期选择区 -->
<view class="date-selector">
<!-- 日模式 -->
<picker v-if="activeTab === 'day'" mode="date" :value="startDate" @change="handleDateChange">
<view class="date-input">
<text class="date-label">日期</text>
<text class="date-value">{{ startDate }}</text>
</view>
</picker>
<!-- 月模式 -->
<view v-else-if="activeTab === 'month'" class="date-range-group">
<picker mode="date" fields="month" :value="startDate" @change="handleStartMonthChange">
<view class="date-input">
<text class="date-label"></text>
<text class="date-value">{{ startDate }}</text>
</view>
</picker>
<view class="date-separator"></view>
<picker mode="date" fields="month" :value="endDate" :start="startDate" :end="maxMonthEnd" @change="handleEndMonthChange">
<view class="date-input">
<text class="date-label"></text>
<text class="date-value">{{ endDate }}</text>
</view>
</picker>
</view>
<!-- 年模式 -->
<view v-else class="date-range-group">
<picker mode="date" fields="year" :value="startDate" @change="handleStartYearChange">
<view class="date-input">
<text class="date-label"></text>
<text class="date-value">{{ startDate }}</text>
</view>
</picker>
<view class="date-separator"></view>
<picker mode="date" fields="year" :value="endDate" @change="handleEndYearChange">
<view class="date-input">
<text class="date-label"></text>
<text class="date-value">{{ endDate }}</text>
</view>
</picker>
</view>
</view>
<!-- 停机汇总 -->
<view class="summary-section">
<view class="section-header">
<text class="section-title">停机汇总</text>
<text class="section-date">{{ displayDateRange }}</text>
</view>
<view class="summary-grid">
<view class="summary-card" v-for="(item, index) in summaryData" :key="index">
<text class="summary-label">{{ item.label }}</text>
<view class="summary-value-box">
<text class="summary-value">{{ item.value }}</text>
<text v-if="item.unit" class="summary-unit">{{ item.unit }}</text>
</view>
</view>
</view>
</view>
<!-- 停机趋势图/年视图 -->
<view class="chart-section" v-if="activeTab !== 'day' && trendChartData.categories && trendChartData.categories.length > 0">
<view class="section-header">
<text class="section-title">停机趋势</text>
</view>
<view class="chart-wrapper trend-chart">
<qiun-data-charts type="mix" :chartData="trendChartData" :opts="trendChartOpts"/>
</view>
</view>
<!-- 停机分布 - 班组 -->
<view class="chart-section">
<view class="section-header">
<text class="section-title">班组停机分布</text>
</view>
<view class="pie-chart-single" v-if="crewPieData.series[0].data.length > 0">
<qiun-data-charts type="pie" :chartData="crewPieData" :opts="pieChartOpts"/>
</view>
<view class="empty-chart" v-else>
<text class="empty-icon">📊</text>
<text class="empty-text">此时间段未发生停机</text>
</view>
</view>
<!-- 停机分布 - 类型 -->
<view class="chart-section">
<view class="section-header">
<text class="section-title">停机类型分布</text>
</view>
<view class="pie-chart-single" v-if="typePieData.series[0].data.length > 0">
<qiun-data-charts type="pie" :chartData="typePieData" :opts="pieChartOpts"/>
</view>
<view class="empty-chart" v-else>
<text class="empty-icon">📊</text>
<text class="empty-text">此时间段未发生停机</text>
</view>
</view>
<!-- 停机详细列表日视图 -->
<view class="detail-section" v-if="activeTab === 'day'">
<view class="section-header">
<text class="section-title">停机详情</text>
</view>
<view class="detail-list">
<view class="detail-item" v-for="(item, index) in tableData" :key="index">
<view class="detail-header">
<text class="detail-time">{{ item.time }}</text>
<text class="detail-duration">{{ item.duration }}</text>
</view>
<view class="detail-info">
<text class="detail-label">机组</text>
<text class="detail-text">{{ item.machine }}</text>
</view>
<view class="detail-info">
<text class="detail-label">备注</text>
<text class="detail-text">{{ item.remark }}</text>
</view>
</view>
<view v-if="tableData.length === 0" class="empty-state">
<text class="empty-text">暂无停机记录</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { listStoppage } from '@/api/pocket/plantState'
// 2. 独立工具函数避免data初始化时调用this.methods的问题
/**
* 获取默认日期(根据视图类型)
* @param {string} type - 视图类型day/month/year
* @returns {string} 格式化后的日期
*/
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); // 减1个月
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
return `${year}-${month}`;
}
/**
* 格式化日期
* @param {Date} date - 日期对象
* @param {string} type - 视图类型day/month/year
* @returns {string} 格式化后的日期
*/
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 {
// 4. 响应式数据(替代 Vue3 的 ref
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: "%" }
],
// 停机趋势图(月/年视图)
trendChartData: {
categories: [],
series: []
},
trendChartOpts: {
color: ["#0066cc", "#f56c6c"],
padding: [15, 15, 0, 15],
enableScroll: false,
legend: { position: "top" },
xAxis: { disableGrid: true },
yAxis: {
gridType: "dash",
dashLength: 4,
gridColor: "#e4e7ed",
data: [
{ position: "left", title: "停机时间(min)" },
{ position: "right", title: "作业率(%)" }
]
},
extra: {
mix: {
column: { width: 20 }
}
}
},
// 饼图配置
pieChartOpts: {
color: ["#0066cc", "#409eff", "#66b1ff", "#a0cfff", "#d9ecff"],
padding: [15, 15, 15, 15],
enableScroll: false,
legend: {
show: true,
position: "bottom",
lineHeight: 16,
fontSize: 10,
fontColor: "#666",
margin: 5,
itemGap: 8
},
extra: {
pie: {
activeOpacity: 0.5,
activeRadius: 10,
labelWidth: 15,
border: false,
ringWidth: 0,
offsetAngle: 0,
disablePieStroke: true
}
}
},
// 班组停机分布
crewPieData: {
series: [{ data: [] }]
},
// 停机类型分布
typePieData: {
series: [{ data: [] }]
},
// 停机详细列表
tableData: []
};
},
// 5. 计算属性(替代 Vue3 的 computed 函数)
computed: {
// 月模式:最大结束月份(开始月份+1年
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 "";
}
}
},
// 6. 方法定义(所有交互逻辑放这里)
methods: {
// 切换视图(日/月/年)
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(e) {
this.startDate = e.detail.value;
this.endDate = e.detail.value; // 日模式首尾日期一致
this.loadStoppageData();
},
// 月模式:开始月份变更
handleStartMonthChange(e) {
this.startDate = e.detail.value;
// 自动调整结束月份:不超过开始月份+1年
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(e) {
this.endDate = e.detail.value;
this.loadStoppageData();
},
// 年模式:开始年份变更
handleStartYearChange(e) {
this.startDate = e.detail.value;
this.loadStoppageData();
},
// 年模式:结束年份变更
handleEndYearChange(e) {
this.endDate = e.detail.value;
this.loadStoppageData();
},
// 加载停机数据
loadStoppageData() {
uni.showLoading({ title: '加载中' })
// 转换为完整日期格式
const start = this.formatFullDate(this.startDate, true)
const end = this.formatFullDate(this.endDate, false)
const queryParams = {
pageNum: 1,
pageSize: 100,
startDate: start,
endDate: end
}
console.log('停机查询参数:', queryParams)
listStoppage(queryParams).then(response => {
uni.hideLoading()
console.log('停机统计响应:', response)
if (response.code === 200 && response.rows && response.rows.length > 0) {
// 处理停机数据
console.log('停机数据:', response.rows)
// 更新表格数据(日视图)
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 = {
series: [{
data: Object.keys(crewMap).map(crew => ({
name: crew,
value: crewMap[crew]
}))
}]
}
this.typePieData = {
series: [{
data: Object.keys(typeMap).map(type => ({
name: type,
value: typeMap[type]
}))
}]
}
// 如果是月/年视图,构建趋势图
if (this.activeTab !== 'day' && response.rows.length > 0) {
this.buildTrendChart(response.rows)
}
} else {
// 没有数据时使用默认值
console.log('暂无停机数据')
// 清空数据
this.tableData = []
this.summaryData = [
{ label: '停机时间', value: 0, unit: 'min' },
{ label: '停机次数', value: 0, unit: '次' },
{ label: '作业率', value: 100, unit: '%' }
]
this.crewPieData = { series: [{ data: [] }] }
this.typePieData = { series: [{ data: [] }] }
// 清空趋势图
this.trendChartData = {
categories: [],
series: []
}
}
}).catch(error => {
uni.hideLoading()
console.error('加载停机数据失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
})
},
// 格式化日期时间
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 // 包含结束日期
// 每天1440分钟24小时
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)
},
// 构建趋势图(月/年视图)
buildTrendChart(stoppageData) {
if (!stoppageData || stoppageData.length === 0) {
console.log('无法构建趋势图:数据为空')
this.trendChartData = {
categories: [],
series: []
}
return
}
// 按日期分组统计
const dateMap = {}
stoppageData.forEach(item => {
if (!item.startDate) return
const date = this.formatDate(item.startDate)
if (!dateMap[date]) {
dateMap[date] = { duration: 0, count: 0 }
}
const durationMinutes = this.secondsToMinutes(item.duration)
dateMap[date].duration += durationMinutes
dateMap[date].count += 1
})
const categories = Object.keys(dateMap).sort()
if (categories.length === 0) {
console.log('无法构建趋势图:无有效日期')
this.trendChartData = {
categories: [],
series: []
}
return
}
const durationData = []
const rateData = []
categories.forEach(date => {
durationData.push(dateMap[date].duration)
// 趋势图中每个点是单天的作业率所以总时间是1440分钟
const rate = this.calculateWorkRate(dateMap[date].duration, 1440)
rateData.push(Number(rate))
})
console.log('趋势图数据构建成功:', { categories, durationData, rateData })
this.trendChartData = {
categories: categories,
series: [
{
name: "停机时间",
type: "column",
index: 0,
data: durationData
},
{
name: "作业率",
type: "line",
index: 1,
style: "curve",
data: rateData
}
]
}
},
// 格式化日期(简短格式)
formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
const month = date.getMonth() + 1
const day = date.getDate()
return `${month}/${day}`
},
// 秒转分钟(保留整数)
secondsToMinutes(seconds) {
if (!seconds || seconds === 0) return 0
return Math.round(Number(seconds) / 60)
},
// 格式化为完整日期(用于查询)
formatFullDate(dateStr, isStart) {
if (!dateStr) return ''
// 如果已经是完整日期格式 yyyy-MM-dd直接返回
if (dateStr.length === 10) {
return dateStr
}
// 月份格式 yyyy-MM
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')}`
}
}
// 年份格式 yyyy
if (dateStr.length === 4) {
if (isStart) {
return `${dateStr}-01-01`
} else {
return `${dateStr}-12-31`
}
}
return dateStr
}
},
// 生命周期钩子
mounted() {
this.loadStoppageData()
}
};
</script>
<style scoped lang="scss">
/* 页面容器 */
.page-container {
background: #f5f7fa;
padding: 24rpx;
}
/* 时间维度切换 */
.time-tab-bar {
display: flex;
background: #fff;
border-radius: 8rpx;
padding: 8rpx;
margin-bottom: 24rpx;
border: 1rpx solid #e4e7ed;
}
.time-tab-item {
flex: 1;
text-align: center;
padding: 16rpx 0;
font-size: 26rpx;
color: #606266;
border-radius: 6rpx;
transition: all 0.2s;
}
.time-tab-active {
background: #0066cc;
color: #fff;
font-weight: 500;
}
/* 日期选择区 */
.date-selector {
background: #fff;
border-radius: 8rpx;
padding: 24rpx;
margin-bottom: 24rpx;
border: 1rpx solid #e4e7ed;
}
.date-range-group {
display: flex;
align-items: stretch;
gap: 0;
}
.date-input {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
background: #f5f7fa;
border: 1rpx solid #e4e7ed;
&:first-child {
border-radius: 6rpx 0 0 6rpx;
border-right: none;
}
&:last-child {
border-radius: 0 6rpx 6rpx 0;
border-left: none;
}
}
.date-label {
font-size: 26rpx;
color: #909399;
margin-right: 16rpx;
}
.date-value {
font-size: 28rpx;
color: #303133;
font-weight: 500;
flex: 1;
text-align: right;
}
.date-separator {
font-size: 28rpx;
color: #fff;
background: #0066cc;
padding: 24rpx 20rpx;
display: flex;
align-items: center;
justify-content: center;
min-width: 80rpx;
}
/* 区块样式 */
.summary-section,
.chart-section,
.detail-section {
margin-bottom: 24rpx;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
padding-left: 16rpx;
border-left: 4rpx solid #0066cc;
}
.section-title {
font-size: 30rpx;
font-weight: 500;
color: #303133;
}
.section-date {
font-size: 24rpx;
color: #909399;
}
/* 汇总卡片网格 */
.summary-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
}
.summary-card {
background: #fff;
border: 1rpx solid #e4e7ed;
border-radius: 8rpx;
padding: 28rpx 20rpx;
text-align: center;
}
.summary-label {
display: block;
font-size: 24rpx;
color: #909399;
margin-bottom: 16rpx;
}
.summary-value-box {
display: flex;
align-items: baseline;
justify-content: center;
}
.summary-value {
font-size: 40rpx;
font-weight: 600;
color: #0066cc;
line-height: 1;
}
.summary-unit {
font-size: 22rpx;
color: #909399;
margin-left: 6rpx;
}
/* 图表容器 */
.chart-wrapper {
background: #fff;
border: 1rpx solid #e4e7ed;
border-radius: 8rpx;
padding: 24rpx 16rpx;
min-height: 450rpx;
}
/* 趋势图固定高度 */
.trend-chart {
height: 500rpx;
min-height: 500rpx;
}
/* 单个饼图容器 */
.pie-chart-single {
background: #fff;
border: 1rpx solid #e4e7ed;
border-radius: 8rpx;
padding: 32rpx 0;
min-height: 480rpx;
}
/* 空状态图表占位 */
.empty-chart {
background: #fff;
border: 1rpx solid #e4e7ed;
border-radius: 8rpx;
padding: 100rpx 0;
min-height: 480rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20rpx;
}
.empty-icon {
font-size: 80rpx;
opacity: 0.3;
}
.empty-text {
font-size: 28rpx;
color: #909399;
}
/* 停机详情列表 */
.detail-list {
background: #fff;
border-radius: 8rpx;
border: 1rpx solid #e4e7ed;
overflow: hidden;
}
.detail-item {
padding: 24rpx;
border-bottom: 1rpx solid #f5f7fa;
&:last-child {
border-bottom: none;
}
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.detail-time {
font-size: 26rpx;
color: #606266;
}
.detail-duration {
font-size: 28rpx;
color: #0066cc;
font-weight: 600;
}
.detail-info {
display: flex;
align-items: baseline;
margin-bottom: 8rpx;
&:last-child {
margin-bottom: 0;
}
}
.detail-label {
font-size: 24rpx;
color: #909399;
min-width: 120rpx;
}
.detail-text {
font-size: 26rpx;
color: #303133;
flex: 1;
}
/* 空状态 */
.empty-state {
padding: 100rpx 0;
text-align: center;
}
.empty-text {
font-size: 28rpx;
color: #909399;
}
</style>