|
@@ -0,0 +1,557 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div style="border: 1px black solid; width: 40%; height: 30%; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 20px;">
|
|
|
|
|
+ <div style="display: flex; align-items: center; gap: 10px;">
|
|
|
|
|
+ <el-form-item label="选择年份:" class="mab" prop="keyOrder" label-width="100">
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="selectedYear"
|
|
|
|
|
+ type="year"
|
|
|
|
|
+ placeholder="选择年份"
|
|
|
|
|
+ :default-value="new Date()"
|
|
|
|
|
+ @change="onYearChange"
|
|
|
|
|
+ style="width: 130px;"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-button type="primary" @click="exportYearlyData">导出年度数据</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div style="display: flex; align-items: center; gap: 10px;">
|
|
|
|
|
+ <el-form-item label="选择月份:" class="mab" prop="keyOrder" label-width="100">
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="selectedMonth"
|
|
|
|
|
+ type="month"
|
|
|
|
|
+ placeholder="选择月份"
|
|
|
|
|
+ :default-value="new Date()"
|
|
|
|
|
+ @change="onMonthChange"
|
|
|
|
|
+ style="width: 150px;"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-button type="success" @click="exportMonthlyData">导出月份数据</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import {ref, reactive} from 'vue'
|
|
|
|
|
+import {
|
|
|
|
|
+ staffSalaryCount,
|
|
|
|
|
+} from '@/api/yunyin/yunying'
|
|
|
|
|
+import {
|
|
|
|
|
+ GetCompletionWorkOrderCostDetail,
|
|
|
|
|
+} from '@/api/jixiaoguanli/jitairibaobiao'
|
|
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
|
|
+import { useUserStore } from '@/pinia/modules/user'
|
|
|
|
|
+import * as ExcelJS from 'exceljs'
|
|
|
|
|
+import { saveAs } from 'file-saver'
|
|
|
|
|
+const userStore = useUserStore()
|
|
|
|
|
+const _username = ref('')
|
|
|
|
|
+_username.value = userStore.userInfo.userName + '/' + userStore.userInfo.nickName
|
|
|
|
|
+
|
|
|
|
|
+const form = reactive({})
|
|
|
|
|
+const visible = ref(true)
|
|
|
|
|
+const selectedYear = ref('')
|
|
|
|
|
+const yearValue = ref('')
|
|
|
|
|
+const selectedMonth = ref('')
|
|
|
|
|
+const monthValue = ref('')
|
|
|
|
|
+
|
|
|
|
|
+// 获取当前日期
|
|
|
|
|
+const currentDate = new Date();
|
|
|
|
|
+// 获取当前年份和月份
|
|
|
|
|
+const currentYear = currentDate.getFullYear();
|
|
|
|
|
+const currentMonth = currentDate.getMonth();
|
|
|
|
|
+// 设置默认年份和月份
|
|
|
|
|
+selectedYear.value = new Date(currentYear, 0, 1);
|
|
|
|
|
+yearValue.value = currentYear.toString();
|
|
|
|
|
+selectedMonth.value = new Date(currentYear, currentMonth, 1);
|
|
|
|
|
+monthValue.value = `${currentYear}${String(currentMonth + 1).padStart(2, '0')}`;
|
|
|
|
|
+
|
|
|
|
|
+// 年份变化时处理
|
|
|
|
|
+const onYearChange = (val) => {
|
|
|
|
|
+ if (val) {
|
|
|
|
|
+ const year = val.getFullYear();
|
|
|
|
|
+ yearValue.value = year.toString();
|
|
|
|
|
+ console.log('Selected year:', yearValue.value);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ yearValue.value = '';
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 月份变化时处理
|
|
|
|
|
+const onMonthChange = (val) => {
|
|
|
|
|
+ if (val) {
|
|
|
|
|
+ const year = val.getFullYear();
|
|
|
|
|
+ const month = val.getMonth() + 1;
|
|
|
|
|
+ monthValue.value = `${year}${String(month).padStart(2, '0')}`;
|
|
|
|
|
+ console.log('Selected month:', monthValue.value);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ monthValue.value = '';
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 导出年度数据
|
|
|
|
|
+const exportYearlyData = async () => {
|
|
|
|
|
+ if (!yearValue.value) {
|
|
|
|
|
+ ElMessage.warning('请选择年份');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 1. 接口数据获取+处理
|
|
|
|
|
+ const response = await GetCompletionWorkOrderCostDetail({ year: yearValue.value});
|
|
|
|
|
+ const originalData = response.data;
|
|
|
|
|
+ if (!originalData || Object.keys(originalData).length === 0) {
|
|
|
|
|
+ ElMessage.warning('暂无数据可导出');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 核心配置
|
|
|
|
|
+ const PROCESS_ORDER = ['胶印','卷凹','圆烫','圆切','单凹','丝印','喷码','覆膜','裁切','切废','烫金','模切','机检','手检'];
|
|
|
|
|
+ const PROCESS_REMARK = {
|
|
|
|
|
+ 胶印: '按所有胶印机台',卷凹: '按所有卷凹机台',圆烫: '按所有圆压圆烫金机台',圆切: '按所有圆压圆模切机台',
|
|
|
|
|
+ 单凹: '按所有单凹机台',丝印: '按所有丝印机台',喷码: '按所有喷码机台',覆膜: '按所有覆膜机台',
|
|
|
|
|
+ 裁切: '按所有切纸机台(仅为裁切工序,不含切废工序)',切废: '按所有切纸机台(仅为切废工序,不含裁切工序)',
|
|
|
|
|
+ 烫金: '按所有烫金机台',模切: '按所有模切机台',机检: '按所有检品机台(不含圆压圆模切配套的检品机)',
|
|
|
|
|
+ 手检: '按工序--手检'
|
|
|
|
|
+ };
|
|
|
|
|
+ const MONTH_KEYS = ['车头产量','补产产量','核算产量','计件工资','加班工资','产量工资合计'];
|
|
|
|
|
+ const YEAR = yearValue.value || new Date().getFullYear();
|
|
|
|
|
+ const totalCol = 1 + 12*6 + 1; // 总列数:工序+12*6指标+备注
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 数据分组
|
|
|
|
|
+ const processGroup = {};
|
|
|
|
|
+ Object.entries(originalData).forEach(([process, monthList]) => {
|
|
|
|
|
+ if (!Array.isArray(monthList) || monthList.length === 0) return;
|
|
|
|
|
+ processGroup[process] = monthList.reduce((obj, item) => {
|
|
|
|
|
+ const monthStr = String(item.年月).slice(-2);
|
|
|
|
|
+ const formatItem = {
|
|
|
|
|
+ 车头产量: Number(item.车头产量) || 0,补产产量: Number(item.补产产量) || 0,核算产量: Number(item.核算产量) || 0,
|
|
|
|
|
+ 计件工资: Number(item.计件工资) || 0,加班工资: Number(item.加班工资) || 0,产量工资合计: Number(item.产量工资合计) || 0
|
|
|
|
|
+ };
|
|
|
|
|
+ obj[monthStr] = formatItem;
|
|
|
|
|
+ return obj;
|
|
|
|
|
+ }, {});
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 构建数据行
|
|
|
|
|
+ // 表头行:标题行、月份行、指标行
|
|
|
|
|
+ const titleRow = [`${YEAR}年按工序归结人工费统计表`];
|
|
|
|
|
+ const monthRow = ['工序'];
|
|
|
|
|
+ const indexRow = [''];
|
|
|
|
|
+ titleRow.push(...Array(totalCol-1).fill('')); // 标题行补空
|
|
|
|
|
+ for (let m = 1; m <= 12; m++) {
|
|
|
|
|
+ monthRow.push(`${m}月`);
|
|
|
|
|
+ // 为每个月添加5个空白单元格,确保每个月占6列
|
|
|
|
|
+ monthRow.push(...Array(5).fill(''));
|
|
|
|
|
+ indexRow.push(...MONTH_KEYS);
|
|
|
|
|
+ }
|
|
|
|
|
+ monthRow.push('备注');
|
|
|
|
|
+ indexRow.push('');
|
|
|
|
|
+
|
|
|
|
|
+ // 数据行:14个工序的所有数据+备注
|
|
|
|
|
+ const dataRows = PROCESS_ORDER.map(process => {
|
|
|
|
|
+ const row = [process];
|
|
|
|
|
+ const monthDataMap = processGroup[process] || {};
|
|
|
|
|
+ for (let m = 1; m <= 12; m++) {
|
|
|
|
|
+ const month = m.toString().padStart(2, '0');
|
|
|
|
|
+ const currentMonth = monthDataMap[month] || {车头产量:0,补产产量:0,核算产量:0,计件工资:0,加班工资:0,产量工资合计:0};
|
|
|
|
|
+ row.push(...MONTH_KEYS.map(key => currentMonth[key].toFixed(1)));
|
|
|
|
|
+ }
|
|
|
|
|
+ row.push(PROCESS_REMARK[process] || '');
|
|
|
|
|
+ return row;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // ============== ExcelJS 核心:创建工作簿+设置样式 ==============
|
|
|
|
|
+ // 4. 创建工作簿和工作表
|
|
|
|
|
+ const workbook = new ExcelJS.Workbook();
|
|
|
|
|
+ const worksheet = workbook.addWorksheet('Sheet1'); // 工作表名与原Excel一致
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 写入数据(按行写入:标题行→月份行→指标行→数据行)
|
|
|
|
|
+ worksheet.addRow(titleRow);
|
|
|
|
|
+ worksheet.addRow(monthRow);
|
|
|
|
|
+ worksheet.addRow(indexRow);
|
|
|
|
|
+ dataRows.forEach(row => worksheet.addRow(row));
|
|
|
|
|
+
|
|
|
|
|
+ // 辅助函数:将列索引转换为Excel列字母(如1→A, 2→B, 26→Z, 27→AA)
|
|
|
|
|
+ const getColumnLetter = (colIndex) => {
|
|
|
|
|
+ let letter = '';
|
|
|
|
|
+ let num = colIndex;
|
|
|
|
|
+ while (num > 0) {
|
|
|
|
|
+ const remainder = (num - 1) % 26;
|
|
|
|
|
+ letter = String.fromCharCode(65 + remainder) + letter;
|
|
|
|
|
+ num = Math.floor((num - 1) / 26);
|
|
|
|
|
+ }
|
|
|
|
|
+ return letter;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 样式配置:合并单元格+边框+底色+居中+列宽
|
|
|
|
|
+ // 6.1 合并单元格
|
|
|
|
|
+ const mergeRange = (range) => worksheet.mergeCells(range);
|
|
|
|
|
+ // 合并1:标题行(A1到最后一列1)
|
|
|
|
|
+ mergeRange(`A1:${getColumnLetter(totalCol)}1`);
|
|
|
|
|
+ // 合并2:工序列(A2:A4)
|
|
|
|
|
+ mergeRange('A2:A3');
|
|
|
|
|
+ // 合并3:每个月的月份行(1月B2:G2、2月H2:M2...)
|
|
|
|
|
+ let startCol = 2; // 从B列开始
|
|
|
|
|
+ for (let m = 1; m <= 12; m++) {
|
|
|
|
|
+ const endCol = startCol + 5;
|
|
|
|
|
+ mergeRange(`${getColumnLetter(startCol)}2:${getColumnLetter(endCol)}2`);
|
|
|
|
|
+ startCol = endCol + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 合并4:备注列(最后一列2:最后一列4)
|
|
|
|
|
+ mergeRange(`${getColumnLetter(totalCol)}2:${getColumnLetter(totalCol)}4`);
|
|
|
|
|
+
|
|
|
|
|
+ // 6.2 定义样式模板
|
|
|
|
|
+ const commonStyle = { // 所有单元格公共样式:边框+居中+自动换行
|
|
|
|
|
+ border: {
|
|
|
|
|
+ top: { style: 'thin', color: { argb: 'FF000000' } },
|
|
|
|
|
+ bottom: { style: 'thin', color: { argb: 'FF000000' } },
|
|
|
|
|
+ left: { style: 'thin', color: { argb: 'FF000000' } },
|
|
|
|
|
+ right: { style: 'thin', color: { argb: 'FF000000' } }
|
|
|
|
|
+ },
|
|
|
|
|
+ alignment: { horizontal: 'center', vertical: 'middle', wrapText: true },
|
|
|
|
|
+ font: { size: 11, color: { argb: 'FF000000' } }
|
|
|
|
|
+ };
|
|
|
|
|
+ const titleStyle = { // 标题行样式:浅灰底+14号字+加粗
|
|
|
|
|
+ ...commonStyle,
|
|
|
|
|
+ font: { size: 14, bold: true, color: { argb: 'FF000000' } },
|
|
|
|
|
+ fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE6E6E6' } }
|
|
|
|
|
+ };
|
|
|
|
|
+ const headerStyle = { // 表头行样式:深灰底+12号字+加粗
|
|
|
|
|
+ ...commonStyle,
|
|
|
|
|
+ font: { size: 12, bold: true, color: { argb: 'FF000000' } },
|
|
|
|
|
+ fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFC0C0C0' } }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 6.3 应用样式到单元格
|
|
|
|
|
+ const lastRow = 4 + dataRows.length; // 最后一行:3行表头+数据行
|
|
|
|
|
+
|
|
|
|
|
+ // 定义特殊列的样式(黄色背景)
|
|
|
|
|
+ const yellowBgStyle = {
|
|
|
|
|
+ ...commonStyle,
|
|
|
|
|
+ fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF00' } }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 标题行(第1行):应用标题样式
|
|
|
|
|
+ worksheet.getRow(1).eachCell(cell => cell.style = titleStyle);
|
|
|
|
|
+
|
|
|
|
|
+ // 表头行(第2-3行):应用表头样式
|
|
|
|
|
+ worksheet.getRow(2).eachCell(cell => cell.style = headerStyle);
|
|
|
|
|
+
|
|
|
|
|
+ // 指标行(第3行):特殊处理核算产量和产量工资合计列
|
|
|
|
|
+ const indexRowObject = worksheet.getRow(3);
|
|
|
|
|
+ indexRowObject.eachCell((cell, colIndex) => {
|
|
|
|
|
+ // 跳过A列(工序列)
|
|
|
|
|
+ if (colIndex === 1) {
|
|
|
|
|
+ cell.style = headerStyle;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 检查是否是核算产量或产量工资合计列
|
|
|
|
|
+ const cellValue = cell.value;
|
|
|
|
|
+ if (cellValue === '核算产量' || cellValue === '产量工资合计') {
|
|
|
|
|
+ cell.style = {
|
|
|
|
|
+ ...headerStyle,
|
|
|
|
|
+ fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF00' } }
|
|
|
|
|
+ };
|
|
|
|
|
+ } else {
|
|
|
|
|
+ cell.style = headerStyle;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 数据行(第4行到最后一行):特殊处理核算产量和产量工资合计列
|
|
|
|
|
+ for (let row = 4; row <= lastRow; row++) {
|
|
|
|
|
+ const currentRow = worksheet.getRow(row);
|
|
|
|
|
+ currentRow.eachCell((cell, colIndex) => {
|
|
|
|
|
+ // 跳过A列(工序列)和最后一列(备注列)
|
|
|
|
|
+ if (colIndex === 1 || colIndex === totalCol) {
|
|
|
|
|
+ cell.style = commonStyle;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算当前列属于第几个月的第几个指标
|
|
|
|
|
+ const monthColIndex = colIndex - 2; // 减去A列
|
|
|
|
|
+ const monthIndex = Math.floor(monthColIndex / 6);
|
|
|
|
|
+ const metricIndex = monthColIndex % 6;
|
|
|
|
|
+
|
|
|
|
|
+ // 核算产量是每个月的第3个指标(索引2),产量工资合计是每个月的第6个指标(索引5)
|
|
|
|
|
+ if (metricIndex === 2 || metricIndex === 5) {
|
|
|
|
|
+ cell.style = yellowBgStyle;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ cell.style = commonStyle;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6.4 设置列宽(与原Excel一致,像素适配)
|
|
|
|
|
+ const colWidths = [120]; // A列(工序):120px
|
|
|
|
|
+ for (let m = 1; m <= 12; m++) {
|
|
|
|
|
+ colWidths.push(...Array(6).fill(80)); // 12个月×6列:80px/列
|
|
|
|
|
+ }
|
|
|
|
|
+ colWidths.push(200); // 最后一列(备注):200px
|
|
|
|
|
+ // ExcelJS列宽单位为「字符」,px转字符按 1px ≈ 0.14 换算(精准适配)
|
|
|
|
|
+ worksheet.columns = colWidths.map((width, index) => ({
|
|
|
|
|
+ key: index+1,
|
|
|
|
|
+ width: width * 0.14
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 6.5 调整行高(可选,适配表头文字,避免遮挡)
|
|
|
|
|
+ worksheet.getRow(1).height = 30; // 标题行高
|
|
|
|
|
+ worksheet.getRow(2).height = 25; // 月份行高
|
|
|
|
|
+ worksheet.getRow(3).height = 25; // 指标行高
|
|
|
|
|
+
|
|
|
|
|
+ // ============== 导出Excel文件 ==============
|
|
|
|
|
+ // 7. 生成blob并下载
|
|
|
|
|
+ const buffer = await workbook.xlsx.writeBuffer();
|
|
|
|
|
+ const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
|
|
|
+ saveAs(blob, `按工序归结人工费统计表${YEAR}年.xlsx`);
|
|
|
|
|
+
|
|
|
|
|
+ ElMessage.success('导出成功!');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Excel导出失败:', error);
|
|
|
|
|
+ ElMessage.error('导出数据失败,请重试');
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 导出月份数据
|
|
|
|
|
+const exportMonthlyData = async () => {
|
|
|
|
|
+ if (!monthValue.value) {
|
|
|
|
|
+ ElMessage.warning('请选择月份');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 1. 接口数据获取+处理
|
|
|
|
|
+ const year = monthValue.value.substring(0, 4);
|
|
|
|
|
+ const month = monthValue.value.substring(4, 6);
|
|
|
|
|
+ const response = await GetCompletionWorkOrderCostDetail({ year: year, month: month });
|
|
|
|
|
+ const originalData = response.data;
|
|
|
|
|
+ if (!originalData || Object.keys(originalData).length === 0) {
|
|
|
|
|
+ ElMessage.warning('暂无数据可导出');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 核心配置
|
|
|
|
|
+ const PROCESS_ORDER = ['胶印','卷凹','圆烫','圆切','单凹','丝印','喷码','覆膜','裁切','切废','烫金','模切','机检','手检'];
|
|
|
|
|
+ const PROCESS_REMARK = {
|
|
|
|
|
+ 胶印: '按所有胶印机台',卷凹: '按所有卷凹机台',圆烫: '按所有圆压圆烫金机台',圆切: '按所有圆压圆模切机台',
|
|
|
|
|
+ 单凹: '按所有单凹机台',丝印: '按所有丝印机台',喷码: '按所有喷码机台',覆膜: '按所有覆膜机台',
|
|
|
|
|
+ 裁切: '按所有切纸机台(仅为裁切工序,不含切废工序)',切废: '按所有切纸机台(仅为切废工序,不含裁切工序)',
|
|
|
|
|
+ 烫金: '按所有烫金机台',模切: '按所有模切机台',机检: '按所有检品机台(不含圆压圆模切配套的检品机)',
|
|
|
|
|
+ 手检: '按工序--手检'
|
|
|
|
|
+ };
|
|
|
|
|
+ const MONTH_KEYS = ['车头产量','补产产量','核算产量','计件工资','加班工资','产量工资合计'];
|
|
|
|
|
+ const YEAR = year;
|
|
|
|
|
+ const MONTH = monthValue.value.substring(4, 6);
|
|
|
|
|
+ const totalCol = 1 + 6 + 1; // 总列数:工序+6指标+备注
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 数据处理
|
|
|
|
|
+ const processData = {};
|
|
|
|
|
+ Object.entries(originalData).forEach(([process, data]) => {
|
|
|
|
|
+ if (!Array.isArray(data) || data.length === 0) {
|
|
|
|
|
+ processData[process] = {车头产量:0,补产产量:0,核算产量:0,计件工资:0,加班工资:0,产量工资合计:0};
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 直接使用返回的数据,因为接口已经按月份过滤
|
|
|
|
|
+ const item = data[0];
|
|
|
|
|
+ processData[process] = {
|
|
|
|
|
+ 车头产量: Number(item.车头产量) || 0,
|
|
|
|
|
+ 补产产量: Number(item.补产产量) || 0,
|
|
|
|
|
+ 核算产量: Number(item.核算产量) || 0,
|
|
|
|
|
+ 计件工资: Number(item.计件工资) || 0,
|
|
|
|
|
+ 加班工资: Number(item.加班工资) || 0,
|
|
|
|
|
+ 产量工资合计: Number(item.产量工资合计) || 0
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 构建数据行
|
|
|
|
|
+ // 表头行:标题行、月份行、指标行
|
|
|
|
|
+ const titleRow = [`${YEAR}年${MONTH}月按工序归结人工费统计表`];
|
|
|
|
|
+ const monthRow = ['工序'];
|
|
|
|
|
+ const indexRow = [''];
|
|
|
|
|
+ titleRow.push(...Array(totalCol-1).fill('')); // 标题行补空
|
|
|
|
|
+ monthRow.push(`${MONTH}月`);
|
|
|
|
|
+ // 为月份添加5个空白单元格,确保占6列
|
|
|
|
|
+ monthRow.push(...Array(5).fill(''));
|
|
|
|
|
+ indexRow.push(...MONTH_KEYS);
|
|
|
|
|
+ monthRow.push('备注');
|
|
|
|
|
+ indexRow.push('');
|
|
|
|
|
+
|
|
|
|
|
+ // 数据行:14个工序的所有数据+备注
|
|
|
|
|
+ const dataRows = PROCESS_ORDER.map(process => {
|
|
|
|
|
+ const row = [process];
|
|
|
|
|
+ const currentData = processData[process] || {车头产量:0,补产产量:0,核算产量:0,计件工资:0,加班工资:0,产量工资合计:0};
|
|
|
|
|
+ row.push(...MONTH_KEYS.map(key => currentData[key].toFixed(1)));
|
|
|
|
|
+ row.push(PROCESS_REMARK[process] || '');
|
|
|
|
|
+ return row;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // ============== ExcelJS 核心:创建工作簿+设置样式 ==============
|
|
|
|
|
+ // 4. 创建工作簿和工作表
|
|
|
|
|
+ const workbook = new ExcelJS.Workbook();
|
|
|
|
|
+ const worksheet = workbook.addWorksheet('Sheet1'); // 工作表名与原Excel一致
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 写入数据(按行写入:标题行→月份行→指标行→数据行)
|
|
|
|
|
+ worksheet.addRow(titleRow);
|
|
|
|
|
+ worksheet.addRow(monthRow);
|
|
|
|
|
+ worksheet.addRow(indexRow);
|
|
|
|
|
+ dataRows.forEach(row => worksheet.addRow(row));
|
|
|
|
|
+
|
|
|
|
|
+ // 辅助函数:将列索引转换为Excel列字母(如1→A, 2→B, 26→Z, 27→AA)
|
|
|
|
|
+ const getColumnLetter = (colIndex) => {
|
|
|
|
|
+ let letter = '';
|
|
|
|
|
+ let num = colIndex;
|
|
|
|
|
+ while (num > 0) {
|
|
|
|
|
+ const remainder = (num - 1) % 26;
|
|
|
|
|
+ letter = String.fromCharCode(65 + remainder) + letter;
|
|
|
|
|
+ num = Math.floor((num - 1) / 26);
|
|
|
|
|
+ }
|
|
|
|
|
+ return letter;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 样式配置:合并单元格+边框+底色+居中+列宽
|
|
|
|
|
+ // 6.1 合并单元格
|
|
|
|
|
+ const mergeRange = (range) => worksheet.mergeCells(range);
|
|
|
|
|
+ // 合并1:标题行(A1到最后一列1)
|
|
|
|
|
+ mergeRange(`A1:${getColumnLetter(totalCol)}1`);
|
|
|
|
|
+ // 合并2:工序列(A2:A4)
|
|
|
|
|
+ mergeRange('A2:A3');
|
|
|
|
|
+ // 合并3:月份行(B2:G2)
|
|
|
|
|
+ mergeRange(`B2:G2`);
|
|
|
|
|
+ // 合并4:备注列(最后一列2:最后一列4)
|
|
|
|
|
+ mergeRange(`${getColumnLetter(totalCol)}2:${getColumnLetter(totalCol)}3`);
|
|
|
|
|
+
|
|
|
|
|
+ // 6.2 定义样式模板
|
|
|
|
|
+ const commonStyle = { // 所有单元格公共样式:边框+居中+自动换行
|
|
|
|
|
+ border: {
|
|
|
|
|
+ top: { style: 'thin', color: { argb: 'FF000000' } },
|
|
|
|
|
+ bottom: { style: 'thin', color: { argb: 'FF000000' } },
|
|
|
|
|
+ left: { style: 'thin', color: { argb: 'FF000000' } },
|
|
|
|
|
+ right: { style: 'thin', color: { argb: 'FF000000' } }
|
|
|
|
|
+ },
|
|
|
|
|
+ alignment: { horizontal: 'center', vertical: 'middle', wrapText: true },
|
|
|
|
|
+ font: { size: 11, color: { argb: 'FF000000' } }
|
|
|
|
|
+ };
|
|
|
|
|
+ const titleStyle = { // 标题行样式:浅灰底+14号字+加粗
|
|
|
|
|
+ ...commonStyle,
|
|
|
|
|
+ font: { size: 14, bold: true, color: { argb: 'FF000000' } },
|
|
|
|
|
+ fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE6E6E6' } }
|
|
|
|
|
+ };
|
|
|
|
|
+ const headerStyle = { // 表头行样式:深灰底+12号字+加粗
|
|
|
|
|
+ ...commonStyle,
|
|
|
|
|
+ font: { size: 12, bold: true, color: { argb: 'FF000000' } },
|
|
|
|
|
+ fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFC0C0C0' } }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 6.3 应用样式到单元格
|
|
|
|
|
+ const lastRow = 3 + dataRows.length; // 最后一行:3行表头+数据行
|
|
|
|
|
+
|
|
|
|
|
+ // 定义特殊列的样式(黄色背景)
|
|
|
|
|
+ const yellowBgStyle = {
|
|
|
|
|
+ ...commonStyle,
|
|
|
|
|
+ fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF00' } }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 标题行(第1行):应用标题样式
|
|
|
|
|
+ worksheet.getRow(1).eachCell(cell => cell.style = titleStyle);
|
|
|
|
|
+
|
|
|
|
|
+ // 表头行(第2行):应用表头样式
|
|
|
|
|
+ worksheet.getRow(2).eachCell(cell => cell.style = headerStyle);
|
|
|
|
|
+
|
|
|
|
|
+ // 指标行(第3行):特殊处理核算产量和产量工资合计列
|
|
|
|
|
+ const indexRowObject = worksheet.getRow(3);
|
|
|
|
|
+ indexRowObject.eachCell((cell, colIndex) => {
|
|
|
|
|
+ // 跳过A列(工序列)
|
|
|
|
|
+ if (colIndex === 1) {
|
|
|
|
|
+ cell.style = headerStyle;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 检查是否是核算产量或产量工资合计列
|
|
|
|
|
+ const cellValue = cell.value;
|
|
|
|
|
+ if (cellValue === '核算产量' || cellValue === '产量工资合计') {
|
|
|
|
|
+ cell.style = {
|
|
|
|
|
+ ...headerStyle,
|
|
|
|
|
+ fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF00' } }
|
|
|
|
|
+ };
|
|
|
|
|
+ } else {
|
|
|
|
|
+ cell.style = headerStyle;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 数据行(第4行到最后一行):特殊处理核算产量和产量工资合计列
|
|
|
|
|
+ for (let row = 4; row <= lastRow; row++) {
|
|
|
|
|
+ const currentRow = worksheet.getRow(row);
|
|
|
|
|
+ currentRow.eachCell((cell, colIndex) => {
|
|
|
|
|
+ // 跳过A列(工序列)和最后一列(备注列)
|
|
|
|
|
+ if (colIndex === 1 || colIndex === totalCol) {
|
|
|
|
|
+ cell.style = commonStyle;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算当前列属于第几个指标
|
|
|
|
|
+ const metricIndex = colIndex - 2; // 减去A列
|
|
|
|
|
+
|
|
|
|
|
+ // 核算产量是第3个指标(索引2),产量工资合计是第6个指标(索引5)
|
|
|
|
|
+ if (metricIndex === 2 || metricIndex === 5) {
|
|
|
|
|
+ cell.style = yellowBgStyle;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ cell.style = commonStyle;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6.4 设置列宽(与原Excel一致,像素适配)
|
|
|
|
|
+ const colWidths = [120]; // A列(工序):120px
|
|
|
|
|
+ colWidths.push(...Array(6).fill(80)); // 6列指标:80px/列
|
|
|
|
|
+ colWidths.push(200); // 最后一列(备注):200px
|
|
|
|
|
+ // ExcelJS列宽单位为「字符」,px转字符按 1px ≈ 0.14 换算(精准适配)
|
|
|
|
|
+ worksheet.columns = colWidths.map((width, index) => ({
|
|
|
|
|
+ key: index+1,
|
|
|
|
|
+ width: width * 0.14
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 6.5 调整行高(可选,适配表头文字,避免遮挡)
|
|
|
|
|
+ worksheet.getRow(1).height = 30; // 标题行高
|
|
|
|
|
+ worksheet.getRow(2).height = 25; // 月份行高
|
|
|
|
|
+ worksheet.getRow(3).height = 25; // 指标行高
|
|
|
|
|
+
|
|
|
|
|
+ // ============== 导出Excel文件 ==============
|
|
|
|
|
+ // 7. 生成blob并下载
|
|
|
|
|
+ const buffer = await workbook.xlsx.writeBuffer();
|
|
|
|
|
+ const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
|
|
|
+ saveAs(blob, `按工序归结人工费统计表${YEAR}年${MONTH}月.xlsx`);
|
|
|
|
|
+
|
|
|
|
|
+ ElMessage.success('导出成功!');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Excel导出失败:', error);
|
|
|
|
|
+ ElMessage.error('导出数据失败,请重试');
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+</script>
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+:deep(.el-table td .cell) {
|
|
|
|
|
+ line-height: 20px !important;
|
|
|
|
|
+}
|
|
|
|
|
+:deep(.el-tabs__header){
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.search{
|
|
|
|
|
+ margin-left: 0px !important;
|
|
|
|
|
+ margin-right: 10px !important;
|
|
|
|
|
+}
|
|
|
|
|
+.bt{
|
|
|
|
|
+ margin-left: 2px !important;
|
|
|
|
|
+ padding: 3px !important;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.el-tabs__header{
|
|
|
|
|
+ margin: 0px !important;
|
|
|
|
|
+}
|
|
|
|
|
+.gva-table-box{
|
|
|
|
|
+ padding: 0px !important;
|
|
|
|
|
+}
|
|
|
|
|
+.el-pagination{
|
|
|
|
|
+ margin-top: 0px !important;
|
|
|
|
|
+}
|
|
|
|
|
+.mab{
|
|
|
|
|
+ margin-bottom: 5px;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|
|
|
|
|
+
|