607 lines
18 KiB
Vue
607 lines
18 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<!-- 搜索工作栏 -->
|
||
<SearchBar :formConfigs="searchBarFormConfig" ref="search-bar" @headBtnClick="handleSearchBarBtnClick" />
|
||
|
||
<!-- 列表 -->
|
||
<base-table :table-props="tableProps" :page="queryParams.pageNo" :limit="queryParams.pageSize" :table-data="list"
|
||
@emitFun="handleEmitFun">
|
||
<method-btn v-if="tableBtn.length" slot="handleBtn" label="操作" :width="120" :method-list="tableBtn"
|
||
@clickBtn="handleTableBtnClick" />
|
||
</base-table>
|
||
|
||
<!-- 分页组件 -->
|
||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
|
||
@pagination="getList" />
|
||
<div v-if="chartData" class="charts-container">
|
||
<div v-for="(lineData, lineName) in chartData" :key="lineName" class="chart-wrapper">
|
||
<div class="blue-block"></div>
|
||
<h3 class="chart-title">{{ lineName }}</h3>
|
||
<div :id="`chart-${lineName}`" class="chart" style="width: 100%; height: 300px;"></div>
|
||
</div>
|
||
</div>
|
||
<defect-summary-det ref="defectSummaryDetRef" v-if="defectVis" />
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import moment from 'moment';
|
||
|
||
import {
|
||
getPdList,
|
||
} from '@/api/core/monitoring/auto';
|
||
import { getFactoryPage } from '@/api/core/base/factory';
|
||
import { getDefectSummaryTable, getDefectSummaryChart } from '@/api/monitoring/defectSummary';
|
||
import * as echarts from 'echarts';
|
||
import basicPageMixin from '@/mixins/lb/basicPageMixin';
|
||
import defectSummaryDet from './defectSummaryDet.vue'
|
||
export default {
|
||
name: 'QualityInspectionType',
|
||
mixins: [basicPageMixin],
|
||
components: {
|
||
defectSummaryDet
|
||
},
|
||
data() {
|
||
return {
|
||
tableBtn: [
|
||
this.$auth.hasPermi('base:quality-inspection-type:update')
|
||
? {
|
||
type: 'detail',
|
||
btnName: '缺陷详情',
|
||
}
|
||
: undefined,
|
||
// this.$auth.hasPermi('base:quality-inspection-type:delete')
|
||
// ? {
|
||
// type: 'delete',
|
||
// btnName: '删除',
|
||
// }
|
||
// : undefined,
|
||
].filter((v) => v),
|
||
tableProps: [
|
||
// {
|
||
// prop: 'createTime',
|
||
// label: '添加时间',
|
||
// fixed: true,
|
||
// width: 180,
|
||
// filter: (val) => moment(val).format('yyyy-MM-DD HH:mm:ss'),
|
||
// },
|
||
{ prop: 'factoryName', label: '工厂' },
|
||
{ prop: 'lineName', label: '产线' },
|
||
{ prop: 'glassNum', label: '玻璃总数' },
|
||
{
|
||
prop: 'okNum',
|
||
label: '合格品数',
|
||
},
|
||
{ prop: 'okRate', label: '合格百分比' },
|
||
{ prop: 'repairNum', label: '返修品数' },
|
||
{ prop: 'repairRate', label: '返修百分比' },
|
||
{ prop: 'ngNum', label: '废片数' },
|
||
{ prop: 'ngRate', label: '废片百分比' },
|
||
{ prop: 'oneNgNum', label: '1类缺陷玻璃数量' },
|
||
{ prop: 'twoNgNum', label: '2类缺陷玻璃数量' },
|
||
{ prop: 'threeNgNum', label: '3类缺陷玻璃数量' },
|
||
|
||
|
||
|
||
|
||
// {
|
||
// label: '操作',
|
||
// alignt: 'center',
|
||
// subcomponent: {
|
||
// render: function (h) {
|
||
// return h('div', null, [
|
||
// h(
|
||
// 'el-button',
|
||
// {
|
||
// props: {
|
||
// icon: 'el-icon-edit',
|
||
// size: 'mini',
|
||
// type: 'text',
|
||
// },
|
||
// },
|
||
// ' 修改'
|
||
// ),
|
||
// h(
|
||
// 'el-button',
|
||
// {
|
||
// props: {
|
||
// icon: 'el-icon-edit',
|
||
// size: 'mini',
|
||
// type: 'text',
|
||
// },
|
||
// },
|
||
// ' 修改'
|
||
// ),
|
||
// ]);
|
||
// },
|
||
// },
|
||
// },
|
||
],
|
||
//
|
||
searchBarFormConfig: [
|
||
{
|
||
type: 'select',
|
||
label: '工厂',
|
||
selectOptions: [],
|
||
param: 'factoryId',
|
||
},
|
||
{
|
||
type: 'select',
|
||
label: '产线',
|
||
selectOptions: [],
|
||
multiple: true,
|
||
param: 'lineId',
|
||
},
|
||
// {
|
||
// type: 'input',
|
||
// label: '缺陷数>',
|
||
// // selectOptions: [],
|
||
// param: 'number',
|
||
// },
|
||
{
|
||
type: 'datePicker',
|
||
label: '时间范围',
|
||
dateType: 'datetimerange',
|
||
format: 'yyyy-MM-dd HH:mm:ss',
|
||
// valueFormat: 'yyyy-MM-dd HH:mm:ss',
|
||
valueFormat: 'timestamp',
|
||
defaultTime: ['00:00:00', '23:59:59'],
|
||
rangeSeparator: '-',
|
||
startPlaceholder: '开始时间',
|
||
endPlaceholder: '结束时间',
|
||
param: 'timeVal',
|
||
width: 350,
|
||
defaultSelect: [],
|
||
},
|
||
{
|
||
type: 'button',
|
||
btnName: '查询',
|
||
name: 'search',
|
||
color: 'primary',
|
||
},
|
||
// {
|
||
// type: 'button',
|
||
// btnName: '重置',
|
||
// name: 'reset',
|
||
// },
|
||
{
|
||
type: 'separate',
|
||
},
|
||
// {
|
||
// type: this.$auth.hasPermi('base:quality-inspection-type:create')
|
||
// ? 'button'
|
||
// : '',
|
||
// btnName: '新增',
|
||
// name: 'add',
|
||
// plain: true,
|
||
// color: 'success',
|
||
// },
|
||
{
|
||
type: this.$auth.hasPermi('base:quality-inspection-type:export')
|
||
? 'button'
|
||
: '',
|
||
btnName: '导出',
|
||
name: 'export',
|
||
color: 'warning',
|
||
},
|
||
],
|
||
// 表单配置
|
||
// formRows: [
|
||
// [
|
||
// {
|
||
// input: true,
|
||
// label: '检测类型名称',
|
||
// prop: 'name',
|
||
// rules: [{ required: true, message: '不能为空', trigger: 'blur' }],
|
||
// // bind: {
|
||
// // disabled: true, // some condition, like detail mode...
|
||
// // }
|
||
// },
|
||
// ],
|
||
// [{ input: true, label: '检测类型编码', prop: 'code' }],
|
||
// [{ input: true, label: '备注', prop: 'remark' }],
|
||
// ],
|
||
// 是否显示弹出层
|
||
open: false,
|
||
chartData: {},
|
||
charts: {},
|
||
defectVis:false,
|
||
// 查询参数
|
||
queryParams: {
|
||
pageNo: 1,
|
||
pageSize: 10,
|
||
lineId: undefined,
|
||
factoryId: undefined,
|
||
checkNum: undefined,
|
||
startTime: undefined,
|
||
},
|
||
// 表单参数
|
||
form: {},
|
||
};
|
||
},
|
||
// watch: {
|
||
// form: {
|
||
// handler: (val) => {
|
||
// console.log('form changed', val);
|
||
// },
|
||
// deep: true
|
||
// },
|
||
// },
|
||
mounted() {
|
||
|
||
const { startTimestamp, endTimestamp } = this.getThreeDaysAgoThisTimeToNowTimeStamps();
|
||
// 找到时间范围的配置项并赋值(对应你代码中的timeVal参数)
|
||
this.searchBarFormConfig[2].defaultSelect = [startTimestamp, endTimestamp]; // 赋值给日期选择器
|
||
this.queryParams.startTime = startTimestamp;
|
||
this.queryParams.endTime = endTimestamp;
|
||
this.getList();
|
||
this.getDict()
|
||
},
|
||
methods: {
|
||
getThreeDaysAgoThisTimeToNowTimeStamps() {
|
||
const now = new Date();
|
||
// 1. 计算三天前的当前时刻(使用setDate直接修改日期,保留时分秒等信息)
|
||
const threeDaysAgoThisTime = new Date(now); // 复制当前日期对象,避免修改原对象
|
||
threeDaysAgoThisTime.setDate(threeDaysAgoThisTime.getDate() - 3); // 日期减3天,时分秒保持和当前一致
|
||
|
||
// 2. 获取时间戳(毫秒级和秒级)
|
||
// 开始时间戳:三天前的当前时刻
|
||
const startTimestamp = threeDaysAgoThisTime.getTime(); // 毫秒级
|
||
const startTimestampSec = Math.floor(startTimestamp / 1000); // 秒级
|
||
// 结束时间戳:当前时刻
|
||
const endTimestamp = now.getTime(); // 毫秒级
|
||
const endTimestampSec = Math.floor(endTimestamp / 1000); // 秒级
|
||
|
||
// 封装日期格式化函数,转换为yyyy-MM-dd HH:mm:ss格式
|
||
const formatDateTime = (date) => {
|
||
const y = date.getFullYear();
|
||
// 月份是从0开始的,所以要+1;补零确保是两位
|
||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||
const d = String(date.getDate()).padStart(2, '0');
|
||
const h = String(date.getHours()).padStart(2, '0');
|
||
const min = String(date.getMinutes()).padStart(2, '0');
|
||
const s = String(date.getSeconds()).padStart(2, '0');
|
||
return `${y}-${m}-${d} ${h}:${min}:${s}`;
|
||
};
|
||
|
||
// 格式化后的字符串:三天前的当前时刻 和 当前时刻
|
||
const startDateTimeStr = formatDateTime(threeDaysAgoThisTime);
|
||
const endDateTimeStr = formatDateTime(now);
|
||
|
||
return {
|
||
startTimestamp, // 三天前当前时刻的毫秒级时间戳
|
||
endTimestamp, // 当前时刻的毫秒级时间戳
|
||
startTimestampSec, // 三天前当前时刻的秒级时间戳
|
||
endTimestampSec, // 当前时刻的秒级时间戳
|
||
startDateTimeStr, // yyyy-MM-dd HH:mm:ss格式的开始时间字符串
|
||
endDateTimeStr // yyyy-MM-dd HH:mm:ss格式的结束时间字符串
|
||
};
|
||
},
|
||
getDict() {
|
||
getPdList().then(res => {
|
||
this.searchBarFormConfig[1].selectOptions = res.data || [];
|
||
});
|
||
|
||
getFactoryPage({ pageSize: 100, pageNo: 1 }).then(res => {
|
||
this.searchBarFormConfig[0].selectOptions = res.data.list || [];
|
||
});
|
||
},
|
||
/** base table related */
|
||
handleTableBtnClick({ data, type }) {
|
||
switch (type) {
|
||
case 'detail':
|
||
this.defectVis = true
|
||
this.$nextTick(() => {
|
||
this.$refs.defectSummaryDetRef.init({
|
||
factoryId:data.factoryId,
|
||
lineId: [data.lineId],
|
||
factoryName: data.factoryName,
|
||
lineName: data.lineName,
|
||
startTime: this.queryParams.startTime,
|
||
endTime: this.queryParams.endTime,
|
||
|
||
})
|
||
})
|
||
break;
|
||
case 'delete':
|
||
this.handleDelete(data);
|
||
break;
|
||
}
|
||
},
|
||
beforeDestroy() {
|
||
// 销毁所有 ECharts 实例
|
||
Object.values(this.charts).forEach(chart => {
|
||
if (chart && chart.dispose) {
|
||
chart.dispose();
|
||
}
|
||
});
|
||
this.charts = {};
|
||
},
|
||
/**
|
||
* 初始化单个图表
|
||
* @param {string} lineName - 产线名称
|
||
* @param {Array} lineData - 该产线的原始数据
|
||
*/
|
||
initSingleChart(lineName, lineData) {
|
||
// 如果已有实例,先销毁
|
||
if (this.charts[lineName]) {
|
||
this.charts[lineName].dispose();
|
||
}
|
||
|
||
const chartDom = document.getElementById(`chart-${lineName}`);
|
||
if (!chartDom || !lineData || lineData.length === 0) {
|
||
console.warn(`图表容器 chart-${lineName} 未找到或数据为空`);
|
||
return;
|
||
}
|
||
|
||
const myChart = echarts.init(chartDom);
|
||
|
||
// --- 动态处理数据逻辑 ---
|
||
const startTime = this.queryParams.startTime;
|
||
const endTime = this.queryParams.endTime;
|
||
|
||
if (!startTime || !endTime) {
|
||
console.error("起始时间或结束时间为空,无法计算时间差。");
|
||
return;
|
||
}
|
||
|
||
const durationDays = (endTime - startTime) / (1000 * 60 * 60 * 24);
|
||
const isHourly = durationDays < 7;
|
||
|
||
// 使用Map来保证顺序,并能存储聚合后的数据和原始显示标签
|
||
const aggregatedMap = new Map();
|
||
|
||
lineData.forEach(item => {
|
||
let groupKey; // 用于分组的键
|
||
let displayLabel; // 用于显示的标签
|
||
|
||
if (isHourly) {
|
||
// --- 方法一:使用字符串分割 ---
|
||
const timePart = item.timePoint.split('T')[1];
|
||
groupKey = timePart ? timePart : item.timePoint; // 安全处理
|
||
|
||
displayLabel = item.timePoint.replace('T', ' ');
|
||
} else {
|
||
// 按天聚合:分组键和显示标签都到天
|
||
groupKey = item.timePoint.split('T')[0];
|
||
displayLabel = groupKey;
|
||
}
|
||
|
||
if (!aggregatedMap.has(groupKey)) {
|
||
aggregatedMap.set(groupKey, {
|
||
displayLabel: displayLabel,
|
||
okNum: 0,
|
||
repairNum: 0,
|
||
ngNum: 0
|
||
});
|
||
}
|
||
|
||
const aggregatedItem = aggregatedMap.get(groupKey);
|
||
aggregatedItem.okNum += item.okNum || 0;
|
||
aggregatedItem.repairNum += item.repairNum || 0;
|
||
aggregatedItem.ngNum += item.ngNum || 0;
|
||
});
|
||
|
||
// 准备 ECharts 所需的数据格式
|
||
const xAxisData = [];
|
||
const okNumData = [];
|
||
const repairNumData = [];
|
||
const ngNumData = [];
|
||
|
||
aggregatedMap.forEach(item => {
|
||
xAxisData.push(item.displayLabel);
|
||
okNumData.push(item.okNum);
|
||
repairNumData.push(item.repairNum);
|
||
ngNumData.push(item.ngNum);
|
||
});
|
||
|
||
// --- ECharts 配置项 ---
|
||
const option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: { type: 'cross' },
|
||
},
|
||
legend: {
|
||
data: ['合格品数', '返修品数', '废片数'],
|
||
itemHeight: 2,
|
||
itemWidth: 20,
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '20%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: xAxisData,
|
||
axisLabel: {
|
||
rotate: 45,
|
||
interval: 0, // 强制显示所有标签
|
||
formatter: function (value) {
|
||
if (isHourly) {
|
||
// 从 "2025-10-30 00:00" 中提取 "00:00" 进行显示
|
||
return value.split(' ')[1];
|
||
} else {
|
||
// 天级别,显示 "MM-DD"
|
||
const dateParts = value.split('-');
|
||
return `${dateParts[1]}-${dateParts[2]}`;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
min: 0
|
||
},
|
||
series: [
|
||
{
|
||
name: '合格品数',
|
||
type: 'line',
|
||
showSymbol: false,
|
||
data: okNumData,
|
||
itemStyle: { color: 'rgba(40, 138, 255, 1)' },
|
||
},
|
||
{
|
||
name: '返修品数',
|
||
type: 'line',
|
||
showSymbol: false,
|
||
data: repairNumData,
|
||
itemStyle: { color: 'rgba(115, 222, 147, 1)' },
|
||
},
|
||
{
|
||
name: '废片数',
|
||
type: 'line',
|
||
showSymbol: false,
|
||
data: ngNumData,
|
||
itemStyle: { color: 'rgba(255, 206, 106, 1)' },
|
||
}
|
||
]
|
||
};
|
||
|
||
myChart.setOption(option);
|
||
this.charts[lineName] = myChart;
|
||
|
||
// 监听容器大小变化
|
||
const resizeObserver = new ResizeObserver(() => {
|
||
myChart.resize();
|
||
});
|
||
resizeObserver.observe(chartDom);
|
||
myChart._resizeObserver = resizeObserver;
|
||
},
|
||
|
||
/**
|
||
* 根据获取到的 chartData 更新所有图表
|
||
*/
|
||
updateAllCharts() {
|
||
if (!this.chartData) {
|
||
return;
|
||
}
|
||
|
||
// 使用 nextTick 确保 v-for 生成的 DOM 已存在
|
||
this.$nextTick(() => {
|
||
const lineNames = Object.keys(this.chartData);
|
||
lineNames.forEach(lineName => {
|
||
this.initSingleChart(lineName, this.chartData[lineName]);
|
||
});
|
||
});
|
||
},
|
||
/** search bar related */
|
||
handleSearchBarBtnClick(btn) {
|
||
switch (btn.btnName) {
|
||
case 'search':
|
||
this.queryParams.lineId = btn.lineId ? btn.lineId : undefined
|
||
this.queryParams.factoryId = btn.factoryId ? btn.factoryId : undefined
|
||
this.queryParams.startTime = btn.timeVal ? btn.timeVal[0] : undefined
|
||
this.queryParams.endTime = btn.timeVal ? btn.timeVal[1] : undefined
|
||
this.getList();
|
||
break;
|
||
case 'add':
|
||
this.handleAdd();
|
||
break;
|
||
case 'export':
|
||
this.handleExport();
|
||
break;
|
||
// case 'reset':
|
||
// this.$refs['search-bar'].resetForm();
|
||
// this.resetQuery();
|
||
// break;
|
||
}
|
||
},
|
||
/** 查询列表 */
|
||
getList() {
|
||
this.loading = true;
|
||
// 执行查询
|
||
getDefectSummaryTable(this.queryParams).then((response) => {
|
||
this.list = response.data;
|
||
// this.total = response.data.total;
|
||
this.loading = false;
|
||
})
|
||
getDefectSummaryChart(this.queryParams).then((res) => {
|
||
console.log('res.data', res.data);
|
||
|
||
this.chartData = res.data || {};
|
||
// 手动调用更新图表的方法
|
||
this.updateAllCharts();
|
||
// // this.total = response.data.total;
|
||
// this.loading = false;
|
||
})
|
||
},
|
||
/** 表单重置 */
|
||
reset() {
|
||
this.form = {
|
||
id: undefined,
|
||
name: undefined,
|
||
code: undefined,
|
||
remark: undefined,
|
||
};
|
||
this.resetForm('form');
|
||
},
|
||
/** 导出按钮操作 */
|
||
handleExport() {
|
||
// 处理查询参数
|
||
let tables = document.querySelector('.el-table').cloneNode(true)
|
||
const fix = tables.querySelector('.el-table__fixed')
|
||
const fixRight = tables.querySelector('.el-table__fixed-right')
|
||
if (fix) {
|
||
tables.removeChild(tables.querySelector('.el-table__fixed'))
|
||
}
|
||
if (fixRight) {
|
||
tables.removeChild(tables.querySelector('.el-table__fixed-right'))
|
||
}
|
||
let exportTable = XLSX.utils.table_to_book(tables)
|
||
|
||
var exportTableOut = XLSX.write(exportTable, {
|
||
bookType: 'xlsx', bookSST: true, type: 'array'
|
||
})
|
||
// sheetjs.xlsx为导出表格的标题名称
|
||
try {
|
||
FileSaver.saveAs(new Blob([exportTableOut], {
|
||
type: 'application/octet-stream'
|
||
}), '缺陷汇总.xlsx')
|
||
} catch (e) {
|
||
if (typeof console !== 'undefined') console.log(e, exportTableOut)
|
||
}
|
||
return exportTableOut
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
<style scoped>
|
||
.charts-container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.chart-wrapper {
|
||
flex: 1;
|
||
min-width: 300px;
|
||
margin-top: 10px;
|
||
/* 最小宽度,防止缩得太小 */
|
||
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
border-radius: 4px; */
|
||
/* padding: 10px; */
|
||
}
|
||
|
||
.blue-block {
|
||
display: inline-block;
|
||
float: left;
|
||
width: 4px;
|
||
height: 16px;
|
||
background-color: #0B58FF;
|
||
border-radius: 1px;
|
||
margin-right: 8px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.chart-title {
|
||
text-align: left;
|
||
color: #333;
|
||
font-size: 16px;
|
||
margin-bottom: 10px;
|
||
}
|
||
</style>
|