You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

818 lines
27 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div>
<xy-dialog title="打印预览" :is-show.sync="isShow" :width="90" ok-text="" @on-ok="printHtml">
<template v-slot:normalContent>
<div style="position: relative; margin-bottom: 16px; min-height: 40px;">
<div style="width: fit-content; margin: 0 auto;">
<RadioGroup v-model="currentForm" type="button">
<!-- <Radio label="pre" :disabled="!getBeforeForms">事前审批表格</Radio> -->
<!-- <Radio label="finance" :disabled="!fundLog">财务审核表</Radio> -->
<Radio label="post" :disabled="!getForms">事后支付表格</Radio>
</RadioGroup>
</div>
<div style="position: absolute; right: 0; top: 0;">
<label>打印方向:</label>
<select v-model="printOrientation" style="margin-right: 16px;">
<option value="portrait">纵向</option>
<option value="landscape">横向</option>
</select>
</div>
</div>
<div class="white-container">
<div class="form-container">
<!-- Pre-payment Form -->
<div v-if="currentForm === 'pre'" class="payment-form">
<div v-if="getBeforeForms" v-html="getBeforeForms" />
<div v-else class="no-form-message">暂无事前审批表格内容</div>
</div>
<!-- Post-payment Form -->
<div v-else-if="currentForm === 'post'" ref="printtable" class="payment-form">
<!-- 财务审核表内容放到事后支付表头部 -->
<table class="finance-review-table no-print">
<tr>
<th colspan="2" class="finance-header-row">合同信息</th>
<th colspan="5" class="finance-header-row">付款信息</th>
</tr>
<tr>
<td class="sub-header">受款单位</td>
<td>{{ fundLog && fundLog.contract && fundLog.contract.supply || '-' }}</td>
<td class="sub-header">申请付款金额</td>
<td>{{ fundLog && fundLog.apply_money || '-' }}</td>
<td class="sub-header">实际支付金额</td>
<td>{{ fundLog && fundLog.act_money || '-' }}</td>
<td class="sub-header">款项类型</td>
</tr>
<tr>
<td class="sub-header">合同名称</td>
<td>{{ fundLog && fundLog.contract && fundLog.contract.name || '-' }}</td>
<td class="sub-header">审计金额</td>
<td>{{ fundLog && fundLog.audit_money || '-' }}</td>
<td class="sub-header">本期扣款金额</td>
<td>{{ fundLog && fundLog.discount_money || '-' }}</td>
<td>{{ fundLog && fundLog.type || '-' }}</td>
</tr>
<tr>
<td class="sub-header">合同金额</td>
<td>{{ fundLog && fundLog.contract && moneyFormat(fundLog.contract.money) || '-' }} (元)</td>
<td class="sub-header">是否为最后一笔</td>
<td>{{ fundLog && fundLog.is_end === 1 ? '是' : '否' }}</td>
<td class="sub-header">备注</td>
<td colspan="2">{{ fundLog && fundLog.remark || '-' }}</td>
</tr>
</table>
<div v-if="getForms" v-html="getForms" />
<div v-else class="no-form-message">暂无事后支付表格内容</div>
</div>
</div>
</div>
</template>
</xy-dialog>
</div>
</template>
<script>
import { detailFundLog } from '@/api/paymentRegistration/fundLog'
import html2canvas from 'html2canvas'
import * as printJS from 'print-js'
// 添加金额转大写的工具函数
function numberToChinese(num) {
const units = ['', '拾', '佰', '仟', '万', '拾', '佰', '仟', '亿', '拾', '佰', '仟', '万']
const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
const [integer, decimal] = num.toString().split('.')
let result = ''
let intNum = parseInt(integer)
if (intNum === 0) {
result = '零'
} else {
let i = 0
let lastDigit = null
let hasZero = false
while (intNum > 0) {
const digit = intNum % 10
if (digit === 0) {
if (!hasZero && lastDigit !== 0) {
result = '零' + result
hasZero = true
}
} else {
let unit = units[i]
if (i === 4 && result.startsWith('零')) {
unit = '万'
} else if (i === 8 && result.startsWith('零')) {
unit = '亿'
}
result = digits[digit] + unit + result
hasZero = false
}
lastDigit = digit
intNum = Math.floor(intNum / 10)
i++
}
result = result.replace(/零+$/, '')
result = result.replace(/零+/, '零')
}
if (decimal) {
const decimalNum = parseInt(decimal)
if (decimalNum > 0) {
result += '点'
for (let i = 0; i < decimal.length; i++) {
result += digits[parseInt(decimal[i])]
}
}
}
if (!decimal || parseInt(decimal) === 0) {
result += '整'
}
return result + '元'
}
export default {
name: 'PrintPaymentForm',
data() {
return {
isShow: false,
currentForm: 'post',
fundLog: null,
printOrientation: 'portrait' // 默认横向
}
},
computed: {
getBeforeForms() {
return this.fundLog && this.fundLog.contract && this.fundLog.contract.before_forms
},
getForms() {
return this.fundLog && this.fundLog.contract && this.fundLog.contract.forms
}
},
watch: {
getForms: {
handler(newVal) {
if (newVal) {
this.$nextTick(() => {
this.setupAmountListeners()
})
}
},
immediate: true
},
isShow: {
handler(newVal) {
if (newVal) {
this.$nextTick(() => {
this.setupAmountListeners()
})
}
},
immediate: true
}
},
methods: {
async getDetailFundLog(id) {
try {
const res = await detailFundLog({ id })
this.fundLog = res
} catch (error) {
console.error('获取付款详情失败:', error)
this.$Message.error('获取付款详情失败')
}
},
setupAmountListeners() {
const dom = this.$refs.printtable
if (!dom) return
const sdateAmountInputs = dom.querySelectorAll('input[data-field^="sdate"]')
// 移除旧的监听器
sdateAmountInputs.forEach(input => {
input.removeEventListener('input', this.caculateRoadDay)
input.removeEventListener('change', this.caculateRoadDay)
input.removeEventListener('blur', this.caculateRoadDay)
})
// 添加新的监听器
sdateAmountInputs.forEach(input => {
input.addEventListener('input', this.caculateRoadDay)
input.addEventListener('change', this.caculateRoadDay)
input.addEventListener('blur', this.caculateRoadDay)
})
const edateAmountInputs = dom.querySelectorAll('input[data-field^="edate"]')
// 移除旧的监听器
edateAmountInputs.forEach(input => {
input.removeEventListener('input', this.caculateRoadDay)
input.removeEventListener('change', this.caculateRoadDay)
input.removeEventListener('blur', this.caculateRoadDay)
})
// 添加新的监听器
edateAmountInputs.forEach(input => {
input.addEventListener('input', this.caculateRoadDay)
input.addEventListener('change', this.caculateRoadDay)
input.addEventListener('blur', this.caculateRoadDay)
})
// 获取所有以 wan 开头的输入框
const wanAmountInputs = dom.querySelectorAll('input[data-field^="wan"]')
// 移除旧的监听器
wanAmountInputs.forEach(input => {
input.removeEventListener('input', this.calculateTotal)
input.removeEventListener('change', this.calculateTotal)
input.removeEventListener('blur', this.calculateTotal)
})
// 添加新的监听器
wanAmountInputs.forEach(input => {
input.addEventListener('input', this.calculateTotal)
input.addEventListener('change', this.calculateTotal)
input.addEventListener('blur', this.calculateTotal)
})
// 获取所有以 qian 开头的输入框
const qianAmountInputs = dom.querySelectorAll('input[data-field^="qian"]')
// 移除旧的监听器
qianAmountInputs.forEach(input => {
input.removeEventListener('input', this.calculateTotal)
input.removeEventListener('change', this.calculateTotal)
input.removeEventListener('blur', this.calculateTotal)
})
// 添加新的监听器
qianAmountInputs.forEach(input => {
input.addEventListener('input', this.calculateTotal)
input.addEventListener('change', this.calculateTotal)
input.addEventListener('blur', this.calculateTotal)
})
// 获取所有以 bai 开头的输入框
const baiAmountInputs = dom.querySelectorAll('input[data-field^="bai"]')
// 移除旧的监听器
baiAmountInputs.forEach(input => {
input.removeEventListener('input', this.calculateTotal)
input.removeEventListener('change', this.calculateTotal)
input.removeEventListener('blur', this.calculateTotal)
})
// 添加新的监听器
baiAmountInputs.forEach(input => {
input.addEventListener('input', this.calculateTotal)
input.addEventListener('change', this.calculateTotal)
input.addEventListener('blur', this.calculateTotal)
})
// 获取所有以 shi 开头的输入框
const shiAmountInputs = dom.querySelectorAll('input[data-field^="shi"]')
// 移除旧的监听器
shiAmountInputs.forEach(input => {
input.removeEventListener('input', this.calculateTotal)
input.removeEventListener('change', this.calculateTotal)
input.removeEventListener('blur', this.calculateTotal)
})
// 添加新的监听器
shiAmountInputs.forEach(input => {
input.addEventListener('input', this.calculateTotal)
input.addEventListener('change', this.calculateTotal)
input.addEventListener('blur', this.calculateTotal)
})
// 获取所有以 yuan 开头的输入框
const yuanAmountInputs = dom.querySelectorAll('input[data-field^="yuan"]')
// 移除旧的监听器
yuanAmountInputs.forEach(input => {
input.removeEventListener('input', this.calculateTotal)
input.removeEventListener('change', this.calculateTotal)
input.removeEventListener('blur', this.calculateTotal)
})
// 添加新的监听器
yuanAmountInputs.forEach(input => {
input.addEventListener('input', this.calculateTotal)
input.addEventListener('change', this.calculateTotal)
input.addEventListener('blur', this.calculateTotal)
})
// 获取所有以 jiao 开头的输入框
const jiaoAmountInputs = dom.querySelectorAll('input[data-field^="jiao"]')
// 移除旧的监听器
jiaoAmountInputs.forEach(input => {
input.removeEventListener('input', this.calculateTotal)
input.removeEventListener('change', this.calculateTotal)
input.removeEventListener('blur', this.calculateTotal)
})
// 添加新的监听器
jiaoAmountInputs.forEach(input => {
input.addEventListener('input', this.calculateTotal)
input.addEventListener('change', this.calculateTotal)
input.addEventListener('blur', this.calculateTotal)
})
// 获取所有以 fen 开头的输入框
const fenAmountInputs = dom.querySelectorAll('input[data-field^="fen"]')
// 移除旧的监听器
fenAmountInputs.forEach(input => {
input.removeEventListener('input', this.calculateTotal)
input.removeEventListener('change', this.calculateTotal)
input.removeEventListener('blur', this.calculateTotal)
})
// 添加新的监听器
fenAmountInputs.forEach(input => {
input.addEventListener('input', this.calculateTotal)
input.addEventListener('change', this.calculateTotal)
input.addEventListener('blur', this.calculateTotal)
})
// 获取所有以 amount 开头的输入框
const amountInputs = dom.querySelectorAll('input[data-field^="amount"]')
console.log('找到的金额输入框:', amountInputs.length)
// 移除旧的监听器
amountInputs.forEach(input => {
input.removeEventListener('input', this.calculateTotal)
input.removeEventListener('change', this.calculateTotal)
input.removeEventListener('blur', this.calculateTotal)
})
// 添加新的监听器
amountInputs.forEach(input => {
input.addEventListener('input', this.calculateTotal)
input.addEventListener('change', this.calculateTotal)
input.addEventListener('blur', this.calculateTotal)
})
// 监听 total 输入框的变化
const totalInput = dom.querySelector('input[data-field="total"]')
if (totalInput) {
totalInput.removeEventListener('input', this.updateUpperCaseFromTotal)
totalInput.removeEventListener('change', this.updateUpperCaseFromTotal)
totalInput.removeEventListener('blur', this.updateUpperCaseFromTotal)
totalInput.addEventListener('input', this.updateUpperCaseFromTotal)
totalInput.addEventListener('change', this.updateUpperCaseFromTotal)
totalInput.addEventListener('blur', this.updateUpperCaseFromTotal)
}
// 初始计算一次
this.calculateTotal()
},
caculateRoadDay() {
const sdateInput = this.$refs.printtable.querySelector('input[data-field^="sdate"]')
const edateInput = this.$refs.printtable.querySelector('input[data-field^="edate"]')
const sdate = sdateInput.value
const edate = edateInput.value
if (sdate && edate) {
const s = new Date(sdate)
const e = new Date(edate)
// 计算天数差
const roadDay = Math.floor((e - s) / (1000 * 60 * 60 * 24))
console.log('roadDay', sdate, edate, roadDay)
const roadDayInput = this.$refs.printtable.querySelector('input[data-field="roadDay"]')
if (roadDayInput) {
roadDayInput.value = roadDay
}
}
},
calculateTotal() {
const dom = this.$refs.printtable
if (!dom) return
let wanTotal = 0
const wanAmountInputs = dom.querySelectorAll('input[data-field^="wan"]')
wanAmountInputs.forEach(input => {
const value = parseFloat(input.value) || 0
wanTotal += value
})
const wanInput = dom.querySelector('input[data-field="wTotal"]')
if (wanInput) {
wanInput.value = wanTotal===0?'':wanTotal
}
let qianTotal = 0
const qianAmountInputs = dom.querySelectorAll('input[data-field^="qian"]')
qianAmountInputs.forEach(input => {
const value = parseFloat(input.value) || 0
qianTotal += value
})
const qianInput = dom.querySelector('input[data-field="qTotal"]')
if (qianInput) {
qianInput.value = qianTotal===0?'':qianTotal
}
let baiTotal = 0
const baiAmountInputs = dom.querySelectorAll('input[data-field^="bai"]')
baiAmountInputs.forEach(input => {
const value = parseFloat(input.value) || 0
baiTotal += value
})
const baiInput = dom.querySelector('input[data-field="bTotal"]')
if (baiInput) {
baiInput.value = baiTotal===0?'':baiTotal
}
let shiTotal = 0
const shiAmountInputs = dom.querySelectorAll('input[data-field^="shi"]')
shiAmountInputs.forEach(input => {
const value = parseFloat(input.value) || 0
shiTotal += value
})
const shiInput = dom.querySelector('input[data-field="sTotal"]')
if (shiInput) {
shiInput.value = shiTotal===0?'':shiTotal
}
let yuanTotal = 0
const yuanAmountInputs = dom.querySelectorAll('input[data-field^="yuan"]')
yuanAmountInputs.forEach(input => {
const value = parseFloat(input.value) || 0
yuanTotal += value
})
const yuanInput = dom.querySelector('input[data-field="yTotal"]')
if (yuanInput) {
yuanInput.value = yuanTotal===0?'':yuanTotal
}
let jiaoTotal = 0
const jiaoAmountInputs = dom.querySelectorAll('input[data-field^="jiao"]')
jiaoAmountInputs.forEach(input => {
const value = parseFloat(input.value) || 0
jiaoTotal += value
})
const jiaoInput = dom.querySelector('input[data-field="jTotal"]')
if (jiaoInput) {
jiaoInput.value = jiaoTotal===0?'':jiaoTotal
}
let fenTotal = 0
const fenAmountInputs = dom.querySelectorAll('input[data-field^="fen"]')
fenAmountInputs.forEach(input => {
const value = parseFloat(input.value) || 0
fenTotal += value
})
const fenInput = dom.querySelector('input[data-field="fTotal"]')
if (fenInput) {
fenInput.value = fenTotal===0?'':fenTotal
}
let otherTotal = wanTotal * 10000 + qianTotal * 1000 + baiTotal * 100 + shiTotal * 10 + yuanTotal + jiaoTotal * 0.1 + fenTotal * 0.01
let total = 0
// 只计算以 amount 开头的输入框
const amountInputs = dom.querySelectorAll('input[data-field^="amount"]')
console.log('计算总金额,找到输入框数量:', amountInputs.length)
amountInputs.forEach(input => {
const value = parseFloat(input.value) || 0
console.log('输入框值:', input.getAttribute('data-field'), value)
total += value
})
console.log('计算得到的总金额:', total)
// 更新总金额输入框
const totalInput = dom.querySelector('input[data-field="total"]')
if (totalInput) {
totalInput.value = total.toFixed(2)
console.log('更新总金额输入框:', totalInput.value)
}
// 更新大写金额
const upperCaseInput = dom.querySelector('input[data-field="upperCaseAmount"]')
if (upperCaseInput) {
upperCaseInput.value = numberToChinese(total)
console.log('更新大写金额:', upperCaseInput.value)
}
if (otherTotal !== 0) {
upperCaseInput.value = numberToChinese(otherTotal)
}
},
updateUpperCaseFromTotal() {
const dom = this.$refs.printtable
if (!dom) return
const totalInput = dom.querySelector('input[data-field="total"]')
const upperCaseInput = dom.querySelector('input[data-field="upperCaseAmount"]')
if (totalInput && upperCaseInput) {
const total = parseFloat(totalInput.value) || 0
upperCaseInput.value = numberToChinese(total)
console.log('从总金额更新大写金额:', total, upperCaseInput.value)
}
},
replaceControls(element) {
const inputs = element.getElementsByTagName('input')
Array.from(inputs).forEach(input => {
if (input.type === 'checkbox' || input.type === 'radio') {
// 跳过checkbox和radio不替换保持原样
return
}
const span = document.createElement('span')
let displayText = input.value || ''
if (input.type === 'date') {
displayText = input.value ? new Date(input.value).toLocaleDateString() : ''
}
span.textContent = displayText
const style = window.getComputedStyle(input)
span.style.cssText = style.cssText
;[
'width',
'height',
'padding',
'margin',
'font',
'fontSize',
'fontFamily',
'lineHeight',
'verticalAlign',
'border',
'background',
'color',
'boxSizing'
].forEach(key => {
span.style[key] = style[key]
})
span.style.width = '100%'
span.style.display = 'block'
span.style.textAlign = 'center'
input.parentNode.replaceChild(span, input)
})
const selects = element.getElementsByTagName('select')
Array.from(selects).forEach(select => {
const span = document.createElement('span')
span.textContent = select.options[select.selectedIndex]?.text || ''
const style = window.getComputedStyle(select)
span.style.cssText = style.cssText
;[
'width',
'height',
'padding',
'margin',
'font',
'fontSize',
'fontFamily',
'lineHeight',
'verticalAlign',
'border',
'background',
'color',
'boxSizing'
].forEach(key => {
span.style[key] = style[key]
})
span.style.width = '100%'
span.style.display = 'block'
span.style.textAlign = 'center'
select.parentNode.replaceChild(span, select)
})
const textareas = element.getElementsByTagName('textarea')
Array.from(textareas).forEach(textarea => {
const span = document.createElement('span')
span.textContent = textarea.value || ''
const style = window.getComputedStyle(textarea)
span.style.cssText = style.cssText
;[
'width',
'height',
'padding',
'margin',
'font',
'fontSize',
'fontFamily',
'lineHeight',
'verticalAlign',
'border',
'background',
'color',
'boxSizing'
].forEach(key => {
span.style[key] = style[key]
})
span.style.width = '100%'
span.style.display = 'block'
span.style.textAlign = 'center'
textarea.parentNode.replaceChild(span, textarea)
})
},
async print() {
try {
const tempContainer = document.createElement('div')
tempContainer.style.position = 'absolute'
tempContainer.style.left = '-9999px'
tempContainer.style.top = '-9999px'
document.body.appendChild(tempContainer)
const originalContent = this.$refs['printtable'].cloneNode(true)
tempContainer.appendChild(originalContent)
this.replaceControls(tempContainer)
const canvas = await html2canvas(tempContainer, {
backgroundColor: null,
useCORS: true
})
document.body.removeChild(tempContainer)
printJS({
printable: canvas.toDataURL(),
type: 'image',
documentTitle: `苏州市河道管理处${this.currentForm === 'pre' ? '事前审批表格' : this.currentForm === 'post' ? '事后支付表格' : '财务审核表'}`,
style: '@page{margin:auto;}'
})
} catch (error) {
console.error('打印失败:', error)
this.$Message.error('打印失败')
}
},
moneyFormat(val) {
if (!val && val !== 0) return '-'
return Number(val).toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,')
},
printHtml() {
const printNode = this.$refs.printtable.cloneNode(true)
// 移除财务审核表
const financeTable = printNode.querySelector('.finance-review-table')
if (financeTable) {
financeTable.remove()
}
// 替换控件为纯文本(保留样式)
this.replaceControls(printNode)
// 计算并更新总金额
const amountInputs = printNode.querySelectorAll('input[data-field^="amount"]')
let total = 0
amountInputs.forEach(input => {
const value = parseFloat(input.value) || 0
total += value
})
// 更新总金额显示
const totalInput = printNode.querySelector('input[data-field="total"]')
if (totalInput) {
totalInput.value = total.toFixed(2)
}
// 更新大写金额显示
const upperCaseInput = printNode.querySelector('input[data-field="upperCaseAmount"]')
if (upperCaseInput) {
upperCaseInput.value = numberToChinese(total)
}
const orientation = this.printOrientation || 'portrait';
const margin = orientation === 'portrait'
? '2mm 10mm 10mm 2mm'
: '10mm 10mm 10mm 10mm';
const win = window.open('', '_blank')
win.document.write(`
<html>
<head>
<title>打印</title>
<style>
@page { size: A4 ${orientation}; margin: ${margin}; }
body {
margin: 0;
padding: 0;
}
.white-container {
width: 100% !important;
margin: 0 auto;
margin-left: 0px;
background: #fff;
padding: 0px;
box-sizing: border-box;
}
.form-container {
width: 100% !important;
margin: 0 auto;
}
table {
width: 100%;
border-collapse: collapse;
}
td {
border: 1px solid #000;
padding: 8px;
}
</style>
</head>
<body>
<div class="white-container">
<div class="form-container">
${printNode.innerHTML}
</div>
</div>
</body>
</html>
`)
win.document.close()
win.focus()
win.print()
win.close()
}
}
}
</script>
<style scoped lang="scss">
.form-switch {
margin-bottom: 20px;
text-align: center;
}
.white-container {
background: #fff;
padding: 20px;
}
.form-container {
position: relative;
width: 100%;
padding: 20px;
font-family: SimSun, serif;
}
.payment-form {
border: 1px solid #000;
padding: 20px;
}
.no-form-message {
text-align: center;
padding: 40px;
color: #999;
font-size: 16px;
}
.finance-review-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
font-size: 15px;
background: #fff;
th, td {
border: 1px solid #e0e0e0;
padding: 10px 8px;
text-align: center;
}
th.finance-header-row {
background: #eaf3ff;
color: #2d8cf0;
font-size: 16px;
font-weight: 600;
}
.sub-header {
background: #f5f7fa;
color: #333;
font-weight: 600;
width: 120px;
}
}
.finance-review-header {
margin-bottom: 16px;
}
.payment-registration-row {
display: flex;
margin-bottom: 4px;
}
.payment-registration-row-title {
width: 100px;
font-weight: bold;
color: #333;
}
.payment-registration-row-content {
flex: 1;
color: #666;
}
@media print {
.white-container {
padding: 0;
}
.form-container {
padding: 0;
}
.payment-form {
border: 1px solid #000;
}
.no-print {
display: none !important;
}
}
</style>