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

688 lines
17 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">
<view class="section-header">
<text class="section-title">产量趋势</text>
</view>
<view class="chart-wrapper">
<qiun-data-charts type="mix" :chartData="productionChartData" :opts="productionChartOpts"/>
</view>
</view>
<!-- 班组产量对比 -->
<view class="chart-section">
<view class="section-header">
<text class="section-title">班组产量对比</text>
</view>
<view class="chart-wrapper">
<qiun-data-charts type="column" :chartData="crewChartData" :opts="columnChartOpts"/>
</view>
</view>
<!-- 规格分布 -->
<view class="chart-section">
<view class="section-header">
<text class="section-title">规格分布</text>
</view>
<view class="pie-charts-row">
<view class="pie-chart-item">
<text class="pie-title">厚度分布</text>
<qiun-data-charts type="pie" :chartData="thicknessPieData" :opts="pieChartOpts"/>
</view>
</view>
<view class="pie-charts-row">
<view class="pie-chart-item">
<text class="pie-title">宽度分布</text>
<qiun-data-charts type="pie" :chartData="widthPieData" :opts="pieChartOpts"/>
</view>
</view>
</view>
</view>
</template>
<script>
import {
getProductionSummary,
getCrewProduction,
getThicknessDistribution,
getWidthDistribution
} from '@/api/pocket/plantState'
// 工具函数
function getDefaultDate(type = "day") {
const date = new Date();
return formatDate(date, type);
}
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 {
data() {
return {
activeTab: "day",
startDate: getDefaultDate(),
endDate: getDefaultDate(),
timeTabs: [
{ label: "日", value: "day" },
{ label: "月", value: "month" },
{ label: "年", value: "year" }
],
// 汇总数据
summaryData: [
{ label: "生产钢卷数", value: 0, unit: "卷" },
{ label: "平均宽度", value: 0, unit: "mm" },
{ label: "平均厚度", value: 0, unit: "mm" },
{ label: "原料总量", value: 0, unit: "t" },
{ label: "成品总量", value: 0, unit: "t" },
{ label: "成材率", value: 0, unit: "%" }
],
// 产量趋势图(柱状+折线)
productionChartData: {},
productionChartOpts: {
color: ["#0066cc", "#00b96b"],
padding: [15, 15, 0, 15],
enableScroll: false,
legend: { position: "top" },
xAxis: { disableGrid: true },
yAxis: {
gridType: "dash",
dashLength: 4,
gridColor: "#e4e7ed",
data: [
{ position: "left", title: "产量(t)" },
{ position: "right", title: "成材率(%)" }
]
},
extra: {
mix: {
column: { width: 20 }
}
}
},
// 班组产量对比
crewChartData: {},
columnChartOpts: {
color: ["#0066cc", "#409eff", "#66b1ff"],
padding: [15, 15, 0, 15],
enableScroll: false,
legend: {},
xAxis: { disableGrid: true },
yAxis: {
gridType: "dash",
dashLength: 4,
gridColor: "#e4e7ed",
data: [{ title: "产量(t)" }]
}
},
// 饼图配置
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,
linearType: "none",
customRadius: 0
}
}
},
thicknessPieData: {
series: [{ data: [] }]
},
widthPieData: {
series: [{ data: [] }]
}
};
},
// 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 "";
}
}
},
mounted() {
this.loadProductionData();
},
methods: {
// 切换时间维度
handleTabChange(tab) {
this.activeTab = tab;
const defaultDate = getDefaultDate();
this.startDate = defaultDate;
this.endDate = tab === "day" ? defaultDate : getDefaultDate(tab);
this.loadProductionData();
},
// 日期选择变更
handleDateChange(e) {
this.startDate = e.detail.value;
this.endDate = e.detail.value;
this.loadProductionData();
},
handleStartMonthChange(e) {
this.startDate = e.detail.value;
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.loadProductionData();
},
handleEndMonthChange(e) {
this.endDate = e.detail.value;
this.loadProductionData();
},
handleStartYearChange(e) {
this.startDate = e.detail.value;
this.loadProductionData();
},
handleEndYearChange(e) {
this.endDate = e.detail.value;
this.loadProductionData();
},
// 加载生产数据
loadProductionData() {
uni.showLoading({ title: '加载中' })
// 并行请求所有数据
Promise.all([
this.loadSummaryData(),
this.loadCrewProductionData(),
this.loadSpecDistribution()
]).finally(() => {
uni.hideLoading()
})
},
// 加载汇总数据
loadSummaryData() {
// 转换为完整日期格式
const start = this.formatFullDate(this.startDate, true)
const end = this.formatFullDate(this.endDate, false)
return getProductionSummary(start, end).then(response => {
if (response.code === 200 && response.data) {
const data = response.data
this.summaryData = [
{ label: "生产钢卷数", value: data.coilCount || 0, unit: "卷" },
{ label: "平均宽度", value: data.avgWidth || 0, unit: "mm" },
{ label: "平均厚度", value: data.avgThick || 0, unit: "mm" },
{ label: "原料总量", value: data.totalEntryWeight || 0, unit: "t" },
{ label: "成品总量", value: data.totalExitWeight || 0, unit: "t" },
{ label: "成材率", value: data.yieldRate || 0, unit: "%" }
]
}
}).catch(error => {
console.error('加载汇总数据失败:', error)
})
},
// 加载班组产量数据
loadCrewProductionData() {
const start = this.formatFullDate(this.startDate, true)
const end = this.formatFullDate(this.endDate, false)
return getCrewProduction(start, end).then(response => {
if (response.code === 200 && response.data && response.data.length > 0) {
const crewData = response.data
// 按班组和班次分组
const categories = []
const crewMap = {}
crewData.forEach(item => {
const label = `${item.crew}-${item.shift}`
categories.push(label)
if (!crewMap[item.shift]) {
crewMap[item.shift] = []
}
crewMap[item.shift].push(Number(item.totalWeight) || 0)
})
// 构建班组产量对比图
const series = Object.keys(crewMap).map(shift => ({
name: shift,
data: crewMap[shift]
}))
this.crewChartData = {
categories: categories,
series: series
}
// 构建产量趋势图(使用班组数据)
this.productionChartData = {
categories: categories,
series: [
{
name: "产量",
type: "column",
index: 0,
data: crewData.map(item => Number(item.totalWeight) || 0)
},
{
name: "平均厚度",
type: "line",
index: 1,
style: "curve",
data: crewData.map(item => Number(item.avgThick) || 0)
}
]
}
}
}).catch(error => {
console.error('加载班组产量失败:', error)
})
},
// 加载规格分布
loadSpecDistribution() {
const start = this.formatFullDate(this.startDate, true)
const end = this.formatFullDate(this.endDate, false)
return Promise.all([
getThicknessDistribution(start, end),
getWidthDistribution(start, end)
]).then(([thicknessRes, widthRes]) => {
// 厚度分布
if (thicknessRes.code === 200 && thicknessRes.data) {
this.thicknessPieData = {
series: [{
data: thicknessRes.data.map(item => ({
name: item.category,
value: item.count
}))
}]
}
}
// 宽度分布
if (widthRes.code === 200 && widthRes.data) {
this.widthPieData = {
series: [{
data: widthRes.data.map(item => ({
name: item.category,
value: item.count
}))
}]
}
}
}).catch(error => {
console.error('加载规格分布失败:', error)
})
},
// 格式化为完整日期(用于查询)
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
}
}
};
</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 {
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;
}
/* 饼图行布局 */
.pie-charts-row {
display: flex;
gap: 16rpx;
}
.pie-chart-item {
flex: 1;
background: #fff;
border: 1rpx solid #e4e7ed;
border-radius: 8rpx;
padding: 24rpx 0 32rpx 0;
min-height: 520rpx;
display: flex;
flex-direction: column;
}
.pie-title {
display: block;
text-align: center;
font-size: 26rpx;
color: #606266;
font-weight: 500;
margin-bottom: 16rpx;
padding: 0 16rpx;
flex-shrink: 0;
}
.pie-chart-wrapper {
flex: 1;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>