金额大写

master
lion 1 week ago
parent 3c115a7610
commit f645540906

@ -160,10 +160,11 @@ export default {
{
if (type === 'form') return ($scopedSlots.footerContent ? $scopedSlots.footerContent() : footerRender())
if (type === 'normal') {
if ($scopedSlots.footerContent) return $scopedSlots.footerContent()
return (
<div>
<Button ghost type='primary' on-click={cancelClick}>取消</Button>
<Button type='primary' on-click={okClick}>确定</Button>
<Button type='primary' on-click={okClick}>{okText || '确定'}</Button>
</div>
)
}

@ -227,85 +227,100 @@ export function buildTree(data, pid = 0) {
return tree
}
/**
* 09999 转为中文含拾//不含万/亿
*/
function convertSection(section) {
const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
const units = ['', '拾', '佰', '仟']
if (section === 0) return ''
let str = ''
let zeroFlag = false
let started = false
for (let i = 3; i >= 0; i--) {
const d = Math.floor(section / Math.pow(10, i)) % 10
if (d === 0) {
if (started) zeroFlag = true
} else {
if (zeroFlag) {
str += '零'
zeroFlag = false
}
if (i === 1 && d === 1 && Math.floor(section / 100) === 0) {
str += units[i]
} else {
str += digits[d] + units[i]
}
started = true
}
}
return str
}
/**
* 数字转中文大写金额
* @param {number} num - 要转换的数字
* @param {number|string} num - 要转换的数字
* @returns {string} 中文大写金额
*/
export function numberToChinese(num) {
if (num === 0) {
const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
const bigUnits = ['', '万', '亿', '兆']
const n = parseFloat(String(num).replace(/[¥¥\s,]/g, ''))
if (!Number.isFinite(n) || n === 0) {
return '零元整'
}
const units = ['', '拾', '佰', '仟', '万', '拾', '佰', '仟', '亿', '拾', '佰', '仟', '万']
const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
const [integer, decimal] = num.toString().split('.')
const fixed = n.toFixed(2)
const [intStr, decStr] = fixed.split('.')
let intNum = parseInt(intStr, 10)
const groups = []
let temp = intNum
while (temp > 0) {
groups.push(temp % 10000)
temp = Math.floor(temp / 10000)
}
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]
// 处理"拾"的特殊情况当十位是1时不写"壹拾",只写"拾"
if (i === 1 && digit === 1) {
result = unit + result
} else {
result = digits[digit] + unit + result
}
hasZero = false
}
lastDigit = digit
intNum = Math.floor(intNum / 10)
i++
let needZero = false
for (let i = 0; i < groups.length; i++) {
const section = groups[i]
if (section === 0) {
if (result) needZero = true
continue
}
// 清理末尾的零
result = result.replace(/零+$/, '')
// 清理连续的零
result = result.replace(/零+/g, '零')
}
// 先加上"元"单位
result += '元'
// 处理小数部分
if (decimal) {
const decimalNum = parseInt(decimal)
if (decimalNum > 0) {
const jiao = Math.floor(decimalNum / 10)
const fen = decimalNum % 10
if (jiao > 0) {
result += digits[jiao] + '角'
}
if (fen > 0) {
result += digits[fen] + '分'
}
return result
const sectionStr = convertSection(section)
let prefix = sectionStr + bigUnits[i]
if (needZero) {
result = prefix + '零' + result
needZero = false
} else {
const lower = groups.slice(0, i).reduce((sum, g, idx) => sum + g * Math.pow(10000, idx), 0)
const padZero = i > 0 && lower > 0 && section < 1000
result = prefix + (padZero ? '零' : '') + result
}
}
// 如果没有小数部分,添加"整"
if (!decimal || parseInt(decimal) === 0) {
if (!result) {
result = '零'
}
result += '元'
const jiao = parseInt(decStr[0] || '0', 10)
const fen = parseInt(decStr[1] || '0', 10)
if (jiao === 0 && fen === 0) {
result += '整'
} else {
if (jiao > 0) {
result += digits[jiao] + '角'
} else if (fen > 0) {
result += '零'
}
if (fen > 0) {
result += digits[fen] + '分'
}
}
return result
}

@ -1,7 +1,13 @@
<template>
<div>
<xy-dialog title="打印预览" :is-show.sync="isShow" :width="90" ok-text="" @on-ok="printHtml">
<xy-dialog
:title="editMode ? '编辑付款登记' : '打印预览'"
: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;">
@ -62,30 +68,48 @@
<td colspan="2">{{ fundLog && fundLog.remark || '-' }}</td>
</tr>
</table>
<div v-if="getForms" v-html="getForms" />
<div v-if="getForms" :key="formsRenderKey" v-html="getForms" />
<div v-else class="no-form-message">暂无事后支付表格内容</div>
</div>
</div>
</div>
</template>
<template v-if="editMode" v-slot:footerContent>
<Button ghost type="primary" @click="handleCancel"></Button>
<Button type="primary" :loading="saving" style="margin-left: 8px" @click="handleSave"></Button>
<Button type="primary" :loading="saving" style="margin-left: 8px" @click="handleSaveAndPrint"></Button>
</template>
</xy-dialog>
</div>
</template>
<script>
import { detailFundLog } from '@/api/paymentRegistration/fundLog'
import { detailFundLog, editorFundLog } from '@/api/paymentRegistration/fundLog'
import html2canvas from 'html2canvas'
import * as printJS from 'print-js'
import { numberToChinese } from '@/utils'
/** 资金划拨等表单:金额字段 -> 大写字段 */
const AMOUNT_REMARK_PAIRS = [
['contractAmount', 'contractRemark'],
['auditAmount', 'auditRemark'],
['previousPayment', 'previousPaymentRemark'],
['totalPaid', 'totalPaidRemark'],
['totalPlanned', 'totalPlannedRemark']
]
export default {
name: 'PrintPaymentForm',
data() {
return {
isShow: false,
editMode: false,
saving: false,
currentForm: 'post',
fundLog: null,
printOrientation: 'portrait' //
formsRenderKey: 0,
printOrientation: 'portrait', //
_uppercaseDomCleanups: null
}
},
computed: {
@ -131,25 +155,241 @@ export default {
}
}
})
} else {
this.editMode = false
this.detachConfigurableUppercase(this.$refs.printtable)
//
this.fundLog = null
}
},
immediate: true
}
},
methods: {
async openForPrint(id) {
this.editMode = false
await this.getDetailFundLog(id)
this.currentForm = 'post'
this.isShow = true
},
async openForEdit(id) {
this.editMode = true
await this.getDetailFundLog(id)
this.currentForm = 'post'
this.isShow = true
},
async getDetailFundLog(id) {
try {
this.fundLog = null
const res = await detailFundLog({ id })
this.fundLog = res
this.formsRenderKey += 1
} catch (error) {
console.error('获取付款详情失败:', error)
this.$Message.error('获取付款详情失败')
throw error
}
},
handleCancel() {
this.isShow = false
},
async handleSave() {
try {
await this.syncAndSave()
this.$Message.success('保存成功')
this.$emit('saved')
this.isShow = false
} catch (error) {
console.error('保存失败:', error)
this.$Message.error('保存失败,请重试')
}
},
async handleSaveAndPrint() {
try {
await this.syncAndSave()
this.$Message.success('保存成功')
this.$emit('saved')
this.printHtml()
} catch (error) {
console.error('保存失败:', error)
this.$Message.error('保存失败,请重试')
}
},
parseMoneyForUppercase(raw) {
if (raw == null) return { kind: 'empty' }
const s = String(raw).trim()
if (s === '') return { kind: 'empty' }
const cleaned = s.replace(/[¥¥\s,]/g, '')
if (cleaned === '') return { kind: 'empty' }
const n = parseFloat(cleaned)
if (!Number.isFinite(n)) return { kind: 'invalid' }
return { kind: 'ok', value: n }
},
getSourceFieldRawValue(dom, fieldName) {
if (!dom || !fieldName) return ''
const radios = dom.querySelectorAll(`input[type="radio"][data-field="${fieldName}"]`)
if (radios.length) {
const c = dom.querySelector(`input[type="radio"][data-field="${fieldName}"]:checked`)
return c ? String(c.value) : ''
}
const el = dom.querySelector(`input[data-field="${fieldName}"],select[data-field="${fieldName}"],textarea[data-field="${fieldName}"]`)
if (!el) return ''
if (el.type === 'checkbox') {
const c = dom.querySelector(`input[type="checkbox"][data-field="${fieldName}"]:checked`)
return c ? String(c.value) : ''
}
return el.value != null ? String(el.value) : ''
},
setFieldValue(dom, fieldName, text) {
if (!dom || !fieldName) return
const els = dom.querySelectorAll(`input[data-field="${fieldName}"],textarea[data-field="${fieldName}"],select[data-field="${fieldName}"]`)
els.forEach((el) => {
if (el.type === 'radio' || el.type === 'checkbox') return
el.value = text
})
},
applyConfigurableUppercase(dom) {
const templateFields = this.fundLog && this.fundLog.other_data
if (!dom || !templateFields || !templateFields.length) return
templateFields.forEach((meta) => {
const conf = meta.uppercase_source
if (!conf || conf.mode !== 'field' || !conf.source_field) return
const src = conf.source_field
if (src === meta.field) return
const raw = this.getSourceFieldRawValue(dom, src)
const parsed = this.parseMoneyForUppercase(raw)
const val = parsed.kind === 'ok' ? numberToChinese(parsed.value) : ''
this.setFieldValue(dom, meta.field, val)
})
},
syncAmountRemarkUppercase(dom) {
if (!dom) return
AMOUNT_REMARK_PAIRS.forEach(([amountField, remarkField]) => {
const amountInput = dom.querySelector(`input[data-field="${amountField}"]`)
const remarkInput = dom.querySelector(`input[data-field="${remarkField}"]`)
if (!amountInput || !remarkInput) return
const parsed = this.parseMoneyForUppercase(amountInput.value)
remarkInput.value = parsed.kind === 'ok' ? numberToChinese(parsed.value) : ''
})
},
syncAllUppercase(dom) {
if (!dom) return
this.calculateTotal()
this.applyConfigurableUppercase(dom)
this.syncAmountRemarkUppercase(dom)
},
detachConfigurableUppercase(dom) {
if (!this._uppercaseDomCleanups || !dom) return
const cleanup = this._uppercaseDomCleanups.get(dom)
if (cleanup) {
cleanup()
this._uppercaseDomCleanups.delete(dom)
}
},
attachConfigurableUppercase(dom) {
if (!dom) return
if (!this._uppercaseDomCleanups) {
this._uppercaseDomCleanups = new Map()
}
const sourceFields = new Set()
const templateFields = this.fundLog && this.fundLog.other_data
if (templateFields && templateFields.length) {
templateFields.forEach(m => {
const cfg = m.uppercase_source
const s = cfg && cfg.mode === 'field' && cfg.source_field
if (s) sourceFields.add(s)
})
}
AMOUNT_REMARK_PAIRS.forEach(([amountField]) => sourceFields.add(amountField))
if (sourceFields.size > 0) {
const handler = () => {
this.syncAllUppercase(dom)
}
const types = ['input', 'change', 'blur']
types.forEach(type => dom.addEventListener(type, handler, true))
const cleanup = () => {
types.forEach(type => dom.removeEventListener(type, handler, true))
}
this._uppercaseDomCleanups.set(dom, cleanup)
}
this.syncAllUppercase(dom)
},
syncFormDomToHtml() {
const dom = this.$refs.printtable
if (!dom || !this.fundLog) return
const templateFields = this.fundLog.other_data
const inputs = dom.querySelectorAll('input, select, textarea')
inputs.forEach(input => {
const fieldName = input.getAttribute('data-field')
if (!fieldName) return
if (templateFields && Array.isArray(templateFields)) {
const field = templateFields.find(f => f.field === fieldName)
if (field) {
if (input.type === 'checkbox' || input.type === 'radio') {
const checkedInput = dom.querySelector(`[data-field="${fieldName}"]:checked`)
field.value = checkedInput ? checkedInput.value : ''
if (checkedInput) {
checkedInput.setAttribute('checked', 'checked')
}
} else if (input.tagName.toLowerCase() === 'textarea') {
field.value = input.value || input.textContent || ''
input.textContent = field.value
} else {
field.value = input.value
input.setAttribute('value', input.value)
}
}
} else if (input.type === 'checkbox' || input.type === 'radio') {
const checkedInput = dom.querySelector(`[data-field="${fieldName}"]:checked`)
if (checkedInput) {
checkedInput.setAttribute('checked', 'checked')
}
} else if (input.tagName.toLowerCase() === 'textarea') {
input.textContent = input.value || ''
} else {
input.setAttribute('value', input.value)
}
})
this.fundLog.forms = dom.innerHTML
},
async syncAndSave() {
if (!this.fundLog || !this.fundLog.id) {
throw new Error('缺少付款登记数据')
}
const dom = this.$refs.printtable
if (dom) {
this.syncAllUppercase(dom)
this.syncFormDomToHtml()
}
this.saving = true
try {
await editorFundLog({
id: this.fundLog.id,
contract_id: this.fundLog.contract_id,
apply_money: this.fundLog.apply_money,
act_money: this.fundLog.act_money,
discount_money: this.fundLog.discount_money,
audit_money: this.fundLog.audit_money,
type: this.fundLog.type,
is_end: this.fundLog.is_end,
remark: this.fundLog.remark,
act_date: this.fundLog.act_date,
forms: this.fundLog.forms,
other_data: this.fundLog.other_data,
before_forms: this.fundLog.before_forms,
before_other_data: this.fundLog.before_other_data
})
} finally {
this.saving = false
}
},
setupAmountListeners() {
const dom = this.$refs.printtable
if (!dom) return
this.detachConfigurableUppercase(dom)
const sdateAmountInputs = dom.querySelectorAll('input[data-field^="sdate"]')
//
sdateAmountInputs.forEach(input => {
@ -321,8 +561,7 @@ export default {
totalInput.addEventListener('blur', this.updateUpperCaseFromTotal)
}
//
this.calculateTotal()
this.attachConfigurableUppercase(dom)
},
caculateRoadDay() {
const sdateInput = this.$refs.printtable.querySelector('input[data-field^="sdate"]')
@ -528,9 +767,12 @@ export default {
console.log('更新大写金额:', upperCaseInput.value)
}
if (otherTotal !== 0) {
upperCaseInput.value = numberToChinese(otherTotal)
if (upperCaseInput && otherTotal !== 0) {
upperCaseInput.value = numberToChinese(otherTotal)
}
this.applyConfigurableUppercase(dom)
this.syncAmountRemarkUppercase(dom)
},
updateUpperCaseFromTotal() {
const dom = this.$refs.printtable
@ -544,6 +786,8 @@ export default {
upperCaseInput.value = numberToChinese(total)
console.log('从总金额更新大写金额:', total, upperCaseInput.value)
}
this.applyConfigurableUppercase(dom)
this.syncAmountRemarkUppercase(dom)
},
replaceControls(element) {
const inputs = element.getElementsByTagName('input')
@ -717,6 +961,7 @@ export default {
// HTML
const dom = this.$refs.printtable
if (dom) {
this.syncAllUppercase(dom)
//
const inputs = dom.querySelectorAll('input, select, textarea')
inputs.forEach(input => {
@ -788,26 +1033,6 @@ export default {
this.replaceControls(printNode)
console.log('printNode', printNode.innerHTML)
//
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'
? '5mm 5mm 10mm 2mm' //

@ -578,6 +578,15 @@
<el-input-number v-model="form.price" :min="0" :precision="2" :step="1000" style="width: 100%" />
</el-form-item>
<el-form-item
v-show="showContractSignPrice"
label="合同签订价(元)"
prop="money"
:rules="[{ required: true, message: '请输入合同签订价', trigger: 'submit' }]"
>
<el-input-number v-model="form.money" :min="0" :precision="2" :step="1000" style="width: 100%" />
</el-form-item>
<el-form-item v-show="showFields.fundChannel" label="资金渠道" prop="moneyWay" :rules="[{ required: true, message: '请选择资金渠道', trigger: 'submit' }]">
<div class="money-way-tree-container">
<el-tree
@ -1771,6 +1780,7 @@ export default {
plan: [],
plan_display: '', //
price: 0,
money: 0,
supply: '',
//
is_simple: 0, //
@ -1842,6 +1852,13 @@ export default {
formType: 'contract' //
}
},
computed: {
// - ()
showContractSignPrice() {
const name = this.getAffairTypeName(this.form.affairType)
return name === '项目采购(无合同)' || (name && name.includes('项目采购') && name.includes('无合同'))
}
},
mounted() {
this.window.width = screen.availWidth * 0.95
this.window.height = screen.availHeight * 0.95
@ -2463,6 +2480,11 @@ export default {
visibleFields.push('price')
}
// ()
if (this.showContractSignPrice) {
visibleFields.push('money')
}
//
if (this.showFields.fundChannel) {
visibleFields.push('moneyWay')
@ -2502,6 +2524,7 @@ export default {
name: '项目名称',
type: '项目类型',
price: '合同预算金额',
money: '合同签订价',
moneyWay: '资金渠道',
plan: '关联预算计划',
supply: '付款对象'
@ -2829,6 +2852,9 @@ export default {
this.form.contractType = ''
this.form.purchaseForm = ''
this.form.purchaseMethod = ''
if (!this.showContractSignPrice) {
this.form.money = 0
}
this.updateTypeOptions()
},
@ -3297,6 +3323,7 @@ export default {
plan: [],
plan_display: '',
price: 0,
money: 0,
supply: '',
is_simple: 0,
has_charge: 0,
@ -3723,6 +3750,7 @@ export default {
is_substitute: detail.is_substitute,
supply: detail.supply,
price: detail.plan_price,
money: detail.money,
name: detail.name,
moneyWay: detail.money_way_id ? detail.money_way_id.split(',').map(Number) : [],
plan: detail.plans.map(item => {
@ -3794,6 +3822,7 @@ export default {
is_substitute: detail.is_substitute,
supply: detail.supply,
price: detail.plan_price,
money: detail.money,
name: detail.name,
moneyWay: detail.money_way_id ? detail.money_way_id.split(',').map(Number) : [],
plan: detail.plans.map(item => {

@ -179,11 +179,17 @@
<el-table-column
label="操作"
fixed="right"
width="160"
width="240"
header-align="center"
>
<template slot-scope="scope">
<template v-if="scope.row.status === 0">
<Button
size="small"
type="primary"
style="margin-left: 10px; margin-bottom: 4px"
@click="handleEdit(scope.row)"
>编辑</Button>
<Button
size="small"
type="primary"
@ -228,7 +234,7 @@
ref="examineRegistration"
@refresh="getFundLogs"
/>
<printPaymentForm ref="printPaymentForm" />
<printPaymentForm ref="printPaymentForm" @saved="getFundLogs" />
</div>
</template>
@ -606,12 +612,19 @@ export default {
})
},
async handleEdit(row) {
try {
await this.$refs['printPaymentForm'].openForEdit(row.id)
} catch (err) {
console.error('编辑错误:', err)
this.$Message.error('打开编辑失败,请重试')
}
},
async handlePrint(row) {
try {
// if (row.contract && (row.contract.forms || row.contract.before_forms)) {
await this.$refs['printPaymentForm'].getDetailFundLog(row.id)
this.$refs['printPaymentForm'].currentForm = 'post'
this.$refs['printPaymentForm'].isShow = true
await this.$refs['printPaymentForm'].openForPrint(row.id)
// return;
// }
// this.$Message.warning('');

Loading…
Cancel
Save