Files
mom-web/src/views/erp/finance/bookkeeping/BookkeepingVoucherForm.vue

413 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 移动端布局 -->
<el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
<div class="mobile-form" v-loading="formLoading">
<el-form ref="formRef" :model="formData" :rules="formRules" :disabled="isDetail" label-position="top">
<div class="mobile-form__section">
<div class="mobile-form__section-title">基本信息</div>
<el-form-item label="凭证编号" prop="voucherNo"><el-input v-model="formData.voucherNo" disabled placeholder="系统自动生成" /></el-form-item>
<el-form-item v-if="!isDetail" label="制单日期" prop="voucherDate"><el-date-picker v-model="formData.voucherDate" type="date" placeholder="选择制单日期" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
<el-form-item label="系统名" prop="systemName"><el-input v-model="formData.systemName" placeholder="请输入系统名" /></el-form-item>
<el-form-item label="摘要" prop="summary"><el-input v-model="formData.summary" placeholder="请输入摘要" @input="syncSummaryToItems" /></el-form-item>
<el-form-item v-if="formType !== 'create'" label="审核状态" prop="status"><dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="formData.status" /></el-form-item>
<el-form-item label="备注" prop="remark"><el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="请输入备注" /></el-form-item>
</div>
<div class="mobile-form__section">
<div class="mobile-form__section-title">凭证明细</div>
<div v-for="(item, index) in formData.items" :key="index" class="mobile-item-card">
<div class="mobile-item-card__header">
<span class="mobile-item-card__index">#{{ index + 1 }}</span>
<span class="mobile-item-card__name">{{ item.subjectName || '未选择科目' }}</span>
<el-button v-if="!isDetail" :disabled="isDetail" @click="removeItem(index)" link type="danger" size="small">删除</el-button>
</div>
<div class="mobile-item-card__body">
<el-form-item label="摘要"><el-input v-model="item.summary" placeholder="请输入摘要" :disabled="isDetail" /></el-form-item>
<el-form-item label="科目名称"><el-tree-select v-model="item.subjectId" :data="subjectTree" :props="{ label: 'name', value: 'id', children: 'children' }" placeholder="请选择科目名称" check-strictly filterable :disabled="isDetail" style="width:100%" @change="(val) => handleSubjectChange(item, val)" /></el-form-item>
<el-form-item label="借方金额"><el-input-number v-model="item.debitAmount" :precision="2" :min="0" :controls="false" placeholder="借方金额" :disabled="isDetail || (item.creditAmount > 0)" style="width:100%" @change="(val) => handleDebitChange(item, val)" /></el-form-item>
<el-form-item label="贷方金额"><el-input-number v-model="item.creditAmount" :precision="2" :min="0" :controls="false" placeholder="贷方金额" :disabled="isDetail || (item.debitAmount > 0)" style="width:100%" @change="(val) => handleCreditChange(item, val)" /></el-form-item>
</div>
</div>
<div class="mobile-item-add" v-if="!isDetail"><el-button @click="addItem" round>+ 添加明细</el-button></div>
<div class="mobile-item-summary">
<div class="mobile-item-summary__row"><span>借方合计</span><span class="text-red-500">{{ formatAmount(debitTotal) }}</span></div>
<div class="mobile-item-summary__row"><span>贷方合计</span><span class="text-blue-500">{{ formatAmount(creditTotal) }}</span></div>
</div>
<el-alert v-if="!isBalanced && formData.items.length > 0" title="借贷不平衡,请检查金额" type="warning" :closable="false" class="mt-10px" />
</div>
</el-form>
</div>
<template #footer>
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false" style="flex:1"> </el-button>
<el-button v-if="!isDetail" type="primary" :disabled="formLoading" @click="submitForm" style="flex:2"> </el-button>
</div>
</template>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else v-model="dialogVisible" :title="dialogTitle" width="900px">
<el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" :disabled="isDetail" label-width="100px">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="凭证编号" prop="voucherNo">
<el-input v-model="formData.voucherNo" disabled placeholder="系统自动生成" />
</el-form-item>
</el-col>
<el-col :span="8" v-if="!isDetail">
<el-form-item label="制单日期" prop="voucherDate">
<el-date-picker
v-model="formData.voucherDate"
type="date"
placeholder="选择制单日期"
value-format="YYYY-MM-DD"
class="!w-full"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="系统名" prop="systemName">
<el-input v-model="formData.systemName" placeholder="请输入系统名" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="16">
<el-form-item label="摘要" prop="summary">
<el-input v-model="formData.summary" placeholder="请输入摘要" @input="syncSummaryToItems" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="审核状态" prop="status" v-if="formType !== 'create'">
<dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="formData.status" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
<!-- 凭证明细 -->
<el-divider content-position="left">凭证明细</el-divider>
<el-table :data="formData.items" border style="width: 100%">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="摘要" min-width="150">
<template #default="{ row }">
<el-input v-model="row.summary" placeholder="请输入摘要" :disabled="isDetail" />
</template>
</el-table-column>
<el-table-column label="科目名称" min-width="200">
<template #default="{ row }">
<el-tree-select
v-model="row.subjectId"
:data="subjectTree"
:props="{ label: 'name', value: 'id', children: 'children' }"
placeholder="请选择科目名称"
check-strictly
filterable
:disabled="isDetail"
class="!w-full"
@change="(val) => handleSubjectChange(row, val)"
/>
</template>
</el-table-column>
<el-table-column label="借方金额" width="140">
<template #default="{ row }">
<el-input-number
v-model="row.debitAmount"
:precision="2"
:min="0"
:controls="false"
placeholder="借方金额"
:disabled="isDetail || (row.creditAmount > 0)"
class="!w-full"
@change="(val) => handleDebitChange(row, val)"
/>
</template>
</el-table-column>
<el-table-column label="贷方金额" width="140">
<template #default="{ row }">
<el-input-number
v-model="row.creditAmount"
:precision="2"
:min="0"
:controls="false"
placeholder="贷方金额"
:disabled="isDetail || (row.debitAmount > 0)"
class="!w-full"
@change="(val) => handleCreditChange(row, val)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center" v-if="!isDetail">
<template #default="{ $index }">
<el-button link type="danger" @click="removeItem($index)">
<Icon icon="ep:delete" />
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加明细按钮和合计 -->
<el-row :gutter="20" class="mt-10px">
<el-col :span="12">
<el-button v-if="!isDetail" type="primary" plain @click="addItem">
<Icon icon="ep:plus" class="mr-5px" />
添加明细
</el-button>
</el-col>
<el-col :span="12" class="text-right">
<span class="mr-20px">借方合计: <strong class="text-red-500">{{ formatAmount(debitTotal) }}</strong></span>
<span>贷方合计: <strong class="text-blue-500">{{ formatAmount(creditTotal) }}</strong></span>
</el-col>
</el-row>
<!-- 借贷平衡提示 -->
<el-alert
v-if="!isBalanced && formData.items.length > 0"
title="借贷不平衡,请检查金额"
type="warning"
:closable="false"
class="mt-10px"
/>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button v-if="!isDetail" type="primary" :disabled="formLoading" @click="submitForm">
</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import * as BookkeepingVoucherApi from '@/api/erp/finance/bookkeeping'
import { SubjectApi } from '@/api/erp/finance/subject'
import { useWindowSize } from '@vueuse/core'
defineOptions({ name: 'BookkeepingVoucherForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中
const formType = ref('') // 表单的类型create - 新增update - 修改detail - 详情
const formData = ref({
id: undefined,
voucherNo: '',
voucherDate: '',
summary: '',
systemName: '',
remark: '',
status: undefined,
items: [] as any[]
})
const formRules = reactive({
voucherDate: [{ required: true, message: '制单日期不能为空', trigger: 'blur' }],
summary: [{ required: true, message: '摘要不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const subjectTree = ref<any[]>([]) // 科目名称树
// 计算属性
const isDetail = computed(() => formType.value === 'detail')
const debitTotal = ref(0)
const creditTotal = ref(0)
const isBalanced = computed(() => {
return Math.abs(debitTotal.value - creditTotal.value) < 0.01
})
/** 格式化金额 */
const formatAmount = (amount: number) => {
return amount.toFixed(2)
}
/** 计算合计 */
const calculateTotal = () => {
debitTotal.value = formData.value.items.reduce((sum, item) => {
return sum + (item.debitAmount || 0)
}, 0)
creditTotal.value = formData.value.items.reduce((sum, item) => {
return sum + (item.creditAmount || 0)
}, 0)
}
/** 借方金额变更 - 清空贷方金额 */
const handleDebitChange = (row: any, val: number) => {
if (val && val > 0) {
row.creditAmount = undefined
}
calculateTotal()
}
/** 贷方金额变更 - 清空借方金额 */
const handleCreditChange = (row: any, val: number) => {
if (val && val > 0) {
row.debitAmount = undefined
}
calculateTotal()
}
/** 添加明细 */
const addItem = () => {
formData.value.items.push({
summary: formData.value.summary || '', // 自动使用主表摘要
subjectId: undefined,
subjectName: '',
debitAmount: undefined,
creditAmount: undefined
})
}
/** 同步主表摘要到明细表 */
const syncSummaryToItems = (val: string) => {
// 将主表摘要同步到所有明细行
formData.value.items.forEach(item => {
item.summary = val
})
}
/** 删除明细 */
const removeItem = (index: number) => {
formData.value.items.splice(index, 1)
calculateTotal()
}
/** 科目变更 */
const handleSubjectChange = (row: any, val: number) => {
// 查找科目名称
const findSubject = (tree: any[], id: number): any => {
for (const item of tree) {
if (item.id === id) {
return item
}
if (item.children && item.children.length > 0) {
const found = findSubject(item.children, id)
if (found) return found
}
}
return null
}
const subject = findSubject(subjectTree.value, val)
if (subject) {
row.subjectName = subject.name
}
}
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = type === 'create' ? '新增记账凭证' : type === 'update' ? '编辑记账凭证' : '记账凭证详情'
formType.value = type
resetForm()
// 加载科目名称树
try {
subjectTree.value = await SubjectApi.getSubjectTree()
} catch (e) {
console.error('加载科目名称失败', e)
}
// 修改或详情时,加载数据
if (id) {
formLoading.value = true
try {
const data = await BookkeepingVoucherApi.BookkeepingVoucherApi.getVoucher(id)
formData.value = data
if (!formData.value.items) {
formData.value.items = []
}
calculateTotal()
} finally {
formLoading.value = false
}
} else {
// 新增时,设置默认制单日期为当天
formData.value.voucherDate = formatDate(new Date(), 'YYYY-MM-DD')
// 添加一行空明细
addItem()
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 校验明细
if (formData.value.items.length === 0) {
message.warning('请至少添加一条凭证明细')
return
}
// 校验每条明细
for (let i = 0; i < formData.value.items.length; i++) {
const item = formData.value.items[i]
if (!item.subjectId) {
message.warning(`${i + 1}行明细请选择科目名称`)
return
}
if (!item.debitAmount && !item.creditAmount) {
message.warning(`${i + 1}行明细请填写借方或贷方金额`)
return
}
}
// 校验借贷平衡
if (!isBalanced.value) {
message.warning('借贷不平衡,请检查金额')
return
}
// 提交请求
formLoading.value = true
try {
const data = { ...formData.value }
if (formType.value === 'create') {
await BookkeepingVoucherApi.BookkeepingVoucherApi.createVoucher(data)
message.success(t('common.createSuccess'))
} else {
await BookkeepingVoucherApi.BookkeepingVoucherApi.updateVoucher(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
voucherNo: '',
voucherDate: '',
summary: '',
systemName: '',
remark: '',
status: undefined,
items: []
}
debitTotal.value = 0
creditTotal.value = 0
formRef.value?.resetFields()
}
const emit = defineEmits(['success']) // 定义 success 事件
</script>