feat(hrm): 统一审批状态显示并添加外出条打印功能

- 在请假、外出申请页面统一审批状态显示为标签样式
- 将审批状态字段从approveStatus改为approvalStatus
- 添加外出条打印组件,支持PDF格式打印
- 优化审批状态标签颜色和文本显示
- 修复已撤销状态显示错误问题
This commit is contained in:
砂糖
2026-01-21 10:54:04 +08:00
parent 9d484f0a8a
commit 9d35f39906
4 changed files with 452 additions and 42 deletions

View File

@@ -37,7 +37,7 @@
<el-form-item label="外出小时数" prop="outHours">
<el-input v-model="form.outHours" placeholder="选择时间后自动计算,也可手动修改" />
</el-form-item>
<el-form-item label="外出地点" prop="outPlace">
<el-form-item label="外出地点" prop="outPlace">
<el-input v-model="form.outPlace" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="外出原因" prop="outReason">
@@ -51,7 +51,8 @@
</el-form-item>
</el-form>
<div style="text-align: center;">
<el-button type="primary" @click="handleSubmit" v-loading="loading">{{ form.applyId ? '更新申请' : '提交申请' }}</el-button>
<el-button type="primary" @click="handleSubmit" v-loading="loading">{{ form.outId ? '更新申请' : '提交申请'
}}</el-button>
<el-button @click="handleReset">重置表单</el-button>
</div>
</el-card>
@@ -63,7 +64,13 @@
<el-button style="float: right;" icon="el-icon-refresh" @click="getList">刷新</el-button>
</template>
<el-table v-loading="loading" :data="leaveRequestList">
<el-table-column label="申请状态" align="center" prop="approvalStatus"></el-table-column>
<el-table-column prop="approvalStatus" label="审批状态" align="center">
<template slot-scope="scope">
<el-tag :type="getStatusTagType(scope.row.approvalStatus)">
{{ getStatusText(scope.row.approvalStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="外出类型" align="center" prop="outType">
<template slot-scope="scope">
<dict-tag :options="dict.type.hrm_out_type" :value="scope.row.outType" />
@@ -89,9 +96,13 @@
<el-table-column label="外出地点" align="center" prop="outPlace" show-overflow-tooltip />
<el-table-column label="外出原因" align="center" prop="outReason" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="160">
<template slot-scope="scope" v-if="scope.row.approvalStatus === '待审批'">
<el-button icon="el-icon-edit" size="mini" @click="handleEdit(scope.row)">修改</el-button>
<el-button icon="el-icon-delete" size="mini" @click="handleWithdraw(scope.row)">撤回</el-button>
<template slot-scope="scope">
<el-button icon="el-icon-printer" size="mini" v-if="scope.row.approvalStatus === '已同意'"
@click="handlePrint(scope.row)">打印</el-button>
<el-button icon="el-icon-edit" size="mini" @click="handleEdit(scope.row)"
v-if="scope.row.approvalStatus === '待审批'">修改</el-button>
<el-button icon="el-icon-delete" size="mini" @click="handleWithdraw(scope.row)"
v-if="scope.row.approvalStatus === '待审批'">撤回</el-button>
</template>
</el-table-column>
</el-table>
@@ -100,6 +111,8 @@
</el-card>
</el-col>
</el-row>
<out-label-printer ref="outLabelPrinter" :printer-info="printerInfo"></out-label-printer>
</div>
</template>
@@ -109,13 +122,15 @@ import { listApproval, updateApproval } from "@/api/wms/approval"
import { listDept } from "@/api/system/dept"
import FileUpload from '@/components/FileUpload'
import DictSelect from '@/components/DictSelect'
import OutLabelPrinter from '../components/outLabelPrinter'
export default {
name: 'LeaveApply',
dicts: ['hrm_leave_shift', 'hrm_out_type', 'hrm_department', 'hrm_leave_employee'],
components: {
FileUpload,
DictSelect
DictSelect,
OutLabelPrinter
},
data() {
return {
@@ -134,18 +149,17 @@ export default {
},
// 表单校验规则【核心新增:完整必填校验】
rules: {
leaveTitle: [{ required: true, message: '请假原因不能为空', trigger: ['blur', 'change'] }],
leaveType: [{ required: true, message: '请假类型不能为空', trigger: 'change' }],
applicantName: [{ required: true, message: '请假人姓名不能为空', trigger: 'change' }],
outType: [{ required: true, message: '外出类型不能为空', trigger: 'change' }],
applicantName: [{ required: true, message: '外出人姓名不能为空', trigger: 'change' }],
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'change' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }],
leaveShift: [{ required: true, message: '请假班次不能为空', trigger: 'change' }],
leaveDays: [
{ required: true, message: '请假天数不能为空', trigger: ['blur', 'change'] },
],
outHours: [{ required: true, message: '外出小时数不能为空', trigger: ['blur', 'change'] }],
outPlace: [{ required: true, message: '外出地点不能为空', trigger: ['blur', 'change'] }],
outReason: [{ required: true, message: '外出原因不能为空', trigger: ['blur', 'change'] }],
applicantDeptName: [{ required: true, message: '审批部门不能为空', trigger: 'change' }],
},
deptOptions: []
deptOptions: [],
printerInfo: {},
}
},
// 核心新增:监听开始/结束时间变化,自动计算天数
@@ -247,6 +261,35 @@ export default {
this.form = response.data;
});
},
handlePrint(row) {
const startYear = new Date(row.startTime).getFullYear()
const endYear = new Date(row.endTime).getFullYear()
const startMonth = new Date(row.startTime).getMonth() + 1
const endMonth = new Date(row.endTime).getMonth() + 1
const startDay = new Date(row.startTime).getDate()
const endDay = new Date(row.endTime).getDate()
const applyName = row.applicantName
const approverName = row.approverName
const reason = row.outReason
const outHours = parseInt(row.outHours)
const outPlace = row.outPlace
this.printerInfo = {
startYear,
endYear,
startMonth,
endMonth,
startDay,
endDay,
applyName,
approverName,
reason,
outHours,
outPlace,
}
this.$nextTick(() => {
this.$refs["outLabelPrinter"].print()
})
},
/** 提交按钮 */
handleSubmit() {
this.form.outTitle = `${this.form.applicantName}-${this.form.outType}-${this.form.startTime}-${this.form.outReason || ''}`
@@ -289,6 +332,27 @@ export default {
this.loading = false;
});
},
// 获取审批状态对应的标签类型
getStatusTagType(status) {
const typeMap = {
'待审批': 'warning',
'已同意': 'success',
'已驳回': 'danger',
'已撤销': 'info',
}
return typeMap[status] || 'default'
},
// 获取审批状态的中文文本
getStatusText(status) {
const textMap = {
'待审批': '待审批',
'已同意': '已同意',
'已驳回': '已驳回',
'已撤销': '已撤销',
}
return textMap[status] || '未知状态'
},
// 核心新增:自动计算请假天数的方法
calculateLeaveDays() {
const { startTime, endTime } = this.form;

View File

@@ -56,7 +56,8 @@
</el-form-item>
</el-form>
<div style="text-align: center;">
<el-button type="primary" @click="handleSubmit" v-loading="loading">{{ form.applyId ? '更新申请' : '提交申请' }}</el-button>
<el-button type="primary" @click="handleSubmit" v-loading="loading">{{ form.leaveId ? '更新申请' : '提交申请'
}}</el-button>
<el-button @click="handleReset">重置表单</el-button>
</div>
</el-card>
@@ -68,7 +69,13 @@
<el-button style="float: right;" icon="el-icon-refresh" @click="getList">刷新</el-button>
</template>
<el-table v-loading="loading" :data="leaveRequestList">
<el-table-column label="申请状态" align="center" prop="approvalStatus"></el-table-column>
<el-table-column prop="approvalStatus" label="审批状态" align="center">
<template slot-scope="scope">
<el-tag :type="getStatusTagType(scope.row.approvalStatus)">
{{ getStatusText(scope.row.approvalStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="请假类型" align="center" prop="leaveType">
<template slot-scope="scope">
<dict-tag :options="dict.type.hrm_leave_type" :value="scope.row.leaveType" />
@@ -299,6 +306,27 @@ export default {
this.loading = false;
});
},
// 获取审批状态对应的标签类型
getStatusTagType(status) {
const typeMap = {
'待审批': 'warning',
'已同意': 'success',
'已驳回': 'danger',
'已撤销': 'info',
}
return typeMap[status] || 'default'
},
// 获取审批状态的中文文本
getStatusText(status) {
const textMap = {
'待审批': '待审批',
'已同意': '已同意',
'已驳回': '已驳回',
'已撤销': '已撤销',
}
return textMap[status] || '未知状态'
},
// 核心新增:自动计算请假天数的方法
calculateLeaveDays() {
const { startTime, endTime } = this.form;

View File

@@ -0,0 +1,320 @@
<template>
<div>
<!-- 点击打印后为表单赋值打印内容为表单内容 -->
<div ref="printer" class="print-container" v-show="false">
<div class="print-content">
<div class="print-stub-title">存根</div>
<div class="print-stub-content">
<div class="print-stub-line">
<h4>外出条</h4>
<b class="print-stub-year" style="float: right; margin-top: 5px;">
<span>{{ printerInfo.startYear }}</span>
</b>
</div>
<div class="print-stub-apply-info">
<div class="print-stub-applicant">请假人<span>{{ printerInfo.applyName }}</span></div>
<div class="print-apply-date">
<div class="print-stub-date">
<span>{{ printerInfo.startMonth }}</span>
<span>{{ printerInfo.startDay }}</span>
</div>
<div class="print-stub-days">
<span>{{ printerInfo.startMonth }}</span>
<span>{{ printerInfo.startDay }}</span>
<span>{{ printerInfo.outHours }}</span>小时
</div>
</div>
<div class="print-stub-reason">事由<span>{{ printerInfo.reason }}</span></div>
<div class="print-stub-reason">外出地点<span>{{ printerInfo.outPlace }}</span></div>
<div class="print-stub-approver">批准人<span>{{ printerInfo.approverName }}</span></div>
</div>
</div>
<!-- 竖虚线分割 -->
<div class="print-vertical-line"></div>
<div class="print-document">
<div class="print-document-line">
<h4>外出条</h4>
</div>
<div class="print-document-apply-info">
<div class="print-document-applicant">请假人<span>{{ printerInfo.applyName }}</span></div>
<div class="print-apply-date">
<span>{{ printerInfo.startYear }}</span>
<span>{{ printerInfo.startMonth }}</span>
<span>{{ printerInfo.startDay }}</span>
<span>{{ printerInfo.endYear }}</span>
<span>{{ printerInfo.endMonth }}</span>
<span>{{ printerInfo.endDay }}</span>
<span>{{ printerInfo.outHours }}</span>小时
</div>
<div class="print-document-reason">事由<span>{{ printerInfo.reason }}</span></div>
<div class="print-document-reason">外出地点<span>{{ printerInfo.outPlace }}</span></div>
<div class="print-document-approver">批准人<span>{{ printerInfo.approverName }}</span></div>
</div>
</div>
<img class="print-stub-sign" src="http://140.143.206.120:10900/klp-oa/files/2025/12/30/1b8230d62d324fe498024b08ae4acc1d.png" alt=""></img>
</div>
</div>
</div>
</template>
<script>
import html2canvas from 'html2canvas';
import { PDFDocument } from 'pdf-lib';
// 假设你用的是Element UI的Message若不是可替换为其他提示组件
import { Message } from 'element-ui';
export default {
props: {
printerInfo: {
type: Object,
default: () => ({}),
}
},
methods: {
// 参考原方法:等待所有资源(图片、字体、二维码等)加载完成
async waitForAllResources(container) {
return new Promise((resolve) => {
// 等待图片加载
const images = container.querySelectorAll('img');
const imagePromises = Array.from(images).map(img => {
if (img.complete) return Promise.resolve();
return new Promise(resolve => {
img.onload = resolve;
img.onerror = resolve; // 加载失败也继续
});
});
// 等待字体加载(可选,根据实际需求)
const fontPromise = new Promise(resolve => setTimeout(resolve, 200));
// 等待所有资源加载完成
Promise.all([...imagePromises, fontPromise]).then(resolve);
});
},
// 重构后的打印方法先生成PDF再打印
async print() {
// 1. 获取打印容器DOM
const printContainer = this.$refs.printer?.querySelector('.print-content');
if (!printContainer) {
Message.error('未找到打印容器,无法打印');
return;
}
try {
Message.info('正在准备打印内容,请稍等...');
// 2. 定义外出条纸张尺寸(自定义,可根据实际需求调整)
// 外出条建议尺寸宽210mmA5宽度高140mm适配内容高度
const paperWidthMm = 240; // 纸张宽度(毫米)
const paperHeightMm = 100; // 纸张高度(毫米)
// 3. 创建临时隐藏容器,模拟打印布局(参考原方法)
const originalParent = printContainer.parentNode;
const originalNext = printContainer.nextSibling;
const wrapper = document.createElement('div');
wrapper.style.position = 'fixed';
wrapper.style.left = '-100000px'; // 隐藏在可视区域外
wrapper.style.top = '0';
wrapper.style.width = `${paperWidthMm}mm`; // 固定纸张宽度
wrapper.style.height = `${paperHeightMm}mm`; // 固定纸张高度
wrapper.style.boxSizing = 'border-box';
wrapper.style.backgroundColor = '#ffffff';
wrapper.style.overflow = 'hidden';
wrapper.style.padding = '10mm'; // 内边距,适配打印内容
// 设置打印内容的布局,保证居中适配
printContainer.style.width = '100%';
printContainer.style.height = '100%';
printContainer.style.display = 'flex';
printContainer.style.alignItems = 'center';
printContainer.style.justifyContent = 'center';
wrapper.appendChild(printContainer);
document.body.appendChild(wrapper);
// 4. 等待资源加载完成(字体、图片等)
await this.waitForAllResources(printContainer);
// 等待布局稳定
await new Promise(resolve => setTimeout(resolve, 100));
// 5. 用html2canvas生成高清Canvas
const containerWidth = printContainer.offsetWidth;
const containerHeight = printContainer.offsetHeight;
const canvas = await html2canvas(printContainer, {
backgroundColor: '#ffffff',
scale: 3, // 3倍缩放保证PDF清晰度
useCORS: true,
willReadFrequently: true, // 优化Canvas读取性能
width: containerWidth,
height: containerHeight,
windowWidth: containerWidth,
windowHeight: containerHeight,
});
// 6. 使用pdf-lib生成单页PDF占满整张纸精准适配
const mmToPt = 72 / 25.4; // 毫米转PDF的点pt1pt = 25.4/72 mm
const pageWidthPt = paperWidthMm * mmToPt;
const pageHeightPt = paperHeightMm * mmToPt;
// 创建PDF文档
const pdfDoc = await PDFDocument.create();
// 嵌入Canvas生成的PNG图片
const imgPng = await pdfDoc.embedPng(canvas.toDataURL('image/png'));
// 添加PDF页面设置页面尺寸与纸张一致
const page = pdfDoc.addPage([pageWidthPt, pageHeightPt]);
// 绘制图片占满整个PDF页面无边距
page.drawImage(imgPng, {
x: 0,
y: 0,
width: pageWidthPt,
height: pageHeightPt
});
// 保存PDF并生成Blob URL
const pdfBytes = await pdfDoc.save();
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const pdfUrl = URL.createObjectURL(blob);
// 7. 打开PDF窗口打印兼容弹窗拦截
const printWin = window.open(pdfUrl, '_blank');
if (!printWin) {
// 弹窗被拦截时改为下载PDF
Message.warning('浏览器弹窗被拦截已为你下载PDF文件请手动打开打印');
const a = document.createElement('a');
a.href = pdfUrl;
a.download = `外出条_${new Date().getTime()}.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// 8. 还原DOM结构和样式
// 恢复printContainer的原始样式
printContainer.style.width = '';
printContainer.style.height = '';
printContainer.style.display = '';
printContainer.style.alignItems = '';
printContainer.style.justifyContent = '';
// 把printContainer放回原位置
if (originalParent) {
if (originalNext) {
originalParent.insertBefore(printContainer, originalNext);
} else {
originalParent.appendChild(printContainer);
}
}
// 移除临时容器
document.body.removeChild(wrapper);
} catch (error) {
console.error('外出条打印准备失败:', error);
Message.error('打印内容准备失败,请重试');
// 异常时也要还原DOM避免布局错乱
const printContainer = this.$refs.printer?.querySelector('.print-content');
if (printContainer) {
printContainer.style.width = '';
printContainer.style.height = '';
printContainer.style.display = '';
printContainer.style.alignItems = '';
printContainer.style.justifyContent = '';
}
}
}
}
}
</script>
<style lang="scss" scoped>
.print-container {
width: 100%;
overflow: hidden;
padding: 20px 0;
}
// 图章
.print-stub-sign {
width: 100px;
height: 100px;
position: absolute;
bottom: 40px;
right: 60px;
}
.print-content {
display: flex;
width: fit-content;
height: fit-content;
margin: 0 auto;
padding: 20px;
position: relative;
align-items: center;
span {
display: inline-block;
min-width: 30px;
overflow: visible;
text-wrap: nowrap;
white-space: nowrap;
text-align: center;
font-size: 14px;
font-style: italic;
}
}
.print-stub-title {
font-size: 24px;
font-weight: 900;
text-align: center;
margin-bottom: 20px;
writing-mode: vertical-rl; // 文字纵向排列
}
.print-vertical-line {
width: 1px;
height: 280px;
border-left: 1px dashed #000;
margin: 0 5px;
}
.print-document,
.print-stub-content {
height: 100%;
margin: 0 auto;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.print-document-line,
.print-stub-line {
height: 50px;
min-height: 50px;
}
h4 {
text-align: center;
font-size: 24px;
font-weight: 900;
margin: 0; // 清除默认margin避免布局偏移
}
.print-document > div > div,
.print-stub-content > div > div {
border: 1px solid #000;
padding: 5px;
}
.print-apply-date {
height: 54px;
line-height: 44px;
.print-stub-date,
.print-stub-days {
line-height: 22px;
}
}
</style>

View File

@@ -40,36 +40,36 @@
<div class="custom-tabs-nav">
<div
class="custom-tabs-item"
:class="{ active: queryParams.approveStatus === undefined || queryParams.approveStatus === '' }"
@click="handleTabClick('approveStatus', '')"
:class="{ active: queryParams.approvalStatus === undefined || queryParams.approvalStatus === '' }"
@click="handleTabClick('approvalStatus', '')"
>
全部
</div>
<div
class="custom-tabs-item"
:class="{ active: queryParams.approveStatus === 'pending' }"
@click="handleTabClick('approveStatus', 'pending')"
:class="{ active: queryParams.approvalStatus === '待审批' }"
@click="handleTabClick('approvalStatus', '待审批')"
>
待审批
</div>
<div
class="custom-tabs-item"
:class="{ active: queryParams.approveStatus === 'approved' }"
@click="handleTabClick('approveStatus', 'approved')"
:class="{ active: queryParams.approvalStatus === '已同意' }"
@click="handleTabClick('approvalStatus', '已同意')"
>
审批
同意
</div>
<div
class="custom-tabs-item"
:class="{ active: queryParams.approveStatus === 'rejected' }"
@click="handleTabClick('approveStatus', 'rejected')"
:class="{ active: queryParams.approvalStatus === '已驳回' }"
@click="handleTabClick('approvalStatus', '已驳回')"
>
已驳回
</div>
<div
class="custom-tabs-item"
:class="{ active: queryParams.approveStatus === 'withdrawn' }"
@click="handleTabClick('approveStatus', 'withdrawn')"
:class="{ active: queryParams.approvalStatus === '已撤销' }"
@click="handleTabClick('approvalStatus', '已撤销')"
>
已撤回
</div>
@@ -320,10 +320,12 @@ export default {
}).then(async () => {
this.buttonLoading = true
try {
const approvalTime = this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')
// 这里替换为你的审批接口调用
await updateApproval({
approvalId: row.approvalId,
approvalStatus: '已同意',
approvalTime: approvalTime,
})
this.$message.success('审批通过成功!')
this.getTodoList() // 重新查询列表
@@ -344,10 +346,12 @@ export default {
}).then(async ({ value }) => {
this.buttonLoading = true
try {
const approvalTime = this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')
// 这里替换为你的驳回接口调用
await updateApproval({
approvalId: row.approvalId,
approvalStatus: '已驳回'
approvalStatus: '已驳回',
approvalTime: approvalTime,
})
this.$message.success('驳回成功!')
this.getTodoList() // 重新查询列表
@@ -371,7 +375,7 @@ export default {
'待审批': 'warning',
'已同意': 'success',
'已驳回': 'danger',
'已撤': 'info',
'已撤': 'info',
}
return typeMap[status] || 'default'
},
@@ -382,7 +386,7 @@ export default {
'待审批': '待审批',
'已同意': '已同意',
'已驳回': '已驳回',
'已撤': '已撤',
'已撤': '已撤',
}
return textMap[status] || '未知状态'
},
@@ -411,17 +415,11 @@ export default {
.filter-container {
background: #f5f7fa;
padding: 20px;
/* padding: 20px; */
border-radius: 4px;
height: fit-content;
}
.filter-container h4 {
margin: 0 0 20px 0;
font-size: 16px;
color: #333;
}
/* 自定义tabs样式 */
.custom-tabs {
margin-bottom: 20px;
@@ -438,14 +436,14 @@ export default {
.custom-tabs-nav {
display: flex;
flex-direction: column;
gap: 8px;
gap: 4px;
width: 100%;
}
.custom-tabs-item {
padding: 10px 15px;
text-align: center;
border-radius: 4px;
border-radius: 2px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
@@ -459,9 +457,9 @@ export default {
}
.custom-tabs-item.active {
background: #409eff;
background: #667996;
color: #ffffff;
border-color: #409eff;
border-color: #667996;
}
.detail-content {