diff --git a/package.json b/package.json index 621d8de..c496ed7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "element-china-area-data": "^6.1.0", "html2canvas": "^1.4.1", "mammoth": "^1.9.0", "pdf-lib": "^1.17.1", diff --git a/pc/package.json b/pc/package.json index 8c79d62..5ae5ba5 100644 --- a/pc/package.json +++ b/pc/package.json @@ -50,7 +50,10 @@ "vue-router": "3.4.9", "vuedraggable": "^2.24.3", "vuex": "3.6.0", - "xlsx": "^0.18.5" + "xlsx": "^0.18.5", + "element-china-area-data": "^6.1.0", + "mammoth": "^1.9.0", + "qrcode.vue": "^3.6.0" }, "devDependencies": { "@vue/cli-plugin-babel": "4.4.6", diff --git a/pc/src/api/finance.js b/pc/src/api/finance.js index 11a2f25..cd2ab71 100644 --- a/pc/src/api/finance.js +++ b/pc/src/api/finance.js @@ -416,7 +416,7 @@ export function uploadBillAttachment(billId, file) { formData.append('billId', billId) formData.append('file', file) return request({ - url: '/bill/attachment/upload', + url: '/bill/upload-attachment', method: 'post', data: formData, headers: { @@ -425,10 +425,11 @@ export function uploadBillAttachment(billId, file) { }) } +// 账单附件删除 export function deleteBillAttachment(attachmentId) { return request({ - url: `/bill/attachment/${attachmentId}`, - method: 'delete' + url: `/bill/attachment/delete/${attachmentId}`, + method: 'get' }) } @@ -446,4 +447,267 @@ export function exportBillList(data) { data, responseType: 'blob' }) +} + +// 收支流水相关接口 +export function getTransactionList(params) { + return request({ + url: '/finance/transaction/page', + method: 'get', + params + }) +} + +export function getTransactionDetail(id) { + return request({ + url: `/finance/transaction/${id}`, + method: 'get' + }) +} + +export function addTransaction(data) { + return request({ + url: '/finance/transaction', + method: 'post', + data + }) +} + +export function updateTransaction(data) { + return request({ + url: '/finance/transaction', + method: 'put', + data + }) +} + +export function exportTransactionList(params) { + return request({ + url: '/finance/transaction/export', + method: 'get', + params, + responseType: 'blob' + }) +} + +export function getTransactionAttachments(transactionId) { + return request({ + url: `/finance/transaction/${transactionId}`, + method: 'get' + }) +} + +export function uploadTransactionAttachment(transactionId, file) { + const formData = new FormData() + formData.append('transactionId', transactionId) + formData.append('userId', localStorage.getItem('userId') || '') + formData.append('userName', localStorage.getItem('userName') || '') + formData.append('file', file) + + return request({ + url: '/finance/transaction/attachment', + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +export function deleteTransactionAttachment(attachmentId) { + return request({ + url: `/finance/transaction/attachment/file/${attachmentId}`, + method: 'delete' + }) +} + +export function previewAttachment(attachmentId) { + return request({ + url: `/finance/transaction/attachment/preview/file/${attachmentId}`, + method: 'get', + responseType: 'blob' + }) +} + +export function previewAttachmentByFileId(fileId) { + return request({ + url: `/finance/transaction/attachment/preview/file/${fileId}`, + method: 'get', + responseType: 'blob' + }) +} + +export function downloadAttachment(fileId, fileName) { + return request({ + url: `/finance/transaction/attachment/download/${fileId}`, + method: 'get', + params: { fileName }, + responseType: 'blob' + }) +} + +export function deleteAttachmentByFileId(fileId) { + return request({ + url: `/finance/transaction/attachment/file/${fileId}`, + method: 'delete', + params: { + userId: localStorage.getItem('userId') || '' + } + }) +} + +export function addTransactionOperation(data) { + return request({ + url: '/finance/transaction/operation', + method: 'post', + data + }) +} + +export function getTransactionOperationLogs(transactionId, query) { + return request({ + url: `/finance/transaction/${transactionId}`, + method: 'get' + }) +} + +// 获取所有收支账户 +export function getAllFinanceAccounts() { + return request({ + url: '/finance/account/list', + method: 'get' + }) +} + +// 账单调整相关接口 + +// 添加账单调整(按金额或按比例) +export function addBillAdjustment(data) { + return request({ + url: '/v1/bill-adjustments', + method: 'post', + data + }) +} + +// 作废账单调整 +export function voidBillAdjustment(data) { + return request({ + url: '/v1/bill-adjustments/void', + method: 'put', + data + }) +} + +// 分页查询账单调整记录 +export function getBillAdjustmentList(params) { + return request({ + url: '/v1/bill-adjustments', + method: 'get', + params + }) +} + +// 获取账单调整详情 +export function getBillAdjustmentDetail(id) { + return request({ + url: `/v1/bill-adjustments/${id}`, + method: 'get' + }) +} + +// 上传账单调整附件 +export function uploadBillAdjustmentAttachment(id, file) { + const formData = new FormData() + formData.append('file', file) + return request({ + url: `/v1/bill-adjustments/${id}/attachments`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除账单调整附件 +export function deleteBillAdjustmentAttachment(attachmentId) { + return request({ + url: `/v1/bill-adjustments/attachments/${attachmentId}`, + method: 'delete' + }) +} + +// 获取账单明细 +export function getBillDetailById(detailId) { + return request({ + url: `/v1/bill-details/${detailId}`, + method: 'get' + }) +} + +// 获取账单调整附件列表 +export function getBillAdjustmentAttachmentList(adjustmentId) { + return request({ + url: `/v1/bill-adjustments/attachments/view/${adjustmentId}`, + method: 'get' + }) +} + +// 获取账单操作记录 +export function getBillOperationRecords(billId, params) { + return request({ + url: `/v1/operation-records/list/${billId}`, + method: 'get', + params + }) +} + +// 账单支付相关接口 +// 提交账单支付 +export function submitBillPayment(data) { + return request({ + url: '/v1/bill-payments', + method: 'post', + data + }) +} + +// 分页查询支付记录 +export function getBillPaymentRecords(params) { + return request({ + url: '/v1/bill-payments', + method: 'get', + params + }) +} + +// 获取支付详情 +export function getBillPaymentDetail(id) { + return request({ + url: `/v1/bill-payments/${id}`, + method: 'get' + }) +} + +// 上传支付附件 +export function uploadBillPaymentAttachment(id, file) { + const formData = new FormData() + formData.append('file', file) + return request({ + url: `/v1/bill-payments/${id}/attachments`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除支付附件 +export function deleteBillPaymentAttachment(attachmentId) { + return request({ + url: `/v1/bill-payments/attachments/${attachmentId}`, + method: 'delete' + }) } \ No newline at end of file diff --git a/pc/src/components/RegionSelector.vue b/pc/src/components/RegionSelector.vue new file mode 100644 index 0000000..2713270 --- /dev/null +++ b/pc/src/components/RegionSelector.vue @@ -0,0 +1,136 @@ +<template> + <el-cascader + v-model="selectedRegion" + :options="regionOptions" + :props="{ expandTrigger: 'hover', value: 'value', label: 'label' }" + :placeholder="placeholder" + filterable + clearable + @change="handleChange" + style="width: 100%" + /> +</template> + +<script> +import { regionData, pcaTextArr } from 'element-china-area-data' + +export default { + name: 'RegionSelector', + props: { + value: { + type: [Array, String], + default: () => [] + }, + placeholder: { + type: String, + default: '请选择省/市/区' + }, + // 是否使用纯文本数据 + useTextData: { + type: Boolean, + default: true + } + }, + data() { + return { + regionOptions: this.useTextData ? pcaTextArr : regionData, + selectedRegion: [] + } + }, + watch: { + value: { + handler(val) { + if (val) { + // 处理传入的值,适配组件 + if (typeof val === 'string') { + // 如果是字符串,按空格分割为数组 + const parts = val.split(' ').filter(item => item); + + if (parts.length > 0) { + if (this.useTextData) { + // 如果使用纯文本数据,直接设置 + this.selectedRegion = parts; + } else { + // 如果使用编码数据,需要转换成编码 + this.convertTextToCode(parts); + } + } else { + this.selectedRegion = []; + } + } else if (Array.isArray(val)) { + // 已经是数组,直接使用 + this.selectedRegion = val; + } else { + this.selectedRegion = []; + } + } else { + this.selectedRegion = []; + } + }, + immediate: true + } + }, + methods: { + // 将文本转换为区域代码 + convertTextToCode(textArray) { + // 由于没有TextToCode对象,我们需要手动查找 + const findValueByLabel = (options, label, level = 0) => { + for (const option of options) { + if (option.label === label) { + return level === textArray.length - 1 ? option.value : + findValueByLabel(option.children || [], textArray[level + 1], level + 1); + } + } + return null; + }; + + const value = findValueByLabel(this.regionOptions, textArray[0], 0); + if (value) { + this.selectedRegion = value.split(','); + } else { + this.selectedRegion = []; + } + }, + + handleChange(value) { + + // 选择值改变时,转换为可读字符串并发送事件 + if (value && value.length > 0) { + const labels = this.getSelectedLabels(value); + const regionText = labels.join(' '); + this.$emit('input', regionText); + this.$emit('change', regionText, value, labels); + + } else { + this.$emit('input', ''); + this.$emit('change', '', [], []); + } + + }, + + getSelectedLabels(codes) { + // 根据选中的代码值获取对应的文本标签 + const labels = []; + let options = this.regionOptions; + + for (let i = 0; i < codes.length; i++) { + const code = codes[i]; + const option = options.find(item => item.value === code); + + if (option) { + labels.push(option.label); + options = option.children || []; + } + } + + return labels; + } + } +} +</script> + +<style scoped> +.el-cascader { + width: 100%; +} +</style> \ No newline at end of file diff --git a/pc/src/router/modules/finance.js b/pc/src/router/modules/finance.js index c20f28b..af16121 100644 --- a/pc/src/router/modules/finance.js +++ b/pc/src/router/modules/finance.js @@ -30,6 +30,12 @@ export default { component: () => import('@/views/finance/billList/index.vue'), name: 'BillList', meta: { title: '所有账单', icon: 'el-icon-notebook-2' } + }, + { + path: 'transaction', + component: () => import('@/views/finance/transaction/index.vue'), + name: 'Transaction', + meta: { title: '收支流水', icon: 'el-icon-money' } } ] } \ No newline at end of file diff --git a/pc/src/views/finance/billList/components/AddBill.vue b/pc/src/views/finance/billList/components/AddBill.vue index 8371e23..6ed1402 100644 --- a/pc/src/views/finance/billList/components/AddBill.vue +++ b/pc/src/views/finance/billList/components/AddBill.vue @@ -5,8 +5,8 @@ <el-form ref="chargeModeForm" :model="billForm" :rules="chargeModeRules" label-width="120px"> <el-form-item label="含税规则" prop="taxInclusiveRule" required> <el-radio-group v-model="billForm.taxInclusiveRule"> - <el-radio label="含税">含税</el-radio> - <el-radio label="不含税">不含税</el-radio> + <el-radio label="1" value="1">含税</el-radio> + <el-radio label="2" value="2">不含税</el-radio> </el-radio-group> </el-form-item> <el-form-item label="税率(%)" prop="taxRate" required> @@ -14,18 +14,18 @@ </el-form-item> <el-form-item label="特殊账单类型" prop="specialBillType" required> <el-radio-group v-model="billForm.specialBillType"> - <el-radio label="正常">正常</el-radio> - <el-radio label="罚金">罚金</el-radio> + <el-radio label="1" value="1">正常</el-radio> + <el-radio label="2" value="2">罚金</el-radio> </el-radio-group> </el-form-item> - <el-form-item label="滞纳金起算天数" prop="lateFeeStartDays"> - <el-input-number v-model="billForm.lateFeeStartDays" :min="0" :precision="0" style="width: 200px"></el-input-number> + <el-form-item label="滞纳金起算天数" prop="ovdueStartDays"> + <el-input-number v-model="billForm.ovdueStartDays" :min="0" :precision="0" style="width: 200px"></el-input-number> </el-form-item> - <el-form-item label="滞纳金比例(%/天)" prop="lateFeeRate"> - <el-input-number v-model="billForm.lateFeeRate" :min="0" :max="100" :precision="10" style="width: 200px"></el-input-number> + <el-form-item label="滞纳金比例(%/天)" prop="ovdueIntRate"> + <el-input-number v-model="billForm.ovdueIntRate" :min="0" :max="100" :precision="10" style="width: 200px"></el-input-number> </el-form-item> - <el-form-item label="滞纳金上限(%)" prop="lateFeeLimit"> - <el-input-number v-model="billForm.lateFeeLimit" :min="0" :max="100" :precision="10" style="width: 200px"></el-input-number> + <el-form-item label="滞纳金上限(%)" prop="ovdueLimitRate"> + <el-input-number v-model="billForm.ovdueLimitRate" :min="0" :max="100" :precision="10" style="width: 200px"></el-input-number> </el-form-item> </el-form> </el-tab-pane> @@ -64,8 +64,8 @@ </el-option> </el-select> </el-form-item> - <el-form-item label="付款方" prop="payeeId" required> - <el-select v-model="billForm.payeeId" placeholder="请选择付款方" filterable remote clearable + <el-form-item label="付款方" prop="paysdId" required> + <el-select v-model="billForm.paysdId" placeholder="请选择付款方" filterable remote clearable :remote-method="searchPayees" @change="handlePayeeChange" style="width: 400px"> <el-option v-for="item in payeeOptions" :key="item.id" :label="item.name" :value="item.id"> @@ -75,12 +75,12 @@ <el-form-item label="币种" prop="currCode" required> <el-input v-model="billForm.currCode" disabled style="width: 400px"></el-input> </el-form-item> - <el-form-item label="应收金额" prop="receivableAmount" required> - <el-input-number v-model="billForm.receivableAmount" :min="0" :precision="2" style="width: 400px"></el-input-number> + <el-form-item label="应收金额" prop="accigRcvAmt" required> + <el-input-number v-model="billForm.accigRcvAmt" :min="0" :precision="2" style="width: 400px"></el-input-number> </el-form-item> - <el-form-item label="应收日期" prop="receivableDate" required> + <el-form-item label="应收日期" prop="pybDt" required> <el-date-picker - v-model="billForm.receivableDate" + v-model="billForm.pybDt" type="date" placeholder="选择日期" value-format="yyyy-MM-dd" @@ -139,18 +139,39 @@ <el-table-column label="操作" width="200" align="center"> <template slot-scope="scope"> <el-button size="mini" type="text" @click="previewFile(scope.row)">预览</el-button> - <el-button size="mini" type="text" @click="downloadFile(scope.row)">下载</el-button> <el-button size="mini" type="text" class="delete-btn" @click="deleteFile(scope.$index)">删除</el-button> </template> </el-table-column> </el-table> </el-tab-pane> </el-tabs> + + <!-- 文件预览对话框 --> + <el-dialog title="附件预览" :visible.sync="previewDialogVisible" width="800px" append-to-body> + <div class="preview-container" v-loading="previewLoading"> + <div v-if="previewUrl && isPdf" class="preview-iframe"> + <iframe :src="previewUrl" width="100%" height="500px" frameborder="0"></iframe> + </div> + <div v-else-if="previewUrl && isImage" class="preview-image"> + <img :src="previewUrl" style="max-width: 100%;" /> + </div> + <div v-else-if="!previewLoading" class="preview-error"> + <i class="el-icon-warning"></i> + <p>预览加载失败,请尝试下载后查看</p> + <div class="preview-actions"> + <el-button type="primary" @click="downloadCurrentPreview">下载文件</el-button> + </div> + </div> + </div> + <div slot="footer" class="dialog-footer"> + <el-button @click="previewDialogVisible = false">关 闭</el-button> + </div> + </el-dialog> </div> </template> <script> -import { getFeeTypeTree } from '@/api/finance' +import { getFeeTypeTree, uploadBillAttachment, addBill } from '@/api/finance' export default { name: 'AddBill', @@ -167,30 +188,28 @@ export default { billForm: { contractId: '', contractNumber: '', - payeeId: '', - payeeName: '', + paysdId: '', + paysdName: '', payeeContact: '', feeCategoryId: '', feeTypeId: '', - feeTypeName: '', + feTpName: '', currCode: '156', // 默认人民币 - billingStartDate: '', - billingEndDate: '', - receivableAmount: 0, - price: 0, - priceU: '元/月', - taxInclusiveRule: '含税', + chggBgnDt: '', + chggEndDt: '', + accigRcvAmt: 0, + taxInclusiveRule: '1', taxRate: 0, - receivableDate: '', - specialBillType: '正常', - lateFeeStartDays: 0, - lateFeeRate: 0, - lateFeeLimit: 0, + pybDt: '', + specialBillType: '1', + ovdueStartDays: 0, + ovdueIntRate: 0, + ovdueLimitRate: 0, companyId: '', - companyName: '', + corNm: '', accountId: '', billRemark: '', - roomInfoList: [] + roomIds: [] }, // 表单验证规则 chargeModeRules: { @@ -205,7 +224,7 @@ export default { ] }, billInfoRules: { - payeeId: [ + paysdId: [ { required: true, message: '请选择付款方', trigger: 'change' } ], companyId: [ @@ -214,10 +233,10 @@ export default { accountId: [ { required: true, message: '请选择收支账户', trigger: 'change' } ], - receivableAmount: [ + accigRcvAmt: [ { required: true, message: '请输入应收金额', trigger: 'blur' } ], - receivableDate: [ + pybDt: [ { required: true, message: '请选择应收日期', trigger: 'change' } ] }, @@ -244,7 +263,16 @@ export default { selectedRoomIds: [], // 附件上传相关 fileList: [], - attachmentLoading: false + attachmentLoading: false, + + // 预览相关 + previewDialogVisible: false, + previewLoading: false, + previewUrl: '', + previewBlob: null, + currentPreviewName: '', + isPdf: false, + isImage: false, } }, created() { @@ -280,10 +308,10 @@ export default { children = category.financeFeeTypes.map(feeType => { return { id: feeType.id || Math.random().toString(36).substr(2, 9), - label: feeType.feeTypeName || '未命名费用', + label: feeType.feTpName || '未命名费用', categoryId: categoryId, // 存储原始数据以便后续使用 - feeTypeName: feeType.feeTypeName || '未命名费用', + feTpName: feeType.feTpName || '未命名费用', // 确保没有更多的子节点 children: null } @@ -302,7 +330,7 @@ export default { getCompanyOptions() { // 模拟数据,实际应调用API this.companyOptions = [ - { id: 'C001', name: '智慧园区物业管理有限公司' } + { id: 123, name: '智慧园区物业管理有限公司' } ] }, // 获取收支账户选项 @@ -318,8 +346,8 @@ export default { if (query) { // 实际应调用API this.contractOptions = [ - { contractId: 'CT001', contractNumber: 'HT2023001', customerName: '张三' }, - { contractId: 'CT002', contractNumber: 'HT2023002', customerName: '李四' } + { contractId: 11, contractNumber: 'HT2023001', customerName: '张三' }, + { contractId: 22, contractNumber: 'HT2023002', customerName: '李四' } ] } else { this.contractOptions = [] @@ -338,19 +366,21 @@ export default { } }, // 处理付款方变更 - handlePayeeChange(payeeId) { - const payee = this.payeeOptions.find(item => item.id === payeeId) + handlePayeeChange(paysdId) { + const payee = this.payeeOptions.find(item => item.id === paysdId) if (payee) { - this.billForm.payeeName = payee.name + this.billForm.paysdName = payee.name this.billForm.payeeContact = payee.contact // 触发表单验证更新 this.$nextTick(() => { - this.$refs.billInfoForm.validateField('payeeId') + this.$refs.billInfoForm.validateField('paysdId') }) } }, // 处理费用类型变更 handleFeeTypeChange(value) { + console.log(value); + if (value) { // 查找匹配的费用类型 let selectedFeeType = null; @@ -370,7 +400,7 @@ export default { } // 检查子节点 if (category.children && category.children.length > 0) { - const found = category.children.find(item => item.id === value); + const found = category.children.find(item => item.id === value[1]); if (found) { selectedCategory = category; selectedFeeType = { @@ -389,25 +419,25 @@ export default { if (selectedFeeType.type === '分类') { this.billForm.feeCategoryId = selectedFeeType.id; this.billForm.feeTypeId = ''; - this.billForm.feeTypeName = selectedFeeType.name; + this.billForm.feTpName = selectedFeeType.name; } else { this.billForm.feeCategoryId = selectedCategory.id; this.billForm.feeTypeId = selectedFeeType.id; - this.billForm.feeTypeName = selectedFeeType.name; + this.billForm.feTpName = selectedFeeType.name; } } } else { // 清空选择 this.billForm.feeCategoryId = ''; this.billForm.feeTypeId = ''; - this.billForm.feeTypeName = ''; + this.billForm.feTpName = ''; } }, // 处理公司变更 handleCompanyChange(companyId) { const company = this.companyOptions.find(item => item.id === companyId) if (company) { - this.billForm.companyName = company.name + this.billForm.corNm = company.name // 触发表单验证更新 this.$nextTick(() => { this.$refs.billInfoForm.validateField('companyId') @@ -420,11 +450,11 @@ export default { // 处理计费周期变更 handleBillingPeriodChange(val) { if (val && val.length === 2) { - this.billForm.billingStartDate = val[0] - this.billForm.billingEndDate = val[1] + this.billForm.chggBgnDt = val[0] + this.billForm.chggEndDt = val[1] } else { - this.billForm.billingStartDate = '' - this.billForm.billingEndDate = '' + this.billForm.chggBgnDt = '' + this.billForm.chggEndDt = '' } }, @@ -525,9 +555,6 @@ export default { // 更新选中的房间ID列表 this.selectedRoomIds = roomNodes.map(node => node.id) - // 重置房源信息列表 - this.billForm.roomInfoList = [] - // 创建一个Map用于收集项目信息 const projectMap = new Map() @@ -553,20 +580,63 @@ export default { // 只有完整路径的房间才处理 if (roomInfo && floorNode && buildingNode && projectNode) { - // 更新房源信息列表 - this.billForm.roomInfoList.push({ - roomId: roomNode.id, + // 确认项目是否已在Map中 + if (!projectMap.has(projectNode.id)) { + projectMap.set(projectNode.id, { + id: projectNode.id, + projectName: projectNode.label, + projectType: '产业园区', // 如果有projectType属性,应该使用projectNode中的值 + buildings: [] + }) + } + + // 获取当前项目 + const project = projectMap.get(projectNode.id) + + // 查找楼宇 + let building = project.buildings.find(b => b.id === buildingNode.id) + if (!building) { + building = { + id: buildingNode.id, + buildingName: buildingNode.label, + buildingCode: '', // 如果有buildingCode属性,应该使用buildingNode中的值 + floors: [] + } + project.buildings.push(building) + } + + // 查找楼层 + let floor = building.floors.find(f => f.id === floorNode.id) + if (!floor) { + floor = { + id: floorNode.id, + floorName: floorNode.label, + floorNumber: 0, // 如果有floorNumber属性,应该使用floorNode中的值 + rooms: [] + } + building.floors.push(floor) + } + + // 添加房间 + const room = { + id: roomInfo.id, roomNumber: roomInfo.roomNumber || roomNode.label.split('(')[0], - buildingId: buildingNode.id, - buildingName: buildingNode.label, - floorId: floorNode.id, - floorName: floorNode.label, - projectId: projectNode.id, - projectName: projectNode.label, - rentArea: roomInfo.rentalArea || 0 - }) + roomType: roomInfo.roomType || '', + roomStatus: roomInfo.roomStatus || '1', + buildingArea: roomInfo.buildingArea || 0, + rentalArea: roomInfo.rentalArea || 0 + } + + // 检查该房间是否已经存在 + const roomExists = floor.rooms.some(r => r.id === room.id) + if (!roomExists) { + floor.rooms.push(room) + } } }) + + // 将Map转换为数组 + this.billForm.roomIds = Array.from(projectMap.values()) }, // 上传文件前检查 beforeUpload(file) { @@ -586,32 +656,87 @@ export default { // 自定义上传文件 uploadFile(options) { this.attachmentLoading = true + const file = options.file - // 此处模拟上传,实际应调用API - setTimeout(() => { - this.fileList.push({ - name: options.file.name, - url: URL.createObjectURL(options.file), - operatorName: '当前用户', - operateTime: new Date().toLocaleString() - }) - - this.attachmentLoading = false - this.$message.success('上传成功') - }, 1000) + // 创建临时URL供预览 + const tempUrl = URL.createObjectURL(file) + const fileName = file.name + + // 向上传列表中添加文件 + const currentUser = this.$store.getters.name || '当前用户' + + this.fileList.push({ + name: fileName, + file: file, + url: tempUrl, + operatorName: currentUser, + operateTime: new Date().toLocaleString() + }) + + this.attachmentLoading = false + this.$message.success('上传成功') + + console.log(this.fileList); + }, + // 预览文件 previewFile(file) { - window.open(file.url) + this.previewLoading = true + this.previewDialogVisible = true + this.currentPreviewName = file.name || '附件' + this.previewUrl = '' + this.isPdf = false + this.isImage = false + + // 根据文件名后缀判断文件类型 + const fileExt = file.name ? file.name.split('.').pop().toLowerCase() : '' + + try { + // 获取文件内容进行预览 + const blob = file.file ? file.file : new Blob() + this.previewBlob = blob + + // 设置文件类型标志 + if (fileExt === 'pdf') { + this.isPdf = true + } else if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt)) { + this.isImage = true + } + + // 创建URL用于预览 + this.previewUrl = file.url || URL.createObjectURL(blob) + this.previewLoading = false + } catch (error) { + console.error('预览失败', error) + this.$message.error('获取预览数据失败') + this.previewLoading = false + } }, + + // 下载当前预览的文件 + downloadCurrentPreview() { + if (!this.previewUrl) { + this.$message.error('没有可下载的文件') + return + } + + const link = document.createElement('a') + link.href = this.previewUrl + link.setAttribute('download', this.currentPreviewName) + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + }, + // 下载文件 downloadFile(file) { - // 实际应调用API下载 const link = document.createElement('a') link.href = file.url link.download = file.name link.click() }, + // 删除文件 deleteFile(index) { this.$confirm('确认删除该附件?', '提示', { @@ -631,8 +756,8 @@ export default { } if (this.formExtension.billingPeriod && this.formExtension.billingPeriod.length === 2) { - this.billForm.billingStartDate = this.formExtension.billingPeriod[0] - this.billForm.billingEndDate = this.formExtension.billingPeriod[1] + this.billForm.chggBgnDt = this.formExtension.billingPeriod[0] + this.billForm.chggEndDt = this.formExtension.billingPeriod[1] } return this.billForm @@ -665,7 +790,7 @@ export default { } // 验证是否选择了房源 - if (this.billForm.roomInfoList.length === 0) { + if (this.billForm.roomIds.length === 0) { this.$message.warning('请至少选择一个房源') this.activeTab = 'roomInfo' resolve(false) @@ -698,30 +823,28 @@ export default { this.billForm = { contractId: '', contractNumber: '', - payeeId: '', - payeeName: '', + paysdId: '', + paysdName: '', payeeContact: '', feeCategoryId: '', feeTypeId: '', - feeTypeName: '', + feTpName: '', currCode: '156', // 默认人民币 - billingStartDate: '', - billingEndDate: '', - receivableAmount: 0, - price: 0, - priceU: '元/月', - taxInclusiveRule: '含税', + chggBgnDt: '', + chggEndDt: '', + accigRcvAmt: 0, + taxInclusiveRule: '1', taxRate: 0, - receivableDate: '', - specialBillType: '正常', - lateFeeStartDays: 0, - lateFeeRate: 0, - lateFeeLimit: 0, + pybDt: '', + specialBillType: '1', + ovdueStartDays: 0, + ovdueIntRate: 0, + ovdueLimitRate: 0, companyId: '', - companyName: '', + corNm: '', accountId: '', billRemark: '', - roomInfoList: [] + roomIds: [] } // 重置扩展表单数据 @@ -738,7 +861,62 @@ export default { // 重置文件上传列表 this.fileList = [] - } + }, + // 表单提交 + submitForm() { + this.validateForm().then(valid => { + if (valid) { + const formData = this.getFormData() + + this.loading = true + addBill(formData) + .then(response => { + if (response.code === '0000000000000000') { + this.$message.success('添加账单成功') + + // 如果有附件,上传附件 + if (this.fileList.length > 0) { + + this.uploadAttachments(response.data) + } else { + this.$emit('success') + this.resetForm() + } + } else { + this.$message.error(response.message || '添加账单失败') + this.loading = false + } + }) + .catch(error => { + console.error('添加账单失败', error) + this.$message.error('添加账单失败,请检查表单数据') + this.loading = false + }) + } + }) + }, + + // 上传附件 + uploadAttachments(billId) { + const uploadPromises = this.fileList.map(fileItem => { + return uploadBillAttachment(billId, fileItem.file) + }) + + Promise.all(uploadPromises) + .then(() => { + this.$message.success('附件上传成功') + this.$emit('success') + this.resetForm() + this.loading = false + }) + .catch(error => { + console.error('附件上传失败', error) + this.$message.warning('账单创建成功,但部分附件上传失败') + this.$emit('success') + this.resetForm() + this.loading = false + }) + }, } } </script> @@ -755,4 +933,45 @@ export default { color: #F56C6C; } } + +.preview-container { + min-height: 500px; + max-height: 700px; + overflow: auto; + + .preview-iframe { + width: 100%; + height: 500px; + border: none; + } + + .preview-image { + display: flex; + justify-content: center; + align-items: center; + min-height: 300px; + } + + .preview-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 300px; + color: #909399; + + i { + font-size: 48px; + margin-bottom: 20px; + } + + p { + font-size: 16px; + } + + .preview-actions { + margin-top: 20px; + } + } +} </style> \ No newline at end of file diff --git a/pc/src/views/finance/billList/components/BillDetail.vue b/pc/src/views/finance/billList/components/BillDetail.vue index cc56d22..06b5ee4 100644 --- a/pc/src/views/finance/billList/components/BillDetail.vue +++ b/pc/src/views/finance/billList/components/BillDetail.vue @@ -2,22 +2,35 @@ <div class="bill-detail-container"> <el-tabs v-model="activeTab"> <el-tab-pane label="账单基本信息" name="basicInfo"> + <div class="bill-header"> + <div class="bill-header-left"> + <h3>账单信息</h3> + </div> + <div class="bill-header-right"> + <el-button + type="primary" + @click="openPaymentDialog" + :disabled="!canPayBill"> + 账单支付 + </el-button> + </div> + </div> <el-descriptions :column="3" border size="medium"> - <el-descriptions-item label="账单编号">{{ billDetail.billNumber }}</el-descriptions-item> + <el-descriptions-item label="账单编号">{{ billDetail.totBillNo }}</el-descriptions-item> <el-descriptions-item label="账单来源">{{ getBillSourceName(billDetail.billSource) }}</el-descriptions-item> <el-descriptions-item label="账单状态"> <el-tag :type="billDetail.billStatus === '1' ? 'success' : 'info'"> {{ billDetail.billStatus === '1' ? '开启' : '关闭' }} </el-tag> </el-descriptions-item> - <el-descriptions-item label="对方名称">{{ billDetail.payeeName }}</el-descriptions-item> + <el-descriptions-item label="对方名称">{{ billDetail.paysdName }}</el-descriptions-item> <el-descriptions-item label="支付联系方式">{{ billDetail.payeeContact }}</el-descriptions-item> <el-descriptions-item label="关联合同">{{ billDetail.contractNumber || '无' }}</el-descriptions-item> - <el-descriptions-item label="费用类型">{{ billDetail.feeTypeName }}</el-descriptions-item> + <el-descriptions-item label="费用类型">{{ billDetail.feTpName }}</el-descriptions-item> <el-descriptions-item label="计费周期"> - {{ billDetail.billingStartDate }} 至 {{ billDetail.billingEndDate }} + {{ billDetail.chggBgnDt }} 至 {{ billDetail.chggEndDt }} </el-descriptions-item> - <el-descriptions-item label="应收日期">{{ billDetail.receivableDate }}</el-descriptions-item> + <el-descriptions-item label="应收日期">{{ billDetail.pybDt }}</el-descriptions-item> <el-descriptions-item label="结清状态"> <el-tag :type="getClearStatusType(billDetail.clearStatus)"> {{ getClearStatusName(billDetail.clearStatus) }} @@ -29,18 +42,18 @@ </el-tag> </el-descriptions-item> <el-descriptions-item label="滞纳金状态"> - <el-tag :type="getLateFeeStatusType(billDetail.lateFeeStatus)"> - {{ getLateFeeStatusName(billDetail.lateFeeStatus) }} + <el-tag :type="getLateFeeStatusType(billDetail.ovdueStatus)"> + {{ getLateFeeStatusName(billDetail.ovdueStatus) }} </el-tag> </el-descriptions-item> - <el-descriptions-item label="账单金额">{{ formatAmount(billDetail.billAmount) }}</el-descriptions-item> - <el-descriptions-item label="应收金额">{{ formatAmount(billDetail.receivableAmount) }}</el-descriptions-item> - <el-descriptions-item label="实收金额">{{ formatAmount(billDetail.receivedAmount) }}</el-descriptions-item> + <el-descriptions-item label="账单金额">{{ formatAmount(billDetail.accblAmt) }}</el-descriptions-item> + <el-descriptions-item label="应收金额">{{ formatAmount(billDetail.accigRcvAmt) }}</el-descriptions-item> + <el-descriptions-item label="实收金额">{{ formatAmount(billDetail.atmRecvAmt) }}</el-descriptions-item> <el-descriptions-item label="需收金额">{{ formatAmount(billDetail.needAmount) }}</el-descriptions-item> - <el-descriptions-item label="调整金额">{{ formatAmount(billDetail.adjustAmount) }}</el-descriptions-item> - <el-descriptions-item label="应收滞纳金">{{ formatAmount(billDetail.receivableLateFee) }}</el-descriptions-item> + <el-descriptions-item label="调整金额">{{ formatAmount(billDetail.adjAmt) }}</el-descriptions-item> + <el-descriptions-item label="应收滞纳金">{{ formatAmount(billDetail.receivableOvdueAmt) }}</el-descriptions-item> <el-descriptions-item label="税率">{{ billDetail.taxRate }}%</el-descriptions-item> - <el-descriptions-item label="税额">{{ formatAmount(billDetail.taxAmount) }}</el-descriptions-item> + <el-descriptions-item label="税额">{{ formatAmount(billDetail.paybleTaxAmount) }}</el-descriptions-item> <el-descriptions-item label="含税规则">{{ billDetail.taxInclusiveRule }}</el-descriptions-item> <el-descriptions-item label="特殊账单类型">{{ billDetail.specialBillType }}</el-descriptions-item> <el-descriptions-item label="开据状态"> @@ -48,13 +61,121 @@ {{ billDetail.receiptStatus === '1' ? '已开据' : '未开据' }} </el-tag> </el-descriptions-item> - <el-descriptions-item label="所属公司">{{ billDetail.companyName }}</el-descriptions-item> + <el-descriptions-item label="所属公司">{{ billDetail.corNm }}</el-descriptions-item> <el-descriptions-item label="租赁数(计租面积)">{{ billDetail.rentArea }} ㎡</el-descriptions-item> <el-descriptions-item label="项目名称">{{ billDetail.projectName }}</el-descriptions-item> <el-descriptions-item label="账单备注" :span="3">{{ billDetail.billRemark || '无' }}</el-descriptions-item> </el-descriptions> </el-tab-pane> + <el-tab-pane label="账单明细查询" name="billDetails"> + <el-table :data="billDetailsList" border style="width: 100%"> + <el-table-column prop="feeTypeName" label="费用类型" min-width="120" show-overflow-tooltip></el-table-column> + <el-table-column label="应收金额" min-width="120" align="right"> + <template slot-scope="scope"> + {{ formatAmount(scope.row.receivableAmount) }} + </template> + </el-table-column> + <el-table-column label="税率" min-width="80" align="center"> + <template slot-scope="scope"> + {{ scope.row.taxRate }}% + </template> + </el-table-column> + <el-table-column label="税额" min-width="120" align="right"> + <template slot-scope="scope"> + {{ formatAmount(scope.row.taxAmount) }} + </template> + </el-table-column> + <el-table-column prop="startDate" label="开始日期" min-width="120" align="center"></el-table-column> + <el-table-column prop="endDate" label="结束日期" min-width="120" align="center"></el-table-column> + <el-table-column prop="remark" label="账单备注" min-width="200" show-overflow-tooltip></el-table-column> + <el-table-column label="操作" width="120" align="center" fixed="right"> + <template slot-scope="scope"> + <el-button + size="mini" + type="text" + @click="handleAdjustment(scope.row)" + :disabled="!canAdjustBill"> + 调整 + </el-button> + </template> + </el-table-column> + </el-table> + <div v-if="billDetailsList.length === 0" class="empty-data"> + <el-empty description="暂无账单明细"></el-empty> + </div> + </el-tab-pane> + + <el-tab-pane label="账单调整" name="billAdjustment"> + <div class="bill-adjustment-container"> + <div class="adjustment-list"> + <el-table :data="adjustmentList" border style="width: 100%; margin-top: 15px"> + <el-table-column prop="adjTpCd" label="调整类型" min-width="100"> + <template slot-scope="scope"> + {{ scope.row.adjTpCd === '1' ? '调增' : '调减' }} + </template> + </el-table-column> + <el-table-column prop="adjDate" label="调整日期" min-width="120" align="center"></el-table-column> + <el-table-column prop="adjMethod" label="调整方式" min-width="120"> + <template slot-scope="scope"> + {{ scope.row.adjMethod === '1' ? '按金额调整' : '按比例调整' }} + </template> + </el-table-column> + <el-table-column label="调整金额" min-width="120" align="right"> + <template slot-scope="scope"> + {{ formatAmount(scope.row.adjAmt) }} + </template> + </el-table-column> + <el-table-column label="调整比例" min-width="100" align="center"> + <template slot-scope="scope"> + {{ scope.row.adjRate ? scope.row.adjRate + '%' : '-' }} + </template> + </el-table-column> + <el-table-column prop="adjStatus" label="调整状态" min-width="100"> + <template slot-scope="scope"> + <el-tag :type="scope.row.adjStatus === '1' ? 'success' : 'info'"> + {{ scope.row.adjStatus === '1' ? '正常' : '作废' }} + </el-tag> + </template> + </el-table-column> + <el-table-column prop="billDetailType" label="账单明细类型" min-width="120"> + <template slot-scope="scope"> + {{ scope.row.billDetailType === '1' ? '滞纳金' : '原账单' }} + </template> + </el-table-column> + <el-table-column prop="adjSource" label="调整来源" min-width="120"> + <template slot-scope="scope"> + {{ scope.row.adjSource === '1' ? '账单直接调整' : '合同调整' }} + </template> + </el-table-column> + <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip></el-table-column> + <el-table-column prop="createTime" label="创建时间" min-width="150" align="center"></el-table-column> + <el-table-column label="操作" width="120" align="center" fixed="right"> + <template slot-scope="scope"> + <el-button size="mini" type="text" @click="viewAdjustmentDetail(scope.row)" v-if="scope.row.adjStatus === '1'">附件详情</el-button> + <el-button size="mini" type="text" class="delete-btn" @click="voidAdjustment(scope.row)" v-if="scope.row.adjStatus === '1'">作废</el-button> + </template> + </el-table-column> + </el-table> + <div v-if="adjustmentList.length === 0" class="empty-data"> + <el-empty description="暂无账单调整记录"></el-empty> + </div> + <div class="pagination-container" v-if="adjustmentList.length > 0"> + <el-pagination + background + @size-change="handleAdjustmentSizeChange" + @current-change="handleAdjustmentCurrentChange" + :current-page="adjustmentQuery.pageNum" + :page-sizes="[5, 10, 20, 50]" + :page-size="adjustmentQuery.pageSize" + layout="total, sizes, prev, pager, next, jumper" + :total="adjustmentTotal"> + </el-pagination> + </div> + </div> + </div> + </el-tab-pane> + <el-tab-pane label="房源信息" name="roomInfo"> <el-table :data="roomList" border style="width: 100%"> <el-table-column prop="roomNumber" label="房号" min-width="100"></el-table-column> @@ -69,7 +190,19 @@ </el-tab-pane> <el-tab-pane label="附件信息" name="attachment"> - <el-table :data="attachmentList" border style="width: 100%"> + <div class="attachment-header"> + <el-upload + class="upload-area" + action="#" + :http-request="uploadFile" + :before-upload="beforeFileUpload" + multiple + :limit="10"> + <el-button type="primary" icon="el-icon-plus">添加附件</el-button> + <div slot="tip" class="el-upload__tip">只能上传jpg/png/pdf文件,且不超过5MB</div> + </el-upload> + </div> + <el-table :data="attachmentList" border style="width: 100%; margin-top: 15px"> <el-table-column prop="fileName" label="文件名" min-width="200" show-overflow-tooltip></el-table-column> <el-table-column prop="operatorName" label="操作人" width="150"></el-table-column> <el-table-column prop="operateTime" label="操作时间" width="180"></el-table-column> @@ -77,6 +210,7 @@ <template slot-scope="scope"> <el-button size="mini" type="text" @click="previewFile(scope.row)">预览</el-button> <el-button size="mini" type="text" @click="downloadFile(scope.row)">下载</el-button> + <el-button size="mini" type="text" class="delete-btn" @click="deleteFile(scope.row)">删除</el-button> </template> </el-table-column> </el-table> @@ -87,21 +221,289 @@ <el-tab-pane label="操作记录" name="operationLog"> <el-table :data="operationList" border style="width: 100%"> - <el-table-column prop="operationType" label="操作类型" width="150"></el-table-column> - <el-table-column prop="operatorName" label="操作人" width="150"></el-table-column> - <el-table-column prop="operateTime" label="操作时间" width="180"></el-table-column> - <el-table-column prop="remark" label="备注" min-width="300" show-overflow-tooltip></el-table-column> + <el-table-column prop="oprContent" label="操作内容" min-width="300" show-overflow-tooltip></el-table-column> + <el-table-column prop="oprPersonNm" label="操作人" width="150"></el-table-column> + <el-table-column prop="oprTime" label="操作时间" width="180"></el-table-column> </el-table> <div v-if="operationList.length === 0" class="empty-data"> <el-empty description="暂无操作记录"></el-empty> </div> + <div class="pagination-container" v-if="operationTotal > 0"> + <el-pagination + background + @size-change="handleOperationSizeChange" + @current-change="handleOperationCurrentChange" + :current-page="operationQuery.pageNum" + :page-sizes="[5, 10, 20, 50]" + :page-size="operationQuery.pageSize" + layout="total, sizes, prev, pager, next, jumper" + :total="operationTotal"> + </el-pagination> + </div> </el-tab-pane> </el-tabs> + + <!-- 文件预览对话框 --> + <el-dialog title="附件预览" :visible.sync="previewDialogVisible" width="800px" append-to-body> + <div class="preview-container" v-loading="previewLoading"> + <div v-if="previewUrl && isPdf" class="preview-iframe"> + <iframe :src="previewUrl" width="100%" height="500px" frameborder="0"></iframe> + </div> + <div v-else-if="previewUrl && isImage" class="preview-image"> + <img :src="previewUrl" style="max-width: 100%;" /> + </div> + <div v-else-if="!previewLoading" class="preview-error"> + <i class="el-icon-warning"></i> + <p>预览加载失败,请尝试下载后查看</p> + <div class="preview-actions"> + <el-button type="primary" @click="downloadCurrentPreview">下载文件</el-button> + </div> + </div> + </div> + <div slot="footer" class="dialog-footer"> + <el-button @click="previewDialogVisible = false">关 闭</el-button> + </div> + </el-dialog> + + <!-- 账单调整对话框 --> + <el-dialog title="账单调整" :visible.sync="adjustmentDialogVisible" width="700px" append-to-body @close="resetAdjustmentForm"> + <el-form ref="adjustmentForm" :model="adjustmentForm" :rules="adjustmentRules" label-width="120px"> + <el-form-item label="调整类型" prop="adjTpCd" required> + <el-radio-group v-model="adjustmentForm.adjTpCd"> + <el-radio label="1">调增</el-radio> + <el-radio label="2">调减</el-radio> + </el-radio-group> + </el-form-item> + + <el-form-item label="调整日期" prop="adjDate" required> + <el-date-picker + v-model="adjustmentForm.adjDate" + type="date" + placeholder="选择日期" + value-format="yyyy-MM-dd" + style="width: 220px"> + </el-date-picker> + </el-form-item> + + <el-form-item label="调整方式" prop="adjMethod" required> + <el-radio-group v-model="adjustmentForm.adjMethod" @change="handleAdjMethodChange"> + <el-radio label="1">按金额调整</el-radio> + <el-radio label="2">按比例调整</el-radio> + </el-radio-group> + </el-form-item> + + <el-form-item label="调整金额" prop="adjAmt" required v-if="adjustmentForm.adjMethod == '1'"> + <el-input-number + v-model="adjustmentForm.adjAmt" + :min="0" + :precision="2" + :max="adjustmentForm.adjTpCd === '2' ? billDetail.accigRcvAmt : undefined" + @change="calculateAdjustAmount" + style="width: 220px"> + </el-input-number> + <span class="form-hint">调整后金额: {{ formatAmount(adjustedAmount) }}</span> + </el-form-item> + + <el-form-item label="调整比例(%)" prop="adjRate" required v-if="adjustmentForm.adjMethod == '2'"> + <el-input-number + v-model="adjustmentForm.adjRate" + :min="0" + :max="adjustmentForm.adjTpCd === '2' ? 100 : undefined" + :precision="2" + @change="calculateAdjustAmount" + style="width: 220px"> + </el-input-number> + <span class="form-hint">调整后金额: {{ formatAmount(adjustedAmount) }}</span> + </el-form-item> + + <el-form-item label="备注" prop="remark"> + <el-input + type="textarea" + v-model="adjustmentForm.remark" + placeholder="请输入调整原因或其他备注信息" + :rows="3" + style="width: 400px"> + </el-input> + </el-form-item> + + <el-form-item label="附件"> + <el-upload + class="upload-area" + action="#" + :http-request="uploadAdjustmentAttachment" + :file-list="adjustmentFileList" + :before-upload="beforeAdjustmentUpload" + multiple + :limit="10"> + <el-button size="small" type="primary">添加附件</el-button> + <div slot="tip" class="el-upload__tip">只能上传jpg/png/pdf文件,且不超过5MB</div> + </el-upload> + + <el-table v-if="adjustmentFileList.length > 0" :data="adjustmentFileList" border style="width: 100%; margin-top: 15px"> + <el-table-column prop="name" label="文件名" min-width="200" show-overflow-tooltip></el-table-column> + <el-table-column label="操作" width="150" align="center"> + <template slot-scope="scope"> + <el-button size="mini" type="text" @click="previewAdjustmentFile(scope.row)">预览</el-button> + <el-button size="mini" type="text" class="delete-btn" @click="deleteAdjustmentFile(scope.$index)">删除</el-button> + </template> + </el-table-column> + </el-table> + </el-form-item> + </el-form> + <div slot="footer" class="dialog-footer"> + <el-button @click="adjustmentDialogVisible = false">取 消</el-button> + <el-button type="primary" @click="submitAdjustment" :loading="adjustmentSubmitting">确 定</el-button> + </div> + </el-dialog> + + <!-- 账单调整附件详情弹窗 --> + <el-dialog title="账单调整附件详情" :visible.sync="adjustmentAttachmentDialogVisible" width="800px" append-to-body> + <el-table :data="adjustmentAttachmentList" border style="width: 100%"> + <el-table-column prop="fileName" label="文件名" min-width="200" show-overflow-tooltip></el-table-column> + <el-table-column prop="createUserId" label="操作人" width="150"></el-table-column> + <el-table-column prop="createTime" label="操作时间" width="180"></el-table-column> + <el-table-column label="操作" width="200" align="center"> + <template slot-scope="scope"> + <el-button size="mini" type="text" @click="previewFile(scope.row)">预览</el-button> + <el-button size="mini" type="text" @click="downloadFile(scope.row)">下载</el-button> + <el-button size="mini" type="text" class="delete-btn" @click="deleteAdjustmentAttachment(scope.row)">删除</el-button> + </template> + </el-table-column> + </el-table> + <div v-if="adjustmentAttachmentList.length === 0" class="empty-data"> + <el-empty description="暂无附件信息"></el-empty> + </div> + <div slot="footer" class="dialog-footer"> + <el-button @click="adjustmentAttachmentDialogVisible = false">关 闭</el-button> + </div> + </el-dialog> + + <!-- 账单支付对话框 --> + <el-dialog title="账单支付" :visible.sync="paymentDialogVisible" width="800px" append-to-body @close="resetPaymentForm"> + <el-form ref="paymentForm" :model="paymentForm" :rules="paymentRules" label-width="120px" class="payment-form"> + <!-- 第一部分:结清状态 --> + <div class="payment-section"> + <h4>账单状态</h4> + <el-descriptions :column="1" border size="medium"> + <el-descriptions-item label="结清状态"> + <el-tag :type="getClearStatusType(billDetail.clearStatus)"> + {{ getClearStatusName(billDetail.clearStatus) }} + </el-tag> + </el-descriptions-item> + </el-descriptions> + </div> + + <!-- 第二部分:可编辑费用列表 --> + <div class="payment-section"> + <h4>费用信息</h4> + <el-descriptions :column="2" border size="medium"> + <el-descriptions-item label="费用类型">{{ billDetail.feTpName }}</el-descriptions-item> + <el-descriptions-item label="应收金额">{{ formatAmount(billDetail.accigRcvAmt) }}</el-descriptions-item> + <el-descriptions-item label="实收金额">{{ formatAmount(billDetail.atmRecvAmt) }}</el-descriptions-item> + <el-descriptions-item label="需收金额">{{ formatAmount(billDetail.needAmount) }}</el-descriptions-item> + </el-descriptions> + + <el-form-item style="margin-top: 20px;" label="本次收款" prop="occuAmt" required> + <div class="amount-input-wrapper"> + <el-input-number + v-model="paymentForm.occuAmt" + :min="0.01" + :max="billDetail.needAmount || 0" + :precision="2" + :step="0.01" + @change="calculateRemainingAmount" + style="width: 300px;margin-right: 10px;"> + </el-input-number> + <el-button size="small" type="primary" @click="fillFullAmount" class="fill-btn">收完</el-button> + </div> + </el-form-item> + + <el-form-item label="剩余待收"> + <span>{{ formatAmount(remainingAmount) }}</span> + </el-form-item> + + <el-form-item label="入账时间" prop="inaccDate" required> + <el-date-picker + v-model="paymentForm.inaccDate" + type="date" + placeholder="选择日期" + value-format="yyyy-MM-dd" + style="width: 300px"> + </el-date-picker> + </el-form-item> + </div> + + <!-- 第三部分:支付方式 --> + <div class="payment-section"> + <h4>支付方式</h4> + <el-form-item label="支付方式" prop="payModeName" required> + <el-select v-model="paymentForm.payModeName" placeholder="请选择支付方式" style="width: 300px"> + <el-option + v-for="item in paymentMethods" + :key="item.value" + :label="item.label" + :value="item.value"> + </el-option> + </el-select> + </el-form-item> + + <el-form-item label="凭证号"> + <el-input v-model="paymentForm.txVchrNo" placeholder="请输入凭证号" style="width: 300px"></el-input> + </el-form-item> + + <el-form-item label="摘要"> + <el-input v-model="paymentForm.summ" placeholder="请输入摘要" style="width: 300px"></el-input> + </el-form-item> + + <el-form-item label="备注"> + <el-input + type="textarea" + v-model="paymentForm.remark" + placeholder="请输入备注信息" + :rows="3" + style="width: 300px"> + </el-input> + </el-form-item> + </div> + + <!-- 第四部分:附件信息 --> + <div class="payment-section"> + <h4>附件信息</h4> + <div class="upload-wrapper"> + <el-upload + class="upload-area" + action="#" + :http-request="uploadPaymentAttachment" + :file-list="paymentFileList" + :before-upload="beforePaymentUpload" + multiple + :limit="10"> + <el-button size="small" type="primary">添加附件</el-button> + <div slot="tip" class="el-upload__tip">只能上传jpg/png/pdf文件,且不超过5MB</div> + </el-upload> + </div> + + <el-table v-if="paymentFileList.length > 0" :data="paymentFileList" border style="width: 100%; margin-top: 15px"> + <el-table-column prop="name" label="文件名" min-width="200" show-overflow-tooltip></el-table-column> + <el-table-column label="操作" width="150" align="center"> + <template slot-scope="scope"> + <el-button size="mini" type="text" @click="previewPaymentFile(scope.row)">预览</el-button> + <el-button size="mini" type="text" class="delete-btn" @click="deletePaymentFile(scope.$index)">删除</el-button> + </template> + </el-table-column> + </el-table> + </div> + </el-form> + <div slot="footer" class="dialog-footer"> + <el-button @click="paymentDialogVisible = false">取 消</el-button> + <el-button type="primary" @click="submitPayment" :loading="paymentSubmitting">确 定</el-button> + </div> + </el-dialog> </div> </template> <script> -import { getBillDetail, getBillAttachmentList } from '@/api/finance' +import { getBillDetail, getBillAttachmentList, deleteBillAttachment, getBillAdjustmentList, addBillAdjustment, voidBillAdjustment, uploadBillAdjustmentAttachment, uploadBillAttachment, getBillAdjustmentAttachmentList, deleteBillAdjustmentAttachment, getBillOperationRecords, submitBillPayment, uploadBillPaymentAttachment, deleteBillPaymentAttachment } from '@/api/finance' export default { name: 'BillDetail', @@ -116,14 +518,132 @@ export default { activeTab: 'basicInfo', loading: false, billDetail: {}, + billDetailsList: [], roomList: [], attachmentList: [], - operationList: [] + operationList: [], + + // 账单调整相关 + adjustmentList: [], + adjustmentTotal: 0, + adjustmentQuery: { + pageNum: 1, + pageSize: 10, + billId: '' + }, + adjustmentLoading: false, + adjustmentDialogVisible: false, + adjustmentSubmitting: false, + adjustedAmount: 0, + adjustmentForm: { + billId: '', + billDetailId: '', + adjTpCd: '1', // 默认调增 + adjDate: new Date().toISOString().slice(0, 10), // 默认今天 + adjMethod: '1', // 默认按金额调整 + adjAmt: 0, + adjRate: 0, + billDetailType: '原账单', + adjSource: '账单直接调整', + projectId: '', + remark: '', + adjSource: '1' // 默认 账单直接调整 + }, + adjustmentRules: { + adjTpCd: [ + { required: true, message: '请选择调整类型', trigger: 'change' } + ], + adjDate: [ + { required: true, message: '请选择调整日期', trigger: 'change' } + ], + adjMethod: [ + { required: true, message: '请选择调整方式', trigger: 'change' } + ], + adjAmt: [ + { required: true, message: '请输入调整金额', trigger: 'blur' } + ], + adjRate: [ + { required: true, message: '请输入调整比例', trigger: 'blur' } + ] + }, + adjustmentFileList: [], + + // 预览相关 + previewDialogVisible: false, + previewLoading: false, + previewUrl: '', + previewBlob: null, + currentPreviewName: '', + isPdf: false, + isImage: false, + + // 调整附件相关 + adjustmentAttachmentDialogVisible: false, + adjustmentAttachmentList: [], + currentAdjustmentId: '', + + // 操作记录相关 + operationTotal: 0, + operationQuery: { + pageNum: 1, + pageSize: 10 + }, + + // 支付相关 + paymentDialogVisible: false, + paymentSubmitting: false, + paymentForm: { + billId: '', + billDetailId: '', + occuAmt: 0, + inaccDate: new Date().toISOString().slice(0, 10), + payModeName: '1', + txVchrNo: '', + summ: '', + remark: '' + }, + paymentRules: { + occuAmt: [ + { required: true, message: '请输入本次收款金额', trigger: 'blur' } + ], + inaccDate: [ + { required: true, message: '请选择入账时间', trigger: 'change' } + ], + payModeName: [ + { required: true, message: '请选择支付方式', trigger: 'change' } + ] + }, + paymentFileList: [], + paymentMethods: [ + { value: '1', label: '现金' }, + { value: '2', label: '网银转账' }, + { value: '3', label: 'POS机' }, + { value: '4', label: '支付宝' }, + { value: '5', label: '微信' }, + { value: '6', label: '转账支票' }, + { value: '7', label: '其他方式' }, + { value: '8', label: '线上支付' } + ], + remainingAmount: 0, // 剩余待收金额 } }, created() { this.getBillData() }, + computed: { + // 判断是否可以调整账单 + canAdjustBill() { + if (!this.billDetail) return false + // 账单状态为开启且不在支付中 + return this.billDetail.billStatus === '1' && this.billDetail.clearStatus !== '6' + }, + // 判断是否可以支付账单 + canPayBill() { + if (!this.billDetail) return false + // 账单不能是已结清,且不能在支付中 + return this.billDetail.clearStatus !== '1' && this.billDetail.clearStatus !== '6' + } + }, methods: { // 获取账单数据 getBillData() { @@ -133,6 +653,14 @@ export default { getBillDetail(this.billId).then(response => { this.billDetail = response.data || {} + // 处理账单明细 + this.processBillDetails() + + // 设置调整查询参数的账单ID + this.adjustmentQuery.billId = this.billId + // 获取账单调整记录 + this.getAdjustmentList() + // 模拟房源信息数据,实际应从接口获取或者账单详情中提取 this.roomList = [ { @@ -151,19 +679,50 @@ export default { }) // 获取附件列表 - getBillAttachmentList(this.billId).then(response => { - this.attachmentList = response.data || [] - }) + this.getAttachmentList() - // 模拟操作记录数据,实际应从接口获取 - this.operationList = [ - { - operationType: '创建账单', - operatorName: '系统管理员', - operateTime: '2023-06-20 10:30:00', - remark: '创建新账单' - } - ] + // 获取操作记录 + this.getOperationList() + }, + // 处理账单明细数据 + processBillDetails() { + this.billDetailsList = [] + + // 添加原始账单明细 + if (this.billDetail) { + this.billDetailsList.push({ + feeTypeName: this.billDetail.feTpName || '-', + receivableAmount: this.billDetail.accigRcvAmt, + taxRate: this.billDetail.taxRate || 0, + taxAmount: this.billDetail.paybleTaxAmount || 0, + startDate: this.billDetail.chggBgnDt || '-', + endDate: this.billDetail.chggEndDt || '-', + remark: this.billDetail.billRemark || '-', + billDetailType: '0' // 原账单类型 + }) + } + + // 添加滞纳金明细(如果存在) + if (this.billDetail.ovdueStatus === '1' && this.billDetail.receivableOvdueAmt > 0) { + this.billDetailsList.push({ + feeTypeName: '滞纳金', + receivableAmount: this.billDetail.receivableOvdueAmt || 0, + taxRate: 0, + taxAmount: 0, + startDate: this.billDetail.pybDt || '-', // 滞纳金产生日期,使用应收日期 + endDate: this.formatDate(new Date()) || '-', // 滞纳金产生的结束日期,使用当前日期 + remark: '滞纳金', + billDetailType: '1' // 滞纳金类型 + }) + } + }, + // 格式化日期 + formatDate(date) { + if (!(date instanceof Date)) return ''; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; }, // 格式化金额 formatAmount(amount) { @@ -229,8 +788,65 @@ export default { }, // 预览文件 previewFile(file) { - window.open(file.storePath) + this.previewLoading = true + this.previewDialogVisible = true + this.currentPreviewName = file.fileName || '附件' + this.previewUrl = '' + this.isPdf = false + this.isImage = false + + // 根据文件名后缀判断文件类型 + const fileExt = file.fileName ? file.fileName.split('.').pop().toLowerCase() : '' + + try { + // 创建blob对象 - 实际项目中应该调用预览API获取文件数据 + fetch(file.storePath) + .then(response => response.blob()) + .then(blob => { + this.previewBlob = blob + + // 判断文件类型 + if (fileExt === 'pdf') { + this.isPdf = true + this.isImage = false + } else if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt)) { + this.isPdf = false + this.isImage = true + } + + // 创建URL用于预览 + this.previewUrl = URL.createObjectURL(blob) + this.previewLoading = false + }) + .catch(error => { + console.error('预览失败', error) + this.$message.error('获取预览数据失败') + this.previewLoading = false + }) + } catch (error) { + console.error('预览失败', error) + this.$message.error('获取预览数据失败') + this.previewLoading = false + } }, + + // 下载当前预览的文件 + downloadCurrentPreview() { + if (!this.previewBlob) { + this.$message.error('没有可下载的文件') + return + } + + const url = URL.createObjectURL(this.previewBlob) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', this.currentPreviewName) + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + }, + // 下载文件 downloadFile(file) { // 实际应调用API下载 @@ -238,19 +854,729 @@ export default { link.href = file.storePath link.download = file.fileName link.click() + }, + + // 删除附件 + deleteFile(file) { + this.$confirm('确认删除该附件?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + deleteBillAttachment(file.id).then(response => { + if (response.code === '0000000000000000') { + this.$message.success('删除成功') + // 重新获取附件列表 + this.getAttachmentList() + } else { + this.$message.error(response.message || '删除失败') + } + }).catch(() => { + this.$message.error('删除失败') + }) + }).catch(() => {}) + }, + + // 获取附件列表 + getAttachmentList() { + getBillAttachmentList(this.billId).then(response => { + this.attachmentList = response.data || [] + }) + }, + // 获取账单调整记录 + getAdjustmentList() { + this.adjustmentLoading = true + getBillAdjustmentList({ + ...this.adjustmentQuery, + billId: this.billId + }).then(response => { + if (response.code === '0000000000000000') { + this.adjustmentList = response.data.list || [] + this.adjustmentTotal = response.data.total || 0 + } else { + this.$message.error(response.message || '获取账单调整记录失败') + this.adjustmentList = [] + this.adjustmentTotal = 0 + } + this.adjustmentLoading = false + }).catch(() => { + this.adjustmentList = [] + this.adjustmentTotal = 0 + this.adjustmentLoading = false + }) + }, + // 账单调整分页大小变化 + handleAdjustmentSizeChange(val) { + this.adjustmentQuery.pageSize = val + this.getAdjustmentList() + }, + // 账单调整页码变化 + handleAdjustmentCurrentChange(val) { + this.adjustmentQuery.pageNum = val + this.getAdjustmentList() + }, + // 查看账单调整详情 + viewAdjustmentDetail(row) { + this.adjustmentAttachmentDialogVisible = true + this.adjustmentAttachmentList = [] + this.currentAdjustmentId = row.id + this.getAdjustmentAttachmentList() + }, + // 进行账单调整 + handleAdjustment(row = null) { + this.adjustmentDialogVisible = true + this.adjustmentForm.billId = this.billId + this.adjustmentForm.billDetailId = this.billDetail.billDetails && this.billDetail.billDetails.length > 0 + ? this.billDetail.billDetails[0].id + : '' + this.adjustmentForm.projectId = this.billDetail.projectId || '' + + // 设置账单明细类型 + if (row && row.billDetailType) { + this.adjustmentForm.billDetailType = row.billDetailType + } else { + this.adjustmentForm.billDetailType = '0' // 默认原账单 + } + + // 预设调整金额为0,方便用户输入 + this.adjustmentForm.adjAmt = 0 + this.adjustmentForm.adjRate = 0 + this.calculateAdjustAmount() + }, + // 调整方式变更 + handleAdjMethodChange() { + // 重新计算调整后金额 + this.calculateAdjustAmount() + }, + // 计算调整后金额 + calculateAdjustAmount() { + const originalAmount = parseFloat(this.billDetail.accigRcvAmt || 0) + let adjustmentValue = 0 + + if (this.adjustmentForm.adjMethod === '1') { + adjustmentValue = parseFloat(this.adjustmentForm.adjAmt || 0) + } else if (this.adjustmentForm.adjMethod === '2') { + // 按比例计算调整金额 + adjustmentValue = originalAmount * parseFloat(this.adjustmentForm.adjRate || 0) / 100 + } + + // 根据调整类型计算最终金额 + if (this.adjustmentForm.adjTpCd === '1') { // 调增 + this.adjustedAmount = originalAmount + adjustmentValue + } else { // 调减 + this.adjustedAmount = Math.max(0, originalAmount - adjustmentValue) + } + }, + // 上传附件前检查 + beforeAdjustmentUpload(file) { + const isValidType = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'application/pdf' + const isLt5M = file.size / 1024 / 1024 < 5 + + if (!isValidType) { + this.$message.error('只能上传JPG/PNG/PDF格式的文件!') + } + + if (!isLt5M) { + this.$message.error('文件大小不能超过5MB!') + } + + return isValidType && isLt5M + }, + // 上传调整附件 + uploadAdjustmentAttachment(options) { + const file = options.file + + // 创建临时URL供预览 + const tempUrl = URL.createObjectURL(file) + + // 向上传列表中添加文件 + this.adjustmentFileList.push({ + name: file.name, + file: file, + url: tempUrl, + status: 'ready' + }) + }, + // 预览调整附件 + previewAdjustmentFile(file) { + this.previewLoading = true + this.previewDialogVisible = true + this.currentPreviewName = file.name || '附件' + this.previewUrl = '' + this.isPdf = false + this.isImage = false + + // 根据文件名后缀判断文件类型 + const fileExt = file.name ? file.name.split('.').pop().toLowerCase() : '' + + try { + // 获取文件内容 + const blob = file.file ? file.file : new Blob() + this.previewBlob = blob + + // 设置文件类型标志 + if (fileExt === 'pdf') { + this.isPdf = true + } else if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt)) { + this.isImage = true + } + + // 创建URL用于预览 + this.previewUrl = file.url + this.previewLoading = false + } catch (error) { + console.error('预览失败', error) + this.$message.error('获取预览数据失败') + this.previewLoading = false + } + }, + // 删除调整附件 + deleteAdjustmentFile(index) { + this.adjustmentFileList.splice(index, 1) + }, + // 重置调整表单 + resetAdjustmentForm() { + if (this.$refs.adjustmentForm) { + this.$refs.adjustmentForm.resetFields() + } + this.adjustmentFileList = [] + this.adjustedAmount = 0 + this.adjustmentForm = { + billId: '', + billDetailId: '', + adjTpCd: '1', + adjDate: new Date().toISOString().slice(0, 10), + adjMethod: '1', + adjAmt: 0, + adjRate: 0, + billDetailType: '原账单', + adjSource: '账单直接调整', + projectId: '', + remark: '', + adjSource: '1' + } + }, + // 提交账单调整 + submitAdjustment() { + this.$refs.adjustmentForm.validate(valid => { + if (valid) { + this.adjustmentSubmitting = true + + // 构建请求数据 + const adjustmentData = { ...this.adjustmentForm } + + // 如果是比例调整,需要计算实际调整金额 + if (adjustmentData.adjMethod === '2') { + const originalAmount = parseFloat(this.billDetail.accigRcvAmt || 0) + adjustmentData.adjAmt = originalAmount * parseFloat(adjustmentData.adjRate || 0) / 100 + } + + // 提交调整 + addBillAdjustment(adjustmentData) + .then(response => { + if (response.code === '0000000000000000') { + // 上传附件 + if (this.adjustmentFileList.length > 0) { + this.uploadAdjustmentAttachments(response.data) + } else { + this.$message.success('账单调整成功') + this.adjustmentDialogVisible = false + // 重新获取账单信息和调整记录 + this.getBillData() + this.adjustmentSubmitting = false + } + } else { + this.$message.error(response.message || '账单调整失败') + this.adjustmentSubmitting = false + } + }) + .catch(error => { + console.error('账单调整失败', error) + this.$message.error('账单调整失败,请稍后重试') + this.adjustmentSubmitting = false + }) + } + }) + }, + // 上传调整附件 + uploadAdjustmentAttachments(adjustmentId) { + const uploadPromises = this.adjustmentFileList.map(fileItem => { + return uploadBillAdjustmentAttachment(adjustmentId, fileItem.file) + }) + + Promise.all(uploadPromises) + .then(() => { + this.$message.success('账单调整成功') + this.adjustmentDialogVisible = false + // 重新获取账单信息和调整记录 + this.getBillData() + this.adjustmentSubmitting = false + }) + .catch(error => { + console.error('附件上传失败', error) + this.$message.warning('账单调整成功,但部分附件上传失败') + this.adjustmentDialogVisible = false + // 重新获取账单信息和调整记录 + this.getBillData() + this.adjustmentSubmitting = false + }) + }, + // 作废调整记录 + voidAdjustment(row) { + this.$confirm('确定要作废此账单调整吗?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + voidBillAdjustment({ + adjustmentId: row.id, + canceledDate: new Date().toISOString().slice(0, 10), + remark: '用户手动作废' + }).then(response => { + if (response.code === '0000000000000000') { + this.$message.success('作废成功') + // 重新获取账单信息和调整记录 + this.getBillData() + } else { + this.$message.error(response.message || '作废失败') + } + }).catch(() => { + this.$message.error('作废失败') + }) + }).catch(() => {}) + }, + // 上传前检查文件 + beforeFileUpload(file) { + const isValidType = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'application/pdf' + const isLt5M = file.size / 1024 / 1024 < 5 + + if (!isValidType) { + this.$message.error('只能上传JPG/PNG/PDF格式的文件!') + } + + if (!isLt5M) { + this.$message.error('文件大小不能超过5MB!') + } + + return isValidType && isLt5M + }, + // 上传文件 + uploadFile(options) { + const file = options.file + this.$confirm('确认上传该文件?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'info' + }).then(() => { + this.$message.info('开始上传附件,请稍候...') + // 调用上传附件API + uploadBillAttachment(this.billDetail.id, file).then(response => { + if (response.code === '0000000000000000') { + this.$message.success('上传成功') + // 重新获取附件列表 + this.getAttachmentList() + } else { + this.$message.error(response.message || '上传失败') + } + }).catch(() => { + this.$message.error('上传失败') + }) + }).catch(() => { + // 取消上传 + }) + }, + // 获取调整附件列表 + getAdjustmentAttachmentList() { + getBillAdjustmentAttachmentList(this.currentAdjustmentId).then(response => { + if (response.code === '0000000000000000') { + this.adjustmentAttachmentList = response.data || [] + } else { + this.$message.error(response.message || '获取附件列表失败') + this.adjustmentAttachmentList = [] + } + }).catch(() => { + this.$message.error('获取附件列表失败') + this.adjustmentAttachmentList = [] + }) + }, + // 删除账单调整附件 + deleteAdjustmentAttachment(file) { + this.$confirm('确认删除该附件?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + deleteBillAdjustmentAttachment(file.id).then(response => { + if (response.code === '0000000000000000') { + this.$message.success('删除成功') + // 重新获取附件列表 + this.getAdjustmentAttachmentList() + } else { + this.$message.error(response.message || '删除失败') + } + }).catch(() => { + this.$message.error('删除失败') + }) + }).catch(() => {}) + }, + // 操作记录分页大小变化 + handleOperationSizeChange(val) { + this.operationQuery.pageSize = val + this.getOperationList() + }, + // 操作记录页码变化 + handleOperationCurrentChange(val) { + this.operationQuery.pageNum = val + this.getOperationList() + }, + // 获取操作记录列表 + getOperationList() { + getBillOperationRecords(this.billId, this.operationQuery).then(response => { + if (response.code === '0000000000000000') { + this.operationList = response.data.list || [] + this.operationTotal = response.data.total || 0 + } else { + this.$message.error(response.message || '获取操作记录失败') + this.operationList = [] + this.operationTotal = 0 + } + }).catch(() => { + this.$message.error('获取操作记录失败') + this.operationList = [] + this.operationTotal = 0 + }) + }, + // 打开支付对话框 + openPaymentDialog() { + // 重置表单 + this.resetPaymentForm() + + this.paymentDialogVisible = true + this.paymentForm.billId = this.billId + this.paymentForm.billDetailId = this.billDetail.billDetails && this.billDetail.billDetails.length > 0 + ? this.billDetail.billDetails[0].id + : '' + this.paymentForm.occuAmt = 0 + this.calculateRemainingAmount() + }, + // 计算剩余待收金额 + calculateRemainingAmount() { + const needAmount = parseFloat(this.billDetail.needAmount || 0) + const paymentAmount = parseFloat(this.paymentForm.occuAmt || 0) + this.remainingAmount = Math.max(0, needAmount - paymentAmount) + }, + // 填充全额 + fillFullAmount() { + this.paymentForm.occuAmt = parseFloat(this.billDetail.needAmount || 0) + this.calculateRemainingAmount() + }, + // 重置支付表单 + resetPaymentForm() { + if (this.$refs.paymentForm) { + this.$refs.paymentForm.resetFields() + } + this.paymentFileList = [] + this.remainingAmount = 0 + this.paymentForm = { + billId: '', + billDetailId: '', + occuAmt: 0, + inaccDate: new Date().toISOString().slice(0, 10), + payModeName: '1', + txVchrNo: '', + summ: '', + remark: '' + } + }, + // 上传前检查支付附件 + beforePaymentUpload(file) { + const isValidType = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'application/pdf' + const isLt5M = file.size / 1024 / 1024 < 5 + + if (!isValidType) { + this.$message.error('只能上传JPG/PNG/PDF格式的文件!') + return false + } + + if (!isLt5M) { + this.$message.error('文件大小不能超过5MB!') + return false + } + + return isValidType && isLt5M + }, + // 上传支付附件 + uploadPaymentAttachment(options) { + const file = options.file + + // 创建临时URL供预览 + const tempUrl = URL.createObjectURL(file) + + // 向上传列表中添加文件 + this.paymentFileList.push({ + name: file.name, + file: file, + url: tempUrl, + status: 'ready' + }) + }, + // 预览支付附件 + previewPaymentFile(file) { + this.previewLoading = true + this.previewDialogVisible = true + this.currentPreviewName = file.name || '附件' + this.previewUrl = '' + this.isPdf = false + this.isImage = false + + // 根据文件名后缀判断文件类型 + const fileExt = file.name ? file.name.split('.').pop().toLowerCase() : '' + + try { + // 获取文件内容 + const blob = file.file ? file.file : new Blob() + this.previewBlob = blob + + // 设置文件类型标志 + if (fileExt === 'pdf') { + this.isPdf = true + } else if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt)) { + this.isImage = true + } + + // 创建URL用于预览 + this.previewUrl = file.url + this.previewLoading = false + } catch (error) { + console.error('预览失败', error) + this.$message.error('获取预览数据失败') + this.previewLoading = false + } + }, + // 删除支付附件 + deletePaymentFile(index) { + this.paymentFileList.splice(index, 1) + }, + // 提交支付 + submitPayment() { + this.$refs.paymentForm.validate(valid => { + if (valid) { + // 验证支付金额 + if (this.paymentForm.occuAmt <= 0) { + this.$message.error('支付金额必须大于0') + return + } + + if (this.paymentForm.occuAmt > this.billDetail.needAmount) { + this.$message.error('支付金额不能超过需收金额') + return + } + + this.paymentSubmitting = true + + // 准备表单数据 + const paymentData = {...this.paymentForm} + + // 如果有附件,需要转换日期格式,后端接收Date类型 + if (paymentData.inaccDate) { + // 保持字符串格式,后端会处理 + } + + submitBillPayment(paymentData) + .then(response => { + if (response.code === '0000000000000000') { + // 上传附件 + if (this.paymentFileList.length > 0) { + this.uploadPaymentAttachments(response.data.id) + } else { + this.$message.success('账单支付成功') + this.paymentDialogVisible = false + // 重新获取账单信息 + this.getBillData() + this.paymentSubmitting = false + } + } else { + this.$message.error(response.message || '账单支付失败') + this.paymentSubmitting = false + } + }) + .catch(error => { + console.error('账单支付失败', error) + this.$message.error('账单支付失败,请稍后重试') + this.paymentSubmitting = false + }) + } + }) + }, + // 上传支付附件 + uploadPaymentAttachments(transactionId) { + if (this.paymentFileList.length === 0) { + this.$message.success('账单支付成功') + this.paymentDialogVisible = false + // 重新获取账单信息 + this.getBillData() + this.paymentSubmitting = false + return + } + + const uploadPromises = this.paymentFileList.map(fileItem => { + return uploadBillPaymentAttachment(transactionId, fileItem.file) + }) + + Promise.all(uploadPromises) + .then(() => { + this.$message.success('账单支付成功') + this.paymentDialogVisible = false + // 重新获取账单信息 + this.getBillData() + this.paymentSubmitting = false + }) + .catch(error => { + console.error('附件上传失败', error) + this.$message.warning('账单支付成功,但部分附件上传失败') + this.paymentDialogVisible = false + // 重新获取账单信息 + this.getBillData() + this.paymentSubmitting = false + }) } } } </script> <style lang="scss" scoped> -.bill-detail-container { - padding: 10px; - - .empty-data { - margin: 20px 0; - display: flex; - justify-content: center; + .delete-btn { + color: #F56C6C; + } + + .pagination-container { + margin-top: 20px; + text-align: right; + } + + .bill-detail-container { + padding: 10px; + + .bill-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + + .bill-header-left { + h3 { + margin: 0; + } + } + } + + .payment-form { + .el-form-item { + margin-bottom: 18px; + } + + .amount-input-wrapper { + display: flex; + align-items: center; + + .fill-btn { + margin-left: 10px; + height: 32px; + padding: 8px 15px; + } + } + } + + .payment-section { + margin-bottom: 25px; + + h4 { + margin: 15px 0 10px; + padding-left: 8px; + border-left: 3px solid #409EFF; + font-size: 16px; + color: #303133; + } + + .el-descriptions { + margin-bottom: 15px; + } + + .upload-wrapper { + margin-bottom: 10px; + padding: 10px; + background-color: #f9f9f9; + border-radius: 4px; + } + } + + .empty-data { + margin: 20px 0; + display: flex; + justify-content: center; + } + + .bill-adjustment-container { + .operation-bar { + display: flex; + justify-content: flex-end; + margin-bottom: 15px; + } + } + + .form-hint { + margin-left: 10px; + color: #909399; + font-size: 13px; + } + + .upload-area { + margin-bottom: 15px; + } + + .preview-container { + min-height: 500px; + max-height: 700px; + overflow: auto; + + .preview-iframe { + width: 100%; + height: 500px; + border: none; + } + + .preview-image { + display: flex; + justify-content: center; + align-items: center; + min-height: 300px; + } + + .preview-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 300px; + color: #909399; + + i { + font-size: 48px; + margin-bottom: 20px; + } + + p { + font-size: 16px; + } + + .preview-actions { + margin-top: 20px; + } + } + } + + .attachment-header { + margin-bottom: 15px; + } } -} </style> \ No newline at end of file diff --git a/pc/src/views/finance/billList/index.vue b/pc/src/views/finance/billList/index.vue index 4a9bbef..9eb4dba 100644 --- a/pc/src/views/finance/billList/index.vue +++ b/pc/src/views/finance/billList/index.vue @@ -2,17 +2,17 @@ <div class="app-container"> <!-- 搜索表单 --> <el-form :model="queryParams" ref="queryForm" :inline="true" class="search-form"> - <el-form-item label="合同编号" prop="contractNumber"> - <el-input v-model="queryParams.contractNumber" placeholder="请输入合同编号" clearable style="width: 220px" /> + <el-form-item label="合同编号" prop="contractNo"> + <el-input v-model="queryParams.contractNo" placeholder="请输入合同编号" clearable style="width: 220px" /> </el-form-item> <el-form-item label="房号" prop="roomNumber"> <el-input v-model="queryParams.roomNumber" placeholder="请输入房号" clearable style="width: 220px" /> </el-form-item> - <el-form-item label="对方名称" prop="payeeName"> - <el-input v-model="queryParams.payeeName" placeholder="请输入对方名称" clearable style="width: 220px" /> + <el-form-item label="对方名称" prop="paysdName"> + <el-input v-model="queryParams.paysdName" placeholder="请输入对方名称" clearable style="width: 220px" /> </el-form-item> - <el-form-item label="账单编号" prop="billNumber"> - <el-input v-model="queryParams.billNumber" placeholder="请输入账单编号" clearable style="width: 220px" /> + <el-form-item label="账单编号" prop="totBillNo"> + <el-input v-model="queryParams.totBillNo" placeholder="请输入账单编号" clearable style="width: 220px" /> </el-form-item> <el-form-item label="账单状态" prop="billStatus"> <el-select v-model="queryParams.billStatus" placeholder="请选择账单状态" clearable multiple style="width: 220px"> @@ -20,7 +20,7 @@ <el-option label="关闭" value="0" /> </el-select> </el-form-item> - <el-form-item label="应收时间" prop="receivableDate"> + <el-form-item label="应收时间" prop="pybDt"> <el-date-picker v-model="dateRange" type="daterange" @@ -77,11 +77,11 @@ <!-- 表格区域 --> <el-table v-loading="loading" :data="billList" border> - <el-table-column label="对方名称" prop="payeeName" min-width="120" show-overflow-tooltip /> + <el-table-column label="对方名称" prop="paysdName" min-width="120" show-overflow-tooltip /> <el-table-column label="项目楼宇名称" prop="projectName" min-width="120" show-overflow-tooltip /> <el-table-column label="房号" prop="roomNumber" min-width="100" show-overflow-tooltip /> - <el-table-column label="账单编号" prop="billNumber" min-width="150" show-overflow-tooltip /> - <el-table-column label="合同编号" prop="contractNumber" min-width="150" show-overflow-tooltip /> + <el-table-column label="账单编号" prop="totBillNo" min-width="150" show-overflow-tooltip /> + <el-table-column label="合同编号" prop="contractNo" min-width="150" show-overflow-tooltip /> <el-table-column label="结清状态" min-width="100" align="center"> <template slot-scope="scope"> <el-tag :type="getClearStatusType(scope.row.clearStatus)"> @@ -89,25 +89,25 @@ </el-tag> </template> </el-table-column> - <el-table-column label="费用类型" prop="feeTypeName" min-width="120" show-overflow-tooltip /> - <el-table-column label="账单金额" prop="billAmount" min-width="100" align="right"> + <el-table-column label="费用类型" prop="feTpName" min-width="120" show-overflow-tooltip /> + <el-table-column label="账单金额" prop="accblAmt" min-width="100" align="right"> <template slot-scope="scope"> - {{ scope.row.billAmount ? scope.row.billAmount.toFixed(2) : '0.00' }} + {{ scope.row.accblAmt ? scope.row.accblAmt.toFixed(2) : '0.00' }} </template> </el-table-column> - <el-table-column label="应收金额" prop="receivableAmount" min-width="100" align="right"> + <el-table-column label="应收金额" prop="accigRcvAmt" min-width="100" align="right"> <template slot-scope="scope"> - {{ scope.row.receivableAmount ? scope.row.receivableAmount.toFixed(2) : '0.00' }} + {{ scope.row.accigRcvAmt ? scope.row.accigRcvAmt.toFixed(2) : '0.00' }} </template> </el-table-column> - <el-table-column label="实收金额" prop="receivedAmount" min-width="100" align="right"> + <el-table-column label="实收金额" prop="atmRecvAmt" min-width="100" align="right"> <template slot-scope="scope"> - {{ scope.row.receivedAmount ? scope.row.receivedAmount.toFixed(2) : '0.00' }} + {{ scope.row.atmRecvAmt ? scope.row.atmRecvAmt.toFixed(2) : '0.00' }} </template> </el-table-column> - <el-table-column label="开始日期" prop="billingStartDate" min-width="120" align="center" /> - <el-table-column label="结束日期" prop="billingEndDate" min-width="120" align="center" /> - <el-table-column label="应收日期" prop="receivableDate" min-width="120" align="center" /> + <el-table-column label="开始日期" prop="chggBgnDt" min-width="120" align="center" /> + <el-table-column label="结束日期" prop="chggEndDt" min-width="120" align="center" /> + <el-table-column label="应收日期" prop="pybDt" min-width="120" align="center" /> <el-table-column label="操作" width="150" align="center" fixed="right"> <template slot-scope="scope"> <el-button size="mini" type="text" icon="el-icon-view" @click="handleViewDetail(scope.row)">查看</el-button> @@ -171,10 +171,10 @@ export default { queryParams: { pageNum: 1, pageSize: 10, - contractNumber: '', + contractNo: '', roomNumber: '', - payeeName: '', - billNumber: '', + paysdName: '', + totBillNo: '', billStatus: [], receivableDateStart: '', receivableDateEnd: '', @@ -217,7 +217,7 @@ export default { } getBillList(queryParams).then(response => { - this.billList = response.data.data || [] + this.billList = response.data.list || [] this.total = response.data.total this.loading = false }).catch(() => { @@ -250,10 +250,10 @@ export default { children = category.financeFeeTypes.map(feeType => { return { id: feeType.id || Math.random().toString(36).substr(2, 9), - label: feeType.feeTypeName || '未命名费用', + label: feeType.feTpName || '未命名费用', categoryId: categoryId, // 存储原始数据以便后续使用 - feeTypeName: feeType.feeTypeName || '未命名费用', + feTpName: feeType.feTpName || '未命名费用', // 确保没有更多的子节点 children: null } @@ -353,12 +353,28 @@ export default { this.$refs.addBillForm.validateForm().then(valid => { if (valid) { const billData = this.$refs.addBillForm.getFormData() + billData.billType = 1 + billData.billSource = 3 + addBill(billData).then(response => { - this.$message.success('添加账单成功') - this.dialogVisible = false - this.getList() + if (response.code === '0000000000000000') { + this.$message.success('添加账单成功') + + // 如果有附件,上传附件 + const fileList = this.$refs.addBillForm.fileList + if (fileList && fileList.length > 0) { + this.$refs.addBillForm.uploadAttachments(response.data) + // 上传附件由组件内部处理后续流程,不需要在这里关闭弹窗 + } else { + this.dialogVisible = false + this.getList() + } + } else { + this.$message.error(response.message || '添加账单失败') + } }).catch(() => { // 错误处理 + this.$message.error('添加账单失败') }) } }) diff --git a/pc/src/views/finance/transaction/components/TransactionDetail.vue b/pc/src/views/finance/transaction/components/TransactionDetail.vue new file mode 100644 index 0000000..ac342a0 --- /dev/null +++ b/pc/src/views/finance/transaction/components/TransactionDetail.vue @@ -0,0 +1,520 @@ +<template> + <div class="transaction-detail-container" v-loading="loading"> + <!-- 详情主体 --> + <el-tabs v-model="activeTab"> + <!-- 总体信息 --> + <el-tab-pane label="总体信息" name="overall"> + <el-card class="box-card"> + <div slot="header" class="clearfix"> + <span>总体信息</span> + </div> + <el-descriptions :column="2" border> + <el-descriptions-item label="对方单位名称">{{ detailData.totalInfo && detailData.totalInfo.unitName || '-' }}</el-descriptions-item> + <el-descriptions-item label="总金额"> + <span class="amount">{{ formatAmount(detailData.totalInfo && detailData.totalInfo.totalAmount) }}</span> + </el-descriptions-item> + <el-descriptions-item label="已匹配金额"> + <span class="amount matched">{{ formatAmount(detailData.totalInfo && detailData.totalInfo.matchedAmount) }}</span> + </el-descriptions-item> + <el-descriptions-item label="未匹配金额"> + <span class="amount unmatched">{{ formatAmount(detailData.totalInfo && detailData.totalInfo.unmatchedAmount) }}</span> + </el-descriptions-item> + </el-descriptions> + </el-card> + </el-tab-pane> + + <!-- 流水信息 --> + <el-tab-pane label="流水信息" name="transaction"> + <el-card class="box-card"> + <div slot="header" class="clearfix"> + <span>流水信息</span> + </div> + <el-descriptions :column="2" border> + <el-descriptions-item label="借贷标"> + <el-tag :type="(detailData.transaction && detailData.transaction.dcFlagCode === '0') ? 'danger' : 'success'"> + {{ (detailData.transaction && detailData.transaction.dcFlagCode === '0') ? '借(支出)' : '贷(收入)' }} + </el-tag> + </el-descriptions-item> + <el-descriptions-item label="发生额"> + <span class="amount">{{ formatAmount(detailData.transaction && detailData.transaction.occuAmt) }}</span> + </el-descriptions-item> + <el-descriptions-item label="入账日期">{{ detailData.transaction && detailData.transaction.inaccDate || '-' }}</el-descriptions-item> + <el-descriptions-item label="对方单位名称">{{ detailData.transaction && detailData.transaction.unitName || '-' }}</el-descriptions-item> + <el-descriptions-item label="支付方式">{{ getPaymentModeName(detailData.transaction && detailData.transaction.payModeName) }}</el-descriptions-item> + <el-descriptions-item label="对方账号">{{ detailData.transaction && detailData.transaction.opsAccname || '-' }}</el-descriptions-item> + <el-descriptions-item label="凭证号">{{ detailData.transaction && detailData.transaction.txVchrNo || '-' }}</el-descriptions-item> + <el-descriptions-item label="收据编号">{{ detailData.transaction && detailData.transaction.receiptNo || '-' }}</el-descriptions-item> + <el-descriptions-item label="联系人">{{ detailData.transaction && detailData.transaction.conterName || '-' }}</el-descriptions-item> + <el-descriptions-item label="流水账户">{{ detailData.transaction && detailData.transaction.openaccBankName || '-' }}</el-descriptions-item> + <el-descriptions-item label="摘要">{{ detailData.transaction && detailData.transaction.summ || '-' }}</el-descriptions-item> + <el-descriptions-item label="备注">{{ detailData.transaction && detailData.transaction.remark || '-' }}</el-descriptions-item> + </el-descriptions> + </el-card> + </el-tab-pane> + + <!-- 匹配账单(新增的独立标签页) --> + <el-tab-pane label="匹配账单" name="matchBills"> + <el-card class="box-card"> + <div slot="header" class="clearfix"> + <span>匹配账单</span> + </div> + <el-table :data="detailData.relatedBills || []" border style="width: 100%"> + <el-table-column prop="unitName" label="对方名称" min-width="120" show-overflow-tooltip /> + <el-table-column prop="feeTypeName" label="费用类型" min-width="120" show-overflow-tooltip /> + <el-table-column label="费用周期" min-width="200"> + <template slot-scope="scope"> + {{ scope.row.billingStartDate || '-' }} 至 {{ scope.row.billingEndDate || '-' }} + </template> + </el-table-column> + <el-table-column prop="receivableAmount" label="应收金额" min-width="120" align="right"> + <template slot-scope="scope"> + {{ formatAmount(scope.row.receivableAmount) }} + </template> + </el-table-column> + </el-table> + <div v-if="!detailData.relatedBills || detailData.relatedBills.length === 0" class="empty-data"> + 暂无匹配账单 + </div> + </el-card> + </el-tab-pane> + + <!-- 附件信息 --> + <el-tab-pane label="附件信息" name="attachment"> + <el-card class="box-card"> + <div slot="header" class="clearfix"> + <span>附件信息</span> + <el-button style="float: right;" type="primary" size="small" @click="handleAddAttachment"> + <i class="el-icon-plus"></i> 添加附件 + </el-button> + </div> + <el-table :data="detailData.attachments || []" border style="width: 100%"> + <el-table-column prop="fileName" label="文件名" min-width="250" show-overflow-tooltip /> + <el-table-column prop="createUserName" label="操作人" width="150" /> + <el-table-column prop="createTime" label="操作时间" width="180" /> + <el-table-column label="操作" width="200" align="center"> + <template slot-scope="scope"> + <el-button size="mini" type="text" @click="previewFile(scope.row)">预览</el-button> + <el-button size="mini" type="text" @click="downloadFile(scope.row)">下载</el-button> + <el-button size="mini" type="text" class="delete-btn" @click="deleteFile(scope.$index, scope.row)">删除</el-button> + </template> + </el-table-column> + </el-table> + <div v-if="!detailData.attachments || detailData.attachments.length === 0" class="empty-data"> + 暂无附件 + </div> + </el-card> + + <!-- 文件上传对话框 --> + <el-dialog title="上传附件" :visible.sync="uploadDialogVisible" width="500px" append-to-body> + <el-upload + class="upload-area" + action="#" + :http-request="uploadFile" + :file-list="uploadFileList" + :before-upload="beforeUpload" + multiple + :limit="10"> + <el-button type="primary">点击上传</el-button> + <div slot="tip" class="el-upload__tip">只能上传jpg/png/pdf文件,且不超过5MB</div> + </el-upload> + <div slot="footer" class="dialog-footer"> + <el-button @click="uploadDialogVisible = false">取 消</el-button> + <el-button type="primary" @click="confirmUpload">确 定</el-button> + </div> + </el-dialog> + </el-tab-pane> + + <!-- 操作记录 --> + <el-tab-pane label="操作记录" name="logs"> + <el-card class="box-card"> + <div slot="header" class="clearfix"> + <span>操作记录</span> + </div> + <el-table :data="detailData.operationRecords || []" border style="width: 100%" v-loading="logsLoading"> + <el-table-column prop="oprPersonNm" label="操作人" width="150" /> + <el-table-column prop="oprContent" label="操作内容" min-width="250" show-overflow-tooltip /> + <el-table-column prop="createTime" label="操作时间" width="180" /> + </el-table> + <div v-if="!detailData.operationRecords || detailData.operationRecords.length === 0" class="empty-data"> + 暂无操作记录 + </div> + + + </el-card> + </el-tab-pane> + </el-tabs> + + <!-- 文件预览对话框 --> + <el-dialog title="附件预览" :visible.sync="previewDialogVisible" width="800px" append-to-body> + <div class="preview-container" v-loading="previewLoading"> + <div v-if="previewUrl && isPdf" class="preview-iframe"> + <iframe :src="previewUrl" width="100%" height="500px" frameborder="0"></iframe> + </div> + <div v-else-if="previewUrl && isImage" class="preview-image"> + <img :src="previewUrl" style="max-width: 100%;" /> + </div> + <div v-else-if="!previewLoading" class="preview-error"> + <i class="el-icon-warning"></i> + <p>预览加载失败,请尝试下载后查看</p> + <div class="preview-actions"> + <el-button type="primary" @click="downloadCurrentPreview">下载文件</el-button> + </div> + </div> + </div> + <div slot="footer" class="dialog-footer"> + <el-button @click="previewDialogVisible = false">关 闭</el-button> + </div> + </el-dialog> + </div> +</template> + +<script> +import { + getTransactionDetail, + uploadTransactionAttachment, + deleteTransactionAttachment, + getTransactionOperationLogs, + previewAttachment, + downloadAttachment, + addTransactionOperation +} from '@/api/finance' + +export default { + name: 'TransactionDetail', + props: { + transactionId: { + type: [String, Number], + required: true + } + }, + data() { + return { + loading: false, + activeTab: 'overall', + detailData: {}, + + // 附件相关 + uploadDialogVisible: false, + uploadFileList: [], + + // 预览相关 + previewDialogVisible: false, + previewLoading: false, + previewUrl: '', + previewBlob: null, + currentPreviewName: '', + isPdf: false, + isImage: false, + + // 操作记录相关 + logsLoading: false, + logsTotal: 0, + logsQuery: { + pageNum: 1, + pageSize: 10, + transactionId: undefined + } + } + }, + created() { + this.getDetail() + }, + methods: { + // 获取流水详情 + getDetail() { + this.loading = true + getTransactionDetail(this.transactionId).then(response => { + this.detailData = response.data || {} + this.loading = false + }).catch(() => { + this.loading = false + }) + }, + + // 获取操作记录 + getOperationLogs() { + this.logsLoading = true + this.logsQuery.transactionId = this.transactionId + getTransactionOperationLogs(this.logsQuery).then(response => { + if (this.detailData.operationRecords) { + this.detailData.operationRecords = response.data.data || [] + } + this.logsTotal = response.data.total + this.logsLoading = false + }).catch(() => { + this.logsLoading = false + }) + }, + + // 操作记录分页大小改变 + handleLogsSizeChange(val) { + this.logsQuery.pageSize = val + this.getOperationLogs() + }, + + // 操作记录分页页码改变 + handleLogsCurrentChange(val) { + this.logsQuery.pageNum = val + this.getOperationLogs() + }, + + // 格式化金额 + formatAmount(amount) { + if (amount === undefined || amount === null) { + return '0.00' + } + return parseFloat(amount).toFixed(2) + }, + + // 处理添加附件 + handleAddAttachment() { + this.uploadDialogVisible = true + this.uploadFileList = [] + }, + + // 上传前检查 + beforeUpload(file) { + const isValidType = ['image/jpeg', 'image/png', 'application/pdf'].includes(file.type) + const isLt5M = file.size / 1024 / 1024 < 5 + + if (!isValidType) { + this.$message.error('只能上传JPG/PNG/PDF格式的文件!') + } + + if (!isLt5M) { + this.$message.error('文件大小不能超过5MB!') + } + + return isValidType && isLt5M + }, + + // 文件上传 + uploadFile(options) { + // 先将文件加入上传列表,等确认后再统一上传 + const file = options.file + + // 创建临时URL供预览 + const tempUrl = URL.createObjectURL(file) + + // 向上传列表中添加文件 + this.uploadFileList.push({ + name: file.name, + file: file, + url: tempUrl + }) + }, + + // 确认上传 + confirmUpload() { + if (this.uploadFileList.length === 0) { + this.$message.warning('请选择要上传的文件') + return + } + + const userId = this.$store.getters.userId || '' + const userName = this.$store.getters.name || '' + + const uploadPromises = this.uploadFileList.map(fileItem => { + return uploadTransactionAttachment(this.transactionId, fileItem.file, userId, userName) + }) + + this.loading = true + Promise.all(uploadPromises) + .then(() => { + this.$message.success('附件上传成功') + this.uploadDialogVisible = false + + + // 重新获取详情以更新附件列表 + this.getDetail() + this.loading = false + }) + .catch(() => { + this.$message.error('部分附件上传失败,请重试') + this.loading = false + }) + }, + + // 预览文件 + previewFile(file) { + this.previewLoading = true + this.previewDialogVisible = true + this.currentPreviewName = file.fileName || '附件' + this.previewUrl = '' + this.isPdf = false + this.isImage = false + + previewAttachment(file.fileId).then(response => { + // 判断文件类型 + const contentType = response.headers['content-type'] + + // 创建blob对象 + const blob = new Blob([response.data], { type: contentType }) + this.previewBlob = blob + + // 判断文件类型并设置标志 + this.isPdf = contentType === 'application/pdf' + this.isImage = contentType.startsWith('image/') + + // 创建URL用于预览 + this.previewUrl = URL.createObjectURL(blob) + this.previewLoading = false + }).catch(error => { + console.error('预览失败', error) + this.$message.error('获取预览数据失败') + this.previewLoading = false + }) + }, + + // 下载当前预览的文件 + downloadCurrentPreview() { + if (!this.previewBlob) { + this.$message.error('没有可下载的文件') + return + } + + const url = URL.createObjectURL(this.previewBlob) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', this.currentPreviewName) + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + }, + + // 下载文件 + downloadFile(file) { + downloadAttachment(file.fileId, file.fileName).then(response => { + // 创建下载链接 + const blob = new Blob([response.data]) + const link = document.createElement('a') + link.href = window.URL.createObjectURL(blob) + link.download = file.fileName + link.style.display = 'none' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(link.href) + }).catch(() => { + this.$message.warning('无法下载该文件') + }) + }, + + // 删除文件 + deleteFile(index, file) { + this.$confirm('确认删除该附件?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + const userId = this.$store.getters.userId || '' + deleteTransactionAttachment(file.fileId, userId).then(() => { + this.$message.success('删除成功') + if (this.detailData.attachments) { + this.detailData.attachments.splice(index, 1) + } + // 重新获取详情以更新附件列表 + this.getDetail() + }) + }).catch(() => {}) + }, + + + + // 添加支付方式显示方法 + getPaymentModeName(code) { + const paymentModeMap = { + '1': '现金', + '2': '网银转账', + '3': 'POS机', + '4': '支付宝', + '5': '微信', + '6': '转账支票', + '7': '其他方式', + '8': '线上支付' + } + return paymentModeMap[code] || code || '-' + } + } +} +</script> + +<style lang="scss" scoped> +.transaction-detail-container { + padding: 0 20px; + + .amount { + font-weight: bold; + color: #409EFF; + + &.matched { + color: #67C23A; + } + + &.unmatched { + color: #E6A23C; + } + } + + .box-card { + margin-bottom: 20px; + } + + .upload-area { + text-align: center; + padding: 20px 0; + } + + .empty-data { + color: #909399; + text-align: center; + padding: 30px 0; + } + + .delete-btn { + color: #F56C6C; + } + + .preview-container { + min-height: 500px; + max-height: 700px; + overflow: auto; + + .preview-iframe { + width: 100%; + height: 500px; + border: none; + } + + .preview-image { + display: flex; + justify-content: center; + align-items: center; + min-height: 300px; + } + + .preview-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 300px; + color: #909399; + + i { + font-size: 48px; + margin-bottom: 20px; + } + + p { + font-size: 16px; + } + + .preview-actions { + margin-top: 20px; + } + } + } +} +</style> \ No newline at end of file diff --git a/pc/src/views/finance/transaction/index.vue b/pc/src/views/finance/transaction/index.vue new file mode 100644 index 0000000..7b19f8a --- /dev/null +++ b/pc/src/views/finance/transaction/index.vue @@ -0,0 +1,352 @@ +<template> + <div class="app-container"> + <!-- 搜索表单 --> + <el-form :model="queryParams" ref="queryForm" :inline="true" class="search-form"> + <el-form-item label="借贷标" prop="dcFlagCode"> + <el-select v-model="queryParams.dcFlagCode" placeholder="请选择借贷标" clearable style="width: 200px"> + <el-option label="借(支出)" value="0" /> + <el-option label="贷(收入)" value="1" /> + </el-select> + </el-form-item> + <el-form-item label="支付方式" prop="payModeName"> + <el-select v-model="queryParams.payModeCode" placeholder="请选择支付方式" clearable style="width: 200px"> + <el-option label="现金" value="1" /> + <el-option label="网银转账" value="2" /> + <el-option label="POS机" value="3" /> + <el-option label="支付宝" value="4" /> + <el-option label="微信" value="5" /> + <el-option label="转账支票" value="6" /> + <el-option label="其他方式" value="7" /> + <el-option label="线上支付" value="8" /> + </el-select> + </el-form-item> + <el-form-item label="流水账户" prop="accountId"> + <el-select v-model="queryParams.accountId" placeholder="请选择流水账户" clearable filterable style="width: 200px"> + <el-option v-for="item in accountOptions" :key="item.id" :label="item.accountName" :value="item.id" /> + </el-select> + </el-form-item> + <el-form-item label="创建人员" prop="createUserName"> + <el-input v-model="queryParams.createUserName" placeholder="请输入创建人员" clearable style="width: 200px" /> + </el-form-item> + <el-form-item label="入账时间" prop="inaccDate"> + <el-date-picker + v-model="accountDateRange" + type="daterange" + range-separator="至" + start-placeholder="开始日期" + end-placeholder="结束日期" + value-format="yyyy-MM-dd" + style="width: 240px"> + </el-date-picker> + </el-form-item> + <el-form-item label="创建时间" prop="createDate"> + <el-date-picker + v-model="createDateRange" + type="daterange" + range-separator="至" + start-placeholder="开始日期" + end-placeholder="结束日期" + value-format="yyyy-MM-dd" + style="width: 240px"> + </el-date-picker> + </el-form-item> + <el-form-item label="发生额" prop="occuAmt"> + <el-input-number v-model="queryParams.occuAmtMin" placeholder="最小值" :min="0" :precision="2" style="width: 110px" controls-position="right" /> + <span style="margin: 0 5px;">-</span> + <el-input-number v-model="queryParams.occuAmtMax" placeholder="最大值" :min="0" :precision="2" style="width: 110px" controls-position="right" /> + </el-form-item> + <el-form-item> + <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button> + <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button> + </el-form-item> + <el-button type="warning" icon="el-icon-download" style="float: right;" @click="handleExport">导出</el-button> + </el-form> + + <!-- 表格区域 --> + <el-table v-loading="loading" :data="transactionList" border> + <el-table-column label="项目楼宇名称" prop="projectName" min-width="120" show-overflow-tooltip> + <template slot-scope="scope"> + {{ scope.row.projectName }} - {{ scope.row.buildingName }} + </template> + </el-table-column> + <el-table-column label="房号" prop="roomNo" min-width="100" show-overflow-tooltip /> + <el-table-column label="对方账号" prop="opsAccname" min-width="150" show-overflow-tooltip /> + <el-table-column label="入账日期" prop="inaccDate" min-width="120" align="center" /> + <el-table-column label="对方名称" prop="unitName" min-width="120" show-overflow-tooltip /> + <el-table-column label="借贷标" min-width="100" align="center"> + <template slot-scope="scope"> + <el-tag :type="scope.row.dcFlagCode === '0' ? 'danger' : 'success'"> + {{ scope.row.dcFlagCode === '0' ? '借(支出)' : '贷(收入)' }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="发生额" prop="occuAmt" min-width="100" align="right"> + <template slot-scope="scope"> + {{ scope.row.occuAmt ? parseFloat(scope.row.occuAmt).toFixed(2) : '0.00' }} + </template> + </el-table-column> + <el-table-column label="币种(单位)" prop="currCode" min-width="100" align="center"> + <template slot-scope="scope"> + {{ getCurrencyName(scope.row.currCode) }} + </template> + </el-table-column> + <el-table-column label="支付方式" prop="payModeName" min-width="100" align="center"> + <template slot-scope="scope"> + {{ getPaymentModeName(scope.row.payModeName) }} + </template> + </el-table-column> + <el-table-column label="创建人" prop="createUserId" min-width="100" show-overflow-tooltip /> + <el-table-column label="状态" prop="transactionStatus" min-width="100" align="center"> + <template slot-scope="scope"> + <el-tag :type="getStatusType(scope.row.transactionStatus)"> + {{ getStatusName(scope.row.transactionStatus) }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="创建日期" prop="createTime" min-width="120" align="center" /> + <el-table-column label="操作" width="100" align="center" fixed="right"> + <template slot-scope="scope"> + <el-button size="mini" type="text" icon="el-icon-view" @click="handleViewDetail(scope.row)">查看</el-button> + </template> + </el-table-column> + </el-table> + + <div class="pagination-container"> + <!-- 分页区域 --> + <el-pagination + background + @size-change="handleSizeChange" + @current-change="handleCurrentChange" + :current-page="queryParams.pageNum" + :page-sizes="[10, 20, 30, 50]" + :page-size="queryParams.pageSize" + layout="total, sizes, prev, pager, next, jumper" + :total="total"> + </el-pagination> + </div> + + <!-- 流水详情对话框 --> + <el-dialog title="收支流水详情" :visible.sync="detailVisible" width="80%" append-to-body> + <transaction-detail v-if="detailVisible" :transactionId="currentTransaction.id" /> + </el-dialog> + </div> +</template> + +<script> +import { getTransactionList, exportTransactionList, getAllFinanceAccounts } from '@/api/finance' +import TransactionDetail from './components/TransactionDetail' + +export default { + name: 'Transaction', + components: { + TransactionDetail + }, + data() { + return { + // 加载标志 + loading: false, + // 收支流水列表数据 + transactionList: [], + // 总记录数 + total: 0, + // 入账时间范围 + accountDateRange: [], + // 创建时间范围 + createDateRange: [], + // 查询参数 + queryParams: { + pageNum: 1, + pageSize: 10, + dcFlagCode: undefined, + payModeCode: undefined, + accountId: undefined, + createUserName: undefined, + inaccDateStart: undefined, + inaccDateEnd: undefined, + createDateStart: undefined, + createDateEnd: undefined, + occuAmtMin: undefined, + occuAmtMax: undefined + }, + // 收支账户选项 + accountOptions: [], + // 详情对话框显示标志 + detailVisible: false, + // 当前查看的流水详情 + currentTransaction: {} + } + }, + created() { + this.getList() + // this.getAccountOptions() + }, + methods: { + // 获取收支流水列表 + getList() { + this.loading = true + // 处理查询参数 + const queryParams = { ...this.queryParams } + + // 处理入账时间范围 + if (this.accountDateRange && this.accountDateRange.length > 0) { + queryParams.inaccDateStart = this.accountDateRange[0] + queryParams.inaccDateEnd = this.accountDateRange[1] + } + + // 处理创建时间范围 + if (this.createDateRange && this.createDateRange.length > 0) { + queryParams.createDateStart = this.createDateRange[0] + queryParams.createDateEnd = this.createDateRange[1] + } + + // 转换支付方式编码为后端参数 + if (queryParams.payModeCode) { + queryParams.payModeName = queryParams.payModeCode + delete queryParams.payModeCode + } + + getTransactionList(queryParams).then(response => { + this.transactionList = response.data.data || [] + this.total = response.data.total + this.loading = false + }).catch(() => { + this.loading = false + }) + }, + // 获取收支账户选项 + getAccountOptions() { + getAllFinanceAccounts().then(response => { + this.accountOptions = response.data || [] + }) + }, + // 搜索按钮操作 + handleQuery() { + this.queryParams.pageNum = 1 + this.getList() + }, + // 重置按钮操作 + resetQuery() { + this.accountDateRange = [] + this.createDateRange = [] + this.$refs.queryForm.resetFields() + this.handleQuery() + }, + // 分页大小改变 + handleSizeChange(val) { + this.queryParams.pageSize = val + this.getList() + }, + // 分页页码改变 + handleCurrentChange(val) { + this.queryParams.pageNum = val + this.getList() + }, + // 查看流水详情 + handleViewDetail(row) { + this.currentTransaction = row + this.detailVisible = true + }, + // 导出收支流水 + handleExport() { + // 处理导出参数 + const exportParams = { ...this.queryParams } + delete exportParams.pageNum + delete exportParams.pageSize + + // 处理入账时间范围 + if (this.accountDateRange && this.accountDateRange.length > 0) { + exportParams.inaccDateStart = this.accountDateRange[0] + exportParams.inaccDateEnd = this.accountDateRange[1] + } + + // 处理创建时间范围 + if (this.createDateRange && this.createDateRange.length > 0) { + exportParams.createDateStart = this.createDateRange[0] + exportParams.createDateEnd = this.createDateRange[1] + } + + this.$confirm('是否确认导出所有数据项?', '警告', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + exportTransactionList(exportParams).then(response => { + // 创建下载链接 + const blob = new Blob([response.data]) + const fileName = `收支流水列表_${new Date().getTime()}.xlsx` + if (window.navigator.msSaveOrOpenBlob) { + // IE浏览器下载 + navigator.msSaveBlob(blob, fileName) + } else { + // 非IE浏览器下载 + const link = document.createElement('a') + link.href = window.URL.createObjectURL(blob) + link.download = fileName + link.style.display = 'none' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(link.href) + } + }) + }) + }, + // 获取币种名称 + getCurrencyName(currency) { + const currencyMap = { + '156': '人民币', + '840': '美元', + '978': '欧元', + '826': '英镑', + '392': '日元' + } + return currencyMap[currency] || currency || '人民币' + }, + // 获取状态类型 + getStatusType(status) { + const statusMap = { + '0': 'info', + '1': 'success' + } + return statusMap[status] || 'info' + }, + // 获取状态名称 + getStatusName(status) { + const statusMap = { + '0': '处理中', + '1': '已完成' + } + return statusMap[status] || '未知状态' + }, + // 获取支付方式名称 + getPaymentModeName(code) { + const paymentModeMap = { + '1': '现金', + '2': '网银转账', + '3': 'POS机', + '4': '支付宝', + '5': '微信', + '6': '转账支票', + '7': '其他方式', + '8': '线上支付' + } + return paymentModeMap[code] || code || '-' + } + } +} +</script> + +<style lang="scss" scoped> +.app-container { + padding: 20px; + + .search-form { + margin-bottom: 20px; + } + + .pagination-container { + margin-top: 20px; + text-align: right; + } +} +</style> \ No newline at end of file diff --git a/pc/src/views/project/index.vue b/pc/src/views/project/index.vue index cfe9034..fdb0266 100644 --- a/pc/src/views/project/index.vue +++ b/pc/src/views/project/index.vue @@ -63,7 +63,7 @@ </el-select> </el-form-item> <el-form-item label="所属地区"> - <el-input v-model="queryParams.region" placeholder="请输入所属地区" clearable size="small" /> + <region-selector v-model="queryParams.region" :value="queryParams.region" placeholder="请选择所属地区" size="small" @change="handleRegionChange" /> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> @@ -133,7 +133,7 @@ </el-col> <el-col :span="12"> <el-form-item label="所属地区" prop="region"> - <el-input v-model="form.region" placeholder="请输入所属地区" /> + <region-selector v-model="form.region" placeholder="请选择所属地区" /> </el-form-item> </el-col> </el-row> @@ -223,9 +223,13 @@ <script> import { getProjectList, getProjectDetail, addProject, updateProject, deleteProject, getAllProjectStatistics, getTagByType } from '@/api/project' +import RegionSelector from '@/components/RegionSelector' export default { name: 'ProjectList', + components: { + RegionSelector + }, data() { return { // 遮罩层 @@ -338,7 +342,9 @@ export default { handleAdd() { this.dialogTitle = '新增项目' // 完全重置表单数据 - this.form = {} + this.form = { + region: '' // 初始化region为空字符串以确保组件正确绑定 + } this.dialogVisible = true }, /** 修改按钮操作 */ @@ -474,6 +480,10 @@ export default { // 重置表单和校验状态 this.form = {} this.$refs['form'].clearValidate() + }, + handleRegionChange(regionText) { + + this.queryParams.region = regionText } } } diff --git a/pc/vue.config.js b/pc/vue.config.js index 6751f82..318111a 100644 --- a/pc/vue.config.js +++ b/pc/vue.config.js @@ -12,7 +12,7 @@ module.exports = { }, // proxy: { // '/': { - // target: 'http://192.168.137.214:8082', + // target: 'http://192.168.137.214:8080', // changeOrigin: true, // pathRewrite: { // '^/api': '/api'