|
|
|
|
@ -0,0 +1,341 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="app-container">
|
|
|
|
|
<el-card shadow="never" class="mb-2">
|
|
|
|
|
<el-form :model="queryParams" inline label-width="90px">
|
|
|
|
|
<el-form-item label="开始日期">
|
|
|
|
|
<el-date-picker v-model="queryParams.beginDate" type="date" value-format="YYYY-MM-DD" placeholder="选择开始日期" :disabled-date="disableBeginDate" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="结束日期">
|
|
|
|
|
<el-date-picker v-model="queryParams.endDate" type="date" value-format="YYYY-MM-DD" placeholder="选择结束日期" :disabled-date="disableEndDate" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="工序">
|
|
|
|
|
<el-select v-model="queryParams.processId" filterable placeholder="请选择工序" style="width:220px">
|
|
|
|
|
<el-option v-for="p in processList" :key="p.processId" :label="p.processName" :value="p.processId" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="机台">
|
|
|
|
|
<el-select v-model="queryParams.machineId" filterable placeholder="请选择机台" style="width:220px" clearable>
|
|
|
|
|
<el-option v-for="m in machineList" :key="m.machineId" :label="m.machineName" :value="m.machineId" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="班次">
|
|
|
|
|
<el-select v-model="queryParams.shiftId" filterable placeholder="请选择班次" style="width:220px" clearable>
|
|
|
|
|
<el-option v-for="s in shiftList" :key="s.shiftId" :label="s.shiftName" :value="s.shiftId" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="班组">
|
|
|
|
|
<el-select v-model="queryParams.classTeamId" filterable placeholder="请选择班组" style="width:220px" clearable>
|
|
|
|
|
<el-option v-for="t in teamList" :key="t.classTeamId" :label="t.teamName" :value="t.classTeamId" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="物料">
|
|
|
|
|
<el-input v-model="queryParams.materialName" placeholder="输入物料名称模糊查询" style="width:220px" clearable />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item>
|
|
|
|
|
<el-button type="primary" @click="handleQuery">查询</el-button>
|
|
|
|
|
<el-button @click="resetQuery">重置</el-button>
|
|
|
|
|
<el-button type="success" @click="handleExportExcel">导出Excel</el-button>
|
|
|
|
|
<el-button @click="handleExportCsv">导出CSV</el-button>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="12" class="mb-2">
|
|
|
|
|
<el-col :span="24">
|
|
|
|
|
<el-card shadow="hover" class="h-100">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">小时产量趋势图(按完成时间小时分布)</div>
|
|
|
|
|
</template>
|
|
|
|
|
<div ref="trendChartRef" style="height: 360px"></div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="12">
|
|
|
|
|
<el-col :span="24">
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">小时产量统计分析表(按完成时间小时分布)</div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-table :data="pivotTableData" border height="520" :cell-style="cellStyle">
|
|
|
|
|
<el-table-column type="index" label="序号" width="70" align="center" />
|
|
|
|
|
<el-table-column prop="item" label="项目" width="150" />
|
|
|
|
|
<el-table-column prop="timeRange" label="时间段" width="220" />
|
|
|
|
|
<el-table-column prop="avg" label="平均" width="100">
|
|
|
|
|
<template #default="scope">{{ formatNumber(scope.row.avg) }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column v-for="d in dateCols" :key="d" :label="formatDateLabel(d)" :prop="d" min-width="90">
|
|
|
|
|
<template #default="scope">{{ formatNumber(scope.row[d]) }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, reactive, onMounted, onUnmounted, getCurrentInstance, watch } from 'vue';
|
|
|
|
|
import * as echarts from 'echarts';
|
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
import { hourlyOutputByHour } from '@/api/mes/prodReport';
|
|
|
|
|
import { getProcessInfoList } from '@/api/mes/baseProcessInfo';
|
|
|
|
|
import { getProdBaseMachineInfoList } from '@/api/mes/prodBaseMachineInfo';
|
|
|
|
|
import { getBaseShiftInfoList } from '@/api/mes/baseShiftInfo';
|
|
|
|
|
import { getBaseClassTeamInfoList } from '@/api/mes/baseClassTeamInfo';
|
|
|
|
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as any;
|
|
|
|
|
|
|
|
|
|
const queryParams = reactive({
|
|
|
|
|
beginDate: '',
|
|
|
|
|
endDate: '',
|
|
|
|
|
processId: undefined as number | undefined,
|
|
|
|
|
machineId: undefined as number | undefined,
|
|
|
|
|
shiftId: undefined as number | undefined,
|
|
|
|
|
classTeamId: undefined as number | undefined,
|
|
|
|
|
materialName: ''
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const processList = ref<any[]>([]);
|
|
|
|
|
const machineList = ref<any[]>([]);
|
|
|
|
|
const shiftList = ref<any[]>([]);
|
|
|
|
|
const teamList = ref<any[]>([]);
|
|
|
|
|
|
|
|
|
|
// 仅按实际完成时间(create_time)的小时进行统计展示
|
|
|
|
|
function pad2(n: number | string): string { return String(n).padStart(2, '0'); }
|
|
|
|
|
const hours = Array.from({ length: 24 }, (_, i) => pad2(i)); // ["00",...,"23"]
|
|
|
|
|
function hourRangeLabel(hh: string): string {
|
|
|
|
|
const next = pad2((Number(hh) + 1) % 24);
|
|
|
|
|
return `${hh}:00:00 ~ ${next}:00:00`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const trendChartRef = ref<HTMLDivElement | null>(null);
|
|
|
|
|
let trendChart: echarts.ECharts | null = null;
|
|
|
|
|
|
|
|
|
|
// 透视数据
|
|
|
|
|
const dateCols = ref<string[]>([]);
|
|
|
|
|
const pivotTableData = ref<any[]>([]);
|
|
|
|
|
let rawList: any[] = [];
|
|
|
|
|
|
|
|
|
|
function normalizeHour(h: string): string {
|
|
|
|
|
if (!h) return '';
|
|
|
|
|
const hh = h.includes(':') ? h.split(':')[0] : h;
|
|
|
|
|
return hh.padStart(2, '0');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildDateRange(begin: string, end: string) {
|
|
|
|
|
const res: string[] = [];
|
|
|
|
|
let cur = dayjs(begin).startOf('day');
|
|
|
|
|
const endD = dayjs(end).startOf('day');
|
|
|
|
|
while (cur.isBefore(endD) || cur.isSame(endD)) {
|
|
|
|
|
res.push(cur.format('YYYY-MM-DD'));
|
|
|
|
|
cur = cur.add(1, 'day');
|
|
|
|
|
}
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildPivot(list: any[]) {
|
|
|
|
|
const dates = buildDateRange(queryParams.beginDate, queryParams.endDate);
|
|
|
|
|
dateCols.value = dates;
|
|
|
|
|
const rows = hours.map((hh, idx) => ({
|
|
|
|
|
_seq: idx + 1,
|
|
|
|
|
item: `${hh}:00`,
|
|
|
|
|
timeRange: hourRangeLabel(hh),
|
|
|
|
|
key: hh,
|
|
|
|
|
avg: 0,
|
|
|
|
|
...Object.fromEntries(dates.map(d => [d, 0]))
|
|
|
|
|
}));
|
|
|
|
|
const mapByDateHour = new Map<string, number>();
|
|
|
|
|
list.forEach((it) => {
|
|
|
|
|
const d = it.productionDate; // YYYY-MM-DD
|
|
|
|
|
const hh = normalizeHour(it.hourSlot);
|
|
|
|
|
const qty = Number(it.productionQuantity || 0);
|
|
|
|
|
mapByDateHour.set(`${d}_${hh}`, (mapByDateHour.get(`${d}_${hh}`) || 0) + qty);
|
|
|
|
|
});
|
|
|
|
|
rows.forEach(row => {
|
|
|
|
|
dates.forEach(d => {
|
|
|
|
|
const qty = mapByDateHour.get(`${d}_${row.key}`) || 0;
|
|
|
|
|
row[d] = qty;
|
|
|
|
|
});
|
|
|
|
|
const values = dates.map(d => Number(row[d] || 0));
|
|
|
|
|
const sum = values.reduce((a, b) => a + b, 0);
|
|
|
|
|
row.avg = values.length ? sum / values.length : 0;
|
|
|
|
|
});
|
|
|
|
|
const totalRow: any = { item: '合计', timeRange: '', avg: 0 };
|
|
|
|
|
const averageRow: any = { item: '平均', timeRange: '', avg: 0 };
|
|
|
|
|
dates.forEach(d => {
|
|
|
|
|
const sum = rows.reduce((acc, r) => acc + Number(r[d] || 0), 0);
|
|
|
|
|
const avg = rows.length ? sum / rows.length : 0;
|
|
|
|
|
totalRow[d] = sum;
|
|
|
|
|
averageRow[d] = avg;
|
|
|
|
|
});
|
|
|
|
|
const sumOfRowAvg = rows.reduce((acc, r) => acc + Number(r.avg || 0), 0);
|
|
|
|
|
totalRow.avg = sumOfRowAvg;
|
|
|
|
|
averageRow.avg = dates.length ? (dates.reduce((a, d) => a + Number(totalRow[d] || 0), 0) / dates.length) : 0;
|
|
|
|
|
pivotTableData.value = [...rows, totalRow, averageRow];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateTrendChart() {
|
|
|
|
|
const dates = dateCols.value;
|
|
|
|
|
if (!trendChart && trendChartRef.value) trendChart = echarts.init(trendChartRef.value);
|
|
|
|
|
const series = hours.map(hh => ({
|
|
|
|
|
name: `${hh}:00`,
|
|
|
|
|
type: 'line',
|
|
|
|
|
smooth: true,
|
|
|
|
|
data: dates.map(d => {
|
|
|
|
|
const row = pivotTableData.value.find((r: any) => r.key === hh);
|
|
|
|
|
return row ? Number(row[d] || 0) : 0;
|
|
|
|
|
})
|
|
|
|
|
}));
|
|
|
|
|
trendChart?.setOption({
|
|
|
|
|
tooltip: { trigger: 'axis' },
|
|
|
|
|
legend: { type: 'scroll', top: 10 },
|
|
|
|
|
grid: { left: 40, right: 40, bottom: 30, containLabel: true },
|
|
|
|
|
xAxis: { type: 'category', data: dates.map(d => formatDateLabel(d)) },
|
|
|
|
|
yAxis: { type: 'value', name: '产量' },
|
|
|
|
|
series
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleQuery() {
|
|
|
|
|
if (!queryParams.processId || !queryParams.beginDate || !queryParams.endDate) {
|
|
|
|
|
proxy.$modal.msgWarning('请选择工序并设置时间范围');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 限制查询时间范围最多31天且结束日期不得早于开始日期
|
|
|
|
|
const begin = dayjs(queryParams.beginDate);
|
|
|
|
|
const end = dayjs(queryParams.endDate);
|
|
|
|
|
if (end.isBefore(begin)) {
|
|
|
|
|
proxy.$modal.msgWarning('结束日期不能早于开始日期');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (end.diff(begin, 'day') > 31) {
|
|
|
|
|
proxy.$modal.msgWarning('时间范围最多为31天,请调整开始/结束日期');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 班次等筛选为可选过滤项,严禁强制选择
|
|
|
|
|
hourlyOutputByHour(queryParams).then((res: any) => {
|
|
|
|
|
rawList = (res?.data) || [];
|
|
|
|
|
buildPivot(rawList);
|
|
|
|
|
updateTrendChart();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetQuery() {
|
|
|
|
|
Object.assign(queryParams, { beginDate: '', endDate: '', processId: undefined, machineId: undefined, shiftId: undefined, classTeamId: undefined, materialName: '' });
|
|
|
|
|
pivotTableData.value = [];
|
|
|
|
|
dateCols.value = [];
|
|
|
|
|
updateTrendChart();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDateLabel(d: string) {
|
|
|
|
|
return dayjs(d).format('YYYY-MM-DD');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatNumber(v: any) {
|
|
|
|
|
const n = Number(v || 0);
|
|
|
|
|
return n % 1 === 0 ? n : n.toFixed(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 限制日期选择:开始日期不能晚于结束日期且最多相差31天;结束日期不能早于开始日期且最多相差31天
|
|
|
|
|
function disableBeginDate(date: Date) {
|
|
|
|
|
if (!queryParams.endDate) return false;
|
|
|
|
|
const end = dayjs(queryParams.endDate);
|
|
|
|
|
const d = dayjs(date);
|
|
|
|
|
return d.isAfter(end) || end.diff(d, 'day') > 31;
|
|
|
|
|
}
|
|
|
|
|
function disableEndDate(date: Date) {
|
|
|
|
|
if (!queryParams.beginDate) return false;
|
|
|
|
|
const begin = dayjs(queryParams.beginDate);
|
|
|
|
|
const d = dayjs(date);
|
|
|
|
|
return d.isBefore(begin) || d.diff(begin, 'day') > 31;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadDicts() {
|
|
|
|
|
try {
|
|
|
|
|
const [pRes, sRes, tRes] = await Promise.all([
|
|
|
|
|
getProcessInfoList({}),
|
|
|
|
|
getBaseShiftInfoList({}),
|
|
|
|
|
getBaseClassTeamInfoList({})
|
|
|
|
|
]);
|
|
|
|
|
processList.value = pRes?.data || [];
|
|
|
|
|
shiftList.value = sRes?.data || [];
|
|
|
|
|
teamList.value = tRes?.data || [];
|
|
|
|
|
await loadMachinesByProcess();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('加载筛选项失败', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadMachinesByProcess() {
|
|
|
|
|
try {
|
|
|
|
|
const params: any = {};
|
|
|
|
|
if (queryParams.processId) params.processId = queryParams.processId;
|
|
|
|
|
const mRes = await getProdBaseMachineInfoList(params);
|
|
|
|
|
machineList.value = mRes?.data || [];
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('加载机台失败', e);
|
|
|
|
|
machineList.value = [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(() => queryParams.processId, async () => {
|
|
|
|
|
queryParams.machineId = undefined;
|
|
|
|
|
await loadMachinesByProcess();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 移除与“关键时段/班次判定”相关的任何自动刷新逻辑
|
|
|
|
|
|
|
|
|
|
function handleExportExcel() {
|
|
|
|
|
proxy?.download('/mes/prodReport/hourlyOutputByHour/export', { ...queryParams }, `小时产量报表_${Date.now()}.xlsx`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleExportCsv() {
|
|
|
|
|
const headers = ['序号','项目','时间段','平均', ...dateCols.value.map(d => formatDateLabel(d))];
|
|
|
|
|
const bodyRows = pivotTableData.value.map((row: any, idx: number) => {
|
|
|
|
|
const isSummary = row.item === '合计' || row.item === '平均';
|
|
|
|
|
const seq = isSummary ? '' : String(idx + 1);
|
|
|
|
|
const vals = dateCols.value.map(d => formatNumber(row[d]));
|
|
|
|
|
return [seq, row.item, row.timeRange || '', formatNumber(row.avg), ...vals];
|
|
|
|
|
});
|
|
|
|
|
const csvContent = [headers.join(','), ...bodyRows.map(r => r.join(','))].join('\n');
|
|
|
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
|
|
|
const url = window.URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = `小时产量统计_${Date.now()}.csv`;
|
|
|
|
|
a.click();
|
|
|
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onResize() {
|
|
|
|
|
trendChart?.resize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cellStyle({ row }: any) {
|
|
|
|
|
if (row.item === '合计' || row.item === '平均') {
|
|
|
|
|
return { fontWeight: 600 };
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
queryParams.endDate = dayjs().format('YYYY-MM-DD');
|
|
|
|
|
queryParams.beginDate = dayjs(queryParams.endDate).subtract(14, 'day').format('YYYY-MM-DD');
|
|
|
|
|
loadDicts();
|
|
|
|
|
window.addEventListener('resize', onResize);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
window.removeEventListener('resize', onResize);
|
|
|
|
|
trendChart?.dispose();
|
|
|
|
|
trendChart = null;
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.app-container { padding: 12px; }
|
|
|
|
|
.mb-2 { margin-bottom: 12px; }
|
|
|
|
|
.h-100 { height: 100%; }
|
|
|
|
|
.card-header { font-weight: 600; }
|
|
|
|
|
</style>
|