From b4128f0da8ecae56af47e805cf729a0e553f97a8 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 26 五月 2026 10:39:56 +0800
Subject: [PATCH] 富边电子 1.迁移财务模块
---
src/views/financialManagement/receivable/salesOut.vue | 180
src/components/AttachmentUpload/image/index.vue | 335 +
src/views/financialManagement/payable/payment.vue | 299 +
src/api/financialManagement/voucher.js | 54
src/views/financialManagement/generalLedger/index.vue | 498 ++
src/views/financialManagement/voucher/index.vue | 1186 ++++++
src/api/financialManagement/ledger.js | 19
src/views/financialManagement/payable/input-invoice.vue | 945 +++++
src/components/Dialog/FileList.vue | 263 +
src/views/financialManagement/assets/intangibleAssets.vue | 480 ++
src/views/financialManagement/receivable/reconciliation.vue | 738 +++
src/components/Dialog/FileListDialog.vue | 1
src/views/financialManagement/voucher/detailLedger.vue | 309 +
src/views/financialManagement/voucher/generalLedger.vue | 312 +
src/api/financialManagement/accountSubject.js | 46
src/api/financialManagement/fixedAsset.js | 50
src/views/financialManagement/receivable/receipt.vue | 855 ++++
src/views/financialManagement/receivable/invoiceApply.vue | 902 ++++
src/views/financialManagement/assets/fixedAssets.vue | 482 ++
src/views/financialManagement/receivable/salesReturn.vue | 171
src/views/financialManagement/payable/paymentApply.vue | 1016 +++++
src/views/financialManagement/payable/reconciliation.vue | 766 ++++
src/api/basicData/common.js | 25
src/views/financialManagement/payable/purchaseIn.vue | 212 +
src/api/financialManagement/intangibleAsset.js | 50
src/api/basicData/storageAttachment.js | 29
src/components/AttachmentUpload/file/index.vue | 309 +
src/views/financialManagement/receivable/outputInvoice.vue | 608 +++
src/components/Dialog/FormDialog.vue | 2
src/views/financialManagement/payable/purchaseReturn.vue | 198 +
30 files changed, 11,339 insertions(+), 1 deletions(-)
diff --git a/src/api/basicData/common.js b/src/api/basicData/common.js
new file mode 100644
index 0000000..547c1e1
--- /dev/null
+++ b/src/api/basicData/common.js
@@ -0,0 +1,25 @@
+import request from '@/utils/request'
+
+// 閫氱敤涓婁紶鎺ュ彛锛屾敮鎸� FormData 鎵归噺浼犳枃浠�
+export function uploadFile(data) {
+ return request({
+ url: '/common/upload',
+ method: 'post',
+ data,
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ })
+}
+
+// 閫氱敤涓婁紶鎺ュ彛锛屾敮鎸� FormData 鎵归噺浼犳枃浠�,姘镐笉杩囨湡锛屾厧鐢�
+export function uploadPublicFile(data) {
+ return request({
+ url: '/common/public/upload',
+ method: 'post',
+ data,
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ })
+}
diff --git a/src/api/basicData/storageAttachment.js b/src/api/basicData/storageAttachment.js
new file mode 100644
index 0000000..3e241f6
--- /dev/null
+++ b/src/api/basicData/storageAttachment.js
@@ -0,0 +1,29 @@
+// 闄勪欢椤甸潰鎺ュ彛
+import request from '@/utils/request'
+
+// 闄勪欢鏌ヨ
+export function attachmentList(query) {
+ return request({
+ url: '/storageAttachment/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 闄勪欢鏂板
+export function createAttachment(data) {
+ return request({
+ url: '/storageAttachment/add',
+ method: 'post',
+ data
+ })
+}
+
+// 闄勪欢鍒犻櫎
+export function deleteAttachment(data) {
+ return request({
+ url: '/storageAttachment/delete',
+ method: 'delete',
+ data
+ })
+}
diff --git a/src/api/financialManagement/accountSubject.js b/src/api/financialManagement/accountSubject.js
new file mode 100644
index 0000000..e54de63
--- /dev/null
+++ b/src/api/financialManagement/accountSubject.js
@@ -0,0 +1,46 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鎬诲笎绉戠洰鍒楄〃
+export function listAccountSubject(query) {
+ return request({
+ url: "/accountSubject/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板鎬诲笎绉戠洰
+export function addAccountSubject(data) {
+ return request({
+ url: "/accountSubject/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼鎬诲笎绉戠洰
+export function updateAccountSubject(data) {
+ return request({
+ url: "/accountSubject/edit",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鎬诲笎绉戠洰
+export function delAccountSubject(ids) {
+ return request({
+ url: "/accountSubject/remove/" + ids,
+ method: "delete",
+ });
+}
+
+// 瀵煎嚭鎬诲笎绉戠洰
+export function exportAccountSubject(data) {
+ return request({
+ url: "/accountSubject/export",
+ method: "post",
+ data: data,
+ responseType: "blob",
+ });
+}
diff --git a/src/api/financialManagement/fixedAsset.js b/src/api/financialManagement/fixedAsset.js
new file mode 100644
index 0000000..5c28db4
--- /dev/null
+++ b/src/api/financialManagement/fixedAsset.js
@@ -0,0 +1,50 @@
+import request from "@/utils/request";
+
+// 鍥哄畾璧勪骇鍒嗛〉鏌ヨ锛坈urrent/size锛�
+export function listFixedAssetPage(params) {
+ return request({
+ url: "/financial/fixedAsset/page",
+ method: "get",
+ params,
+ });
+}
+
+// 鏂板鍥哄畾璧勪骇
+export function addFixedAsset(data) {
+ return request({
+ url: "/financial/fixedAsset/add",
+ method: "post",
+ data,
+ });
+}
+
+// 淇敼鍥哄畾璧勪骇
+export function updateFixedAsset(data) {
+ return request({
+ url: "/financial/fixedAsset/update",
+ method: "put",
+ data,
+ });
+}
+
+// 鍒犻櫎鍥哄畾璧勪骇锛堝悗绔姹� ids=1&ids=2 褰㈠紡锛�
+export function deleteFixedAsset(ids) {
+ const idList = Array.isArray(ids) ? ids : [ids];
+ const query = idList
+ .filter(id => id !== undefined && id !== null && id !== "")
+ .map(id => `ids=${encodeURIComponent(id)}`)
+ .join("&");
+ return request({
+ url: `/financial/fixedAsset/delete?${query}`,
+ method: "delete",
+ });
+}
+
+// 鎶樻棫璁℃彁锛坽} 琛ㄧず鍏ㄩ儴鍦ㄧ敤璧勪骇锛�
+export function depreciateFixedAsset(data = {}) {
+ return request({
+ url: "/financial/fixedAsset/depreciate",
+ method: "post",
+ data,
+ });
+}
diff --git a/src/api/financialManagement/intangibleAsset.js b/src/api/financialManagement/intangibleAsset.js
new file mode 100644
index 0000000..802e649
--- /dev/null
+++ b/src/api/financialManagement/intangibleAsset.js
@@ -0,0 +1,50 @@
+import request from "@/utils/request";
+
+// 鏃犲舰璧勪骇鍒嗛〉鏌ヨ锛坈urrent/size锛�
+export function listIntangibleAssetPage(params) {
+ return request({
+ url: "/financial/intangibleAsset/page",
+ method: "get",
+ params,
+ });
+}
+
+// 鏂板鏃犲舰璧勪骇
+export function addIntangibleAsset(data) {
+ return request({
+ url: "/financial/intangibleAsset/add",
+ method: "post",
+ data,
+ });
+}
+
+// 淇敼鏃犲舰璧勪骇
+export function updateIntangibleAsset(data) {
+ return request({
+ url: "/financial/intangibleAsset/update",
+ method: "put",
+ data,
+ });
+}
+
+// 鍒犻櫎鏃犲舰璧勪骇锛堝悗绔姹� ids=1&ids=2 褰㈠紡锛�
+export function deleteIntangibleAsset(ids) {
+ const idList = Array.isArray(ids) ? ids : [ids];
+ const query = idList
+ .filter(id => id !== undefined && id !== null && id !== "")
+ .map(id => `ids=${encodeURIComponent(id)}`)
+ .join("&");
+ return request({
+ url: `/financial/intangibleAsset/delete?${query}`,
+ method: "delete",
+ });
+}
+
+// 鎽婇攢璁℃彁锛坽} 琛ㄧず鍏ㄩ儴鍦ㄧ敤璧勪骇锛�
+export function amortizeIntangibleAsset(data = {}) {
+ return request({
+ url: "/financial/intangibleAsset/amortize",
+ method: "post",
+ data,
+ });
+}
diff --git a/src/api/financialManagement/ledger.js b/src/api/financialManagement/ledger.js
new file mode 100644
index 0000000..17e62fc
--- /dev/null
+++ b/src/api/financialManagement/ledger.js
@@ -0,0 +1,19 @@
+import request from "@/utils/request";
+
+// 绉戠洰鎬昏处
+export function getGeneralLedger(params) {
+ return request({
+ url: "/financial/ledger/general",
+ method: "get",
+ params,
+ });
+}
+
+// 绉戠洰鏄庣粏璐�
+export function getDetailLedger(params) {
+ return request({
+ url: "/financial/ledger/detail",
+ method: "get",
+ params,
+ });
+}
diff --git a/src/api/financialManagement/voucher.js b/src/api/financialManagement/voucher.js
new file mode 100644
index 0000000..ccb0908
--- /dev/null
+++ b/src/api/financialManagement/voucher.js
@@ -0,0 +1,54 @@
+import request from "@/utils/request";
+
+// 鍑瘉鍒嗛〉鏌ヨ锛坈urrent/size + 杩囨护鏉′欢锛�
+export function listVoucherPage(params) {
+ return request({
+ url: "/financial/voucher/page",
+ method: "get",
+ params,
+ });
+}
+
+// 鏂板鍑瘉
+export function addVoucher(data) {
+ return request({
+ url: "/financial/voucher/add",
+ method: "post",
+ data,
+ });
+}
+
+// 淇敼鍑瘉锛堜粎鏈繃璐︼級
+export function updateVoucher(data) {
+ return request({
+ url: "/financial/voucher/update",
+ method: "put",
+ data,
+ });
+}
+
+// 杩囪处
+export function postVoucher(data) {
+ return request({
+ url: "/financial/voucher/post",
+ method: "post",
+ data,
+ });
+}
+
+// 浣滃簾
+export function cancelVoucher(data) {
+ return request({
+ url: "/financial/voucher/cancel",
+ method: "post",
+ data,
+ });
+}
+
+// 璇︽儏
+export function getVoucherDetail(id) {
+ return request({
+ url: `/financial/voucher/detail/${id}`,
+ method: "get",
+ });
+}
diff --git a/src/components/AttachmentUpload/file/index.vue b/src/components/AttachmentUpload/file/index.vue
new file mode 100644
index 0000000..1e4508c
--- /dev/null
+++ b/src/components/AttachmentUpload/file/index.vue
@@ -0,0 +1,309 @@
+<script setup>
+import { UploadFilled } from '@element-plus/icons-vue'
+import { uploadFile } from '@/api/basicData/common'
+
+const props = defineProps({
+ fileList: {
+ type: Array,
+ default: () => [],
+ },
+ index: {
+ type: Number,
+ default: -1,
+ },
+ childrenKey: {
+ type: String,
+ default: 'files',
+ },
+ limit: {
+ type: Number,
+ default: 10,
+ },
+ fileSize: {
+ type: Number,
+ default: 50,
+ },
+ fileType: {
+ type: Array,
+ default: () => [],
+ },
+ buttonText: {
+ type: String,
+ default: '鍗曞嚮閫夋嫨鏂囦欢',
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ uploadFieldName: {
+ type: String,
+ default: 'files',
+ },
+})
+
+const emit = defineEmits(['update:fileList', 'change'])
+const { proxy } = getCurrentInstance()
+
+const uploadRef = ref()
+const uploadQueueTimer = ref(null)
+const uploading = ref(false)
+const queuedUidSet = ref(new Set())
+const innerList = ref([])
+
+function readListFromProps() {
+ if (props.index > -1) {
+ const row = props.fileList?.[props.index]
+ return Array.isArray(row?.[props.childrenKey]) ? row[props.childrenKey] : []
+ }
+ return Array.isArray(props.fileList) ? props.fileList : []
+}
+
+watch(
+ () => props.fileList,
+ () => {
+ innerList.value = [...readListFromProps()]
+ },
+ { deep: true, immediate: true },
+)
+
+const currentList = computed({
+ get() {
+ return innerList.value
+ },
+ set(value) {
+ const nextList = Array.isArray(value) ? value : []
+ innerList.value = nextList
+
+ if (props.index > -1) {
+ const nextModelValue = Array.isArray(props.fileList) ? [...props.fileList] : []
+ const currentRow = nextModelValue[props.index] || {}
+ nextModelValue[props.index] = {
+ ...currentRow,
+ [props.childrenKey]: nextList,
+ }
+ emit('update:fileList', nextModelValue)
+ emit('change', nextList, nextModelValue)
+ return
+ }
+
+ emit('update:fileList', nextList)
+ emit('change', nextList, nextList)
+ },
+})
+
+const displayFileList = computed(() => {
+ return currentList.value.map((item, index) => ({
+ uid: getItemUid(item, index),
+ name: getItemName(item, index),
+ url: getItemUrl(item),
+ status: 'success',
+ rawData: item,
+ }))
+})
+
+const uploadTip = computed(() => {
+ if (!props.fileType.length) return `鍗曚釜鏂囦欢涓嶈秴杩� ${props.fileSize}MB`
+ return `鏀寔 ${props.fileType.join('/')}锛屽崟涓枃浠朵笉瓒呰繃 ${props.fileSize}MB`
+})
+
+function getItemUid(item, index) {
+ if (item?.id !== undefined && item?.id !== null) return `${item.id}`
+ return `${getItemName(item, index)}-${getItemUrl(item) || index}`
+}
+
+function getItemUrl(item) {
+ if (!item) return ''
+ if (typeof item === 'string') return item
+ return item.url || item.downloadURL || item.previewURL || item.previewUrl || ''
+}
+
+function getItemName(item, index = 0) {
+ if (!item) return `file-${index + 1}`
+ if (typeof item === 'string') return `file-${index + 1}`
+ return item.name || item.originalFilename || item.fileName || item.uidFilename || `file-${index + 1}`
+}
+
+function normalizeResponseItem(item, index) {
+ if (typeof item === 'string') {
+ return {
+ name: `file-${currentList.value.length + index + 1}`,
+ url: item,
+ }
+ }
+ return Object.assign({}, item, {
+ url: item.url || item.downloadURL || item.previewURL || item.previewUrl || '',
+ name: item.name || item.originalFilename || item.fileName || item.uidFilename || `file-${currentList.value.length + index + 1}`,
+ })
+}
+
+function extractResponseArray(response) {
+ if (Array.isArray(response)) return response
+ if (Array.isArray(response?.data)) return response.data
+ if (Array.isArray(response?.data?.data)) return response.data.data
+ if (Array.isArray(response?.payload)) return response.payload
+ if (Array.isArray(response?.payload?.data)) return response.payload.data
+ if (Array.isArray(response?.rows)) return response.rows
+ if (Array.isArray(response?.result)) return response.result
+ return []
+}
+
+function validateFile(rawFile) {
+ const extension = rawFile.name.includes('.')
+ ? rawFile.name.slice(rawFile.name.lastIndexOf('.') + 1).toLowerCase()
+ : ''
+
+ if (props.fileType.length) {
+ const isValidType = props.fileType.some((type) => {
+ const normalizedType = String(type).toLowerCase()
+ return rawFile.type.toLowerCase().includes(normalizedType) || extension === normalizedType
+ })
+ if (!isValidType) {
+ proxy.$modal.msgError(`璇蜂笂浼� ${props.fileType.join('/')} 鏍煎紡鐨勬枃浠禶)
+ return false
+ }
+ }
+
+ const isWithinSize = rawFile.size / 1024 / 1024 <= props.fileSize
+ if (!isWithinSize) {
+ proxy.$modal.msgError(`鏂囦欢澶у皬涓嶈兘瓒呰繃 ${props.fileSize}MB`)
+ return false
+ }
+
+ return true
+}
+
+function scheduleUpload(uploadFiles) {
+ clearTimeout(uploadQueueTimer.value)
+ uploadQueueTimer.value = setTimeout(() => {
+ const readyFiles = uploadFiles.filter((file) => file.raw && !queuedUidSet.value.has(file.uid))
+ if (!readyFiles.length) return
+
+ const remainCount = props.limit - currentList.value.length
+ if (remainCount <= 0) {
+ proxy.$modal.msgError(`鏈�澶氫笂浼� ${props.limit} 涓枃浠禶)
+ uploadRef.value?.clearFiles()
+ return
+ }
+
+ const selectedFiles = readyFiles.slice(0, remainCount)
+ if (selectedFiles.length < readyFiles.length) {
+ proxy.$modal.msgWarning(`鏈�澶氫笂浼� ${props.limit} 涓枃浠讹紝瓒呭嚭閮ㄥ垎宸插拷鐣)
+ }
+
+ selectedFiles.forEach((file) => queuedUidSet.value.add(file.uid))
+ uploadSelectedFiles(selectedFiles)
+ }, 0)
+}
+
+async function uploadSelectedFiles(files) {
+ const validFiles = files.filter((file) => validateFile(file.raw))
+ const invalidFiles = files.filter((file) => !validFiles.includes(file))
+
+ invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+
+ if (!validFiles.length) {
+ uploadRef.value?.clearFiles()
+ return
+ }
+
+ const formData = new FormData()
+ validFiles.forEach((file) => {
+ formData.append(props.uploadFieldName, file.raw)
+ })
+
+ uploading.value = true
+ proxy.$modal.loading('鏂囦欢涓婁紶涓紝璇风◢鍊�...')
+
+ try {
+ const response = await uploadFile(formData)
+ const responseList = extractResponseArray(response).map((item, index) => normalizeResponseItem(item, index))
+
+ if (!responseList.length) {
+ proxy.$modal.msgError('涓婁紶鎺ュ彛鏈繑鍥炴暟缁勬暟鎹�')
+ return
+ }
+
+ currentList.value = [...currentList.value, ...responseList]
+ proxy.$modal.msgSuccess('涓婁紶鎴愬姛')
+ } catch (error) {
+ proxy.$modal.msgError(error?.message || '涓婁紶澶辫触')
+ } finally {
+ validFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+ invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+ uploadRef.value?.clearFiles()
+ uploading.value = false
+ proxy.$modal.closeLoading()
+ }
+}
+
+function handleChange(file, uploadFiles) {
+ if (props.disabled || uploading.value) return
+ scheduleUpload(uploadFiles)
+}
+
+function handleRemove(file) {
+ const targetUrl = file.url || getItemUrl(file.rawData)
+ const nextList = currentList.value.filter((item, index) => {
+ const itemUrl = getItemUrl(item)
+ const itemName = getItemName(item, index)
+ return !(itemUrl === targetUrl && itemName === file.name)
+ })
+ currentList.value = nextList
+}
+
+function handleExceed() {
+ proxy.$modal.msgError(`鏈�澶氫笂浼� ${props.limit} 涓枃浠禶)
+}
+
+function openFile(file) {
+ const fileUrl = file.url || getItemUrl(file.rawData)
+ if (!fileUrl) return
+ window.open(fileUrl, '_blank')
+}
+
+onBeforeUnmount(() => {
+ clearTimeout(uploadQueueTimer.value)
+})
+</script>
+
+<template>
+ <div class="attachment-upload-file">
+ <el-upload
+ ref="uploadRef"
+ drag
+ :auto-upload="false"
+ :multiple="true"
+ :show-file-list="true"
+ :file-list="displayFileList"
+ :disabled="disabled || uploading"
+ :limit="limit"
+ :on-change="handleChange"
+ :on-remove="handleRemove"
+ :on-exceed="handleExceed"
+ :on-preview="openFile"
+ >
+ <el-icon class="upload-drag-icon"><UploadFilled /></el-icon>
+ <div class="el-upload__text">
+ 灏嗘枃浠舵嫋鍒版澶勶紝鎴� <em>{{ buttonText }}</em>
+ </div>
+ <div class="upload-tip">{{ uploadTip }}</div>
+ </el-upload>
+ </div>
+</template>
+
+<style scoped lang="scss">
+.attachment-upload-file {
+ width: 100%;
+}
+
+.upload-drag-icon {
+ font-size: 40px;
+ color: var(--el-text-color-secondary);
+}
+
+.upload-tip {
+ margin-top: 8px;
+ color: var(--el-text-color-secondary);
+ font-size: 12px;
+}
+</style>
diff --git a/src/components/AttachmentUpload/image/index.vue b/src/components/AttachmentUpload/image/index.vue
new file mode 100644
index 0000000..8243f9c
--- /dev/null
+++ b/src/components/AttachmentUpload/image/index.vue
@@ -0,0 +1,335 @@
+<script setup>
+import {Plus} from '@element-plus/icons-vue'
+import {uploadFile} from '@/api/basicData/common'
+
+const props = defineProps({
+ fileList: {
+ type: Array,
+ default: () => [],
+ },
+ index: {
+ type: Number,
+ default: -1,
+ },
+ childrenKey: {
+ type: String,
+ default: 'images',
+ },
+ limit: {
+ type: Number,
+ default: 10,
+ },
+ fileSize: {
+ type: Number,
+ default: 10,
+ },
+ fileType: {
+ type: Array,
+ default: () => ['png', 'jpg', 'jpeg', 'webp'],
+ },
+ buttonText: {
+ type: String,
+ default: '涓婁紶鍥剧墖',
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ uploadFieldName: {
+ type: String,
+ default: 'files',
+ },
+})
+
+const emit = defineEmits(['update:fileList', 'change'])
+const {proxy} = getCurrentInstance()
+
+const uploadRef = ref()
+const previewVisible = ref(false)
+const previewUrl = ref('')
+const uploadQueueTimer = ref(null)
+const uploading = ref(false)
+const queuedUidSet = ref(new Set())
+
+const currentList = computed({
+ get() {
+ if (props.index > -1) {
+ const row = props.fileList?.[props.index]
+ return Array.isArray(row?.[props.childrenKey]) ? row[props.childrenKey] : []
+ }
+ return Array.isArray(props.fileList) ? props.fileList : []
+ },
+ set(value) {
+ const nextList = Array.isArray(value) ? value : []
+ if (props.index > -1) {
+ const nextModelValue = Array.isArray(props.fileList) ? [...props.fileList] : []
+ const currentRow = nextModelValue[props.index] || {}
+ nextModelValue[props.index] = {
+ ...currentRow,
+ [props.childrenKey]: nextList,
+ }
+ emit('update:fileList', nextModelValue)
+ emit('change', nextList, nextModelValue)
+ return
+ }
+ emit('update:fileList', nextList)
+ emit('change', nextList, nextList)
+ },
+})
+
+const displayFileList = computed(() => {
+ return currentList.value.map((item, index) => ({
+ uid: getItemUid(item, index),
+ name: getItemName(item, index),
+ url: getItemUrl(item),
+ status: 'success',
+ rawData: item,
+ }))
+})
+
+const uploadTip = computed(() => {
+ return `鏀寔 ${props.fileType.join('/')}锛屽崟寮犱笉瓒呰繃 ${props.fileSize}MB锛屾渶澶氫笂浼� ${props.limit} 寮犲浘鐗嘸
+})
+
+function getItemUid(item, index) {
+ if (item?.id !== undefined && item?.id !== null) return `${item.id}`
+ return `${getItemName(item, index)}-${getItemUrl(item) || index}`
+}
+
+function getItemUrl(item) {
+ if (!item) return ''
+ if (typeof item === 'string') return item
+ return item.url || item.previewURL || ''
+}
+
+function getItemName(item, index = 0) {
+ if (!item) return `image-${index + 1}`
+ if (typeof item === 'string') return `image-${index + 1}`
+ return item.name || item.fileName || item.originalFilename || `image-${index + 1}`
+}
+
+function normalizeResponseItem(item, index) {
+ if (typeof item === 'string') {
+ return {
+ name: `image-${currentList.value.length + index + 1}`,
+ url: item,
+ }
+ }
+ return Object.assign({}, item, {
+ url: item.url || item.previewURL || item.previewUrl || '',
+ name: item.name || item.originalFilename || item.fileName || `image-${currentList.value.length + index + 1}`,
+ })
+}
+
+function extractResponseArray(response) {
+ if (Array.isArray(response)) return response
+ if (Array.isArray(response?.data)) return response.data
+ if (Array.isArray(response?.data?.data)) return response.data.data
+ if (Array.isArray(response?.payload)) return response.payload
+ if (Array.isArray(response?.payload?.data)) return response.payload.data
+ if (Array.isArray(response?.rows)) return response.rows
+ if (Array.isArray(response?.result)) return response.result
+ return []
+}
+
+function validateFile(rawFile) {
+ let isValidType = false
+ const extension = rawFile.name.includes('.')
+ ? rawFile.name.slice(rawFile.name.lastIndexOf('.') + 1).toLowerCase()
+ : ''
+
+ if (props.fileType.length) {
+ isValidType = props.fileType.some((type) => {
+ const normalizedType = String(type).toLowerCase()
+ return rawFile.type.toLowerCase().includes(normalizedType) || extension === normalizedType
+ })
+ } else {
+ isValidType = rawFile.type.includes('image')
+ }
+
+ if (!isValidType) {
+ proxy.$modal.msgError(`璇蜂笂浼� ${props.fileType.join('/')} 鏍煎紡鐨勫浘鐗嘸)
+ return false
+ }
+
+ const isWithinSize = rawFile.size / 1024 / 1024 <= props.fileSize
+ if (!isWithinSize) {
+ proxy.$modal.msgError(`鍥剧墖澶у皬涓嶈兘瓒呰繃 ${props.fileSize}MB`)
+ return false
+ }
+
+ return true
+}
+
+function scheduleUpload(uploadFiles) {
+ clearTimeout(uploadQueueTimer.value)
+ uploadQueueTimer.value = setTimeout(() => {
+ const readyFiles = uploadFiles.filter((file) => file.raw && !queuedUidSet.value.has(file.uid))
+ if (!readyFiles.length) return
+
+ const remainCount = props.limit - currentList.value.length
+ if (remainCount <= 0) {
+ proxy.$modal.msgError(`鏈�澶氫笂浼� ${props.limit} 寮犲浘鐗嘸)
+ uploadRef.value?.clearFiles()
+ return
+ }
+
+ const selectedFiles = readyFiles.slice(0, remainCount)
+ if (selectedFiles.length < readyFiles.length) {
+ proxy.$modal.msgWarning(`鏈�澶氫笂浼� ${props.limit} 寮犲浘鐗囷紝瓒呭嚭閮ㄥ垎宸插拷鐣)
+ }
+
+ selectedFiles.forEach((file) => queuedUidSet.value.add(file.uid))
+ uploadSelectedFiles(selectedFiles)
+ }, 0)
+}
+
+async function uploadSelectedFiles(files) {
+ const validFiles = files.filter((file) => validateFile(file.raw))
+ const invalidFiles = files.filter((file) => !validFiles.includes(file))
+
+ invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+
+ if (!validFiles.length) {
+ uploadRef.value?.clearFiles()
+ return
+ }
+
+ const formData = new FormData()
+ validFiles.forEach((file) => {
+ formData.append(props.uploadFieldName, file.raw)
+ })
+
+ uploading.value = true
+ proxy.$modal.loading('鍥剧墖涓婁紶涓紝璇风◢鍊�...')
+
+ try {
+ const response = await uploadFile(formData)
+ const responseList = extractResponseArray(response).map((item, index) => normalizeResponseItem(item, index))
+
+ if (!responseList.length) {
+ proxy.$modal.msgError('涓婁紶鎺ュ彛鏈繑鍥炴暟缁勬暟鎹�')
+ return
+ }
+ console.log('responseList', responseList)
+
+ currentList.value = [...currentList.value, ...responseList]
+ console.log('currentList.value', currentList.value)
+ proxy.$modal.msgSuccess('涓婁紶鎴愬姛')
+ } catch (error) {
+ proxy.$modal.msgError(error?.message || '涓婁紶澶辫触')
+ } finally {
+ validFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+ invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+ uploadRef.value?.clearFiles()
+ uploading.value = false
+ proxy.$modal.closeLoading()
+ }
+}
+
+function handleChange(file, uploadFiles) {
+ if (props.disabled || uploading.value) return
+ scheduleUpload(uploadFiles)
+}
+
+function handleRemove(file) {
+ const targetUrl = file.url || getItemUrl(file.rawData)
+ const nextList = currentList.value.filter((item, index) => {
+ const itemUrl = getItemUrl(item)
+ const itemName = getItemName(item, index)
+ return !(itemUrl === targetUrl && itemName === file.name)
+ })
+ currentList.value = nextList
+}
+
+function handlePreview(file) {
+ previewUrl.value = file.url || getItemUrl(file.rawData)
+ previewVisible.value = true
+}
+
+function handleExceed() {
+ proxy.$modal.msgError(`鏈�澶氫笂浼� ${props.limit} 寮犲浘鐗嘸)
+}
+
+onBeforeUnmount(() => {
+ clearTimeout(uploadQueueTimer.value)
+})
+</script>
+
+<template>
+ <div class="attachment-upload-image">
+ <el-upload
+ ref="uploadRef"
+ :auto-upload="false"
+ :multiple="true"
+ :show-file-list="true"
+ :file-list="displayFileList"
+ list-type="picture-card"
+ accept="image/*"
+ :disabled="disabled || uploading"
+ :limit="limit"
+ :on-change="handleChange"
+ :on-remove="handleRemove"
+ :on-preview="handlePreview"
+ :on-exceed="handleExceed"
+ >
+ <div class="upload-trigger">
+ <el-icon>
+ <Plus/>
+ </el-icon>
+ <span>{{ buttonText }}</span>
+ </div>
+ </el-upload>
+
+ <div class="upload-tip">
+ {{ uploadTip }}
+ </div>
+
+ <el-dialog v-model="previewVisible" title="鍥剧墖棰勮" width="720px" append-to-body>
+ <img class="preview-image" :src="previewUrl" alt="preview"/>
+ </el-dialog>
+ </div>
+</template>
+
+<style scoped lang="scss">
+.attachment-upload-image {
+ width: 100%;
+}
+
+.upload-trigger {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ color: var(--el-text-color-secondary);
+ font-size: 12px;
+ line-height: 1.2;
+}
+
+.upload-tip {
+ margin-top: 8px;
+ color: var(--el-text-color-secondary);
+ font-size: 12px;
+}
+
+.preview-image {
+ display: block;
+ max-width: 100%;
+ margin: 0 auto;
+}
+
+:deep(.el-upload-list--picture-card) {
+ margin: 0;
+}
+
+:deep(.el-upload--picture-card) {
+ width: 132px;
+ height: 132px;
+}
+
+:deep(.el-upload-list--picture-card .el-upload-list__item) {
+ width: 132px;
+ height: 132px;
+}
+</style>
diff --git a/src/components/Dialog/FileList.vue b/src/components/Dialog/FileList.vue
new file mode 100644
index 0000000..b0e78cf
--- /dev/null
+++ b/src/components/Dialog/FileList.vue
@@ -0,0 +1,263 @@
+<template>
+ <el-dialog v-model="isShow"
+ :title="title"
+ :width="width"
+ @close="handleClose"
+ class="attachment-dialog">
+ <!-- 宸ュ叿鏍� -->
+ <div v-if="editable"
+ class="toolbar">
+ <el-button type="primary"
+ size="small"
+ @click="handleUpload">
+ 涓婁紶闄勪欢
+ </el-button>
+ </div>
+ <!-- 涓婁紶缁勪欢寮圭獥 -->
+ <el-dialog v-model="uploadDialogVisible"
+ title="涓婁紶闄勪欢"
+ width="50%"
+ @close="closeUpload">
+ <AttachmentUpload v-model:file-list="newFileList" />
+ <template #footer>
+ <el-button @click="saveUpload">淇濆瓨</el-button>
+ <el-button @click="closeUpload">鍏抽棴</el-button>
+ </template>
+ </el-dialog>
+ <!-- 鏂囦欢鍒楄〃琛ㄦ牸 -->
+ <div class="table-container">
+ <el-table :data="tableData"
+ border
+ class="attachment-table"
+ :height="tableData.length > 0 ? 'auto' : '120px'">
+ <el-table-column label="闄勪欢鍚嶇О"
+ prop="originalFilename"
+ show-overflow-tooltip />
+ <el-table-column v-if="showActions"
+ fixed="right"
+ label="鎿嶄綔"
+ :width="150"
+ align="center">
+ <template #default="scope">
+ <el-button link
+ type="primary"
+ size="small"
+ class="download-link"
+ @click="previewFile(scope.row.previewURL)">
+ 棰勮
+ </el-button>
+ <el-button link
+ type="primary"
+ size="small"
+ class="download-link"
+ @click="downloadFile(scope.row.downloadURL)">
+ 涓嬭浇
+ </el-button>
+ <el-button v-if="editable"
+ link
+ type="danger"
+ size="small"
+ @click="handleDelete(scope.row)">
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+</template>
+
+<script setup>
+import { ElMessage } from 'element-plus'
+ import { ref, computed, getCurrentInstance, onMounted, watch } from "vue";
+ import AttachmentUpload from "@/components/AttachmentUpload/file/index.vue";
+ import {
+ attachmentList,
+ deleteAttachment,
+ createAttachment,
+ } from "@/api/basicData/storageAttachment.js";
+ import filePreview from '@/components/filePreview/index.vue'
+ const filePreviewRef = ref()
+
+ const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ recordType: {
+ type: String,
+ default: "",
+ required: true,
+ },
+ recordId: {
+ type: Number,
+ default: 0,
+ required: true,
+ },
+ title: {
+ type: String,
+ default: "闄勪欢",
+ },
+ width: {
+ type: String,
+ default: "50%",
+ },
+ showActions: {
+ type: Boolean,
+ default: true,
+ },
+ editable: {
+ type: Boolean,
+ default: true,
+ },
+ });
+
+ const emit = defineEmits(["close", "download", "upload", "delete"]);
+
+ const { proxy } = getCurrentInstance();
+ const tableData = ref([]);
+ const uploadDialogVisible = ref(false);
+ const newFileList = ref([]);
+
+ const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit("update:visible", val);
+ },
+ });
+
+ const handleClose = () => {
+ isShow.value = false;
+ };
+
+ // 棰勮鏂囦欢
+ const previewFile = (url) => {
+ if (url) {
+ filePreviewRef.value.open(url)
+ } else {
+ ElMessage.warning('鏂囦欢鍦板潃鏃犳晥锛屾棤娉曢瑙�')
+ }
+ }
+
+ const handleUpload = () => {
+ uploadDialogVisible.value = true;
+ };
+
+ const saveUpload = async () => {
+ // 妫�鏌ユ槸鍚︽湁鏂颁笂浼犵殑鏂囦欢
+ if (newFileList.value.length > 0) {
+ createAttachment({
+ application: "file",
+ recordType: props.recordType,
+ recordId: props.recordId,
+ storageBlobDTOs: [...newFileList.value, ...tableData.value],
+ }).then((res) => {
+ if (res && res.code === 200) {
+ proxy?.$modal?.msgSuccess("涓婁紶鎴愬姛");
+ newFileList.value = [];
+ // 鍒锋柊鍒楄〃
+ setList();
+ }
+ }).finally(() => {
+ uploadDialogVisible.value = false;
+ })
+ }
+ }
+
+ const closeUpload = () => {
+ newFileList.value = [];
+ uploadDialogVisible.value = false;
+ };
+
+ const handleDelete = async (row, index) => {
+ deleteAttachment([row.storageAttachmentId]).then((res) => {
+ if (res && res.code === 200) {
+ proxy?.$modal?.msgSuccess("鍒犻櫎鎴愬姛");
+ setList();
+ }
+ })
+ };
+
+ const setList = () => {
+ attachmentList({
+ recordType: props.recordType,
+ recordId: props.recordId,
+ }).then(res => {
+ tableData.value = (res && res.data) || [];
+ });
+ };
+
+ const downloadFile = url => {
+ window.open(url, "_blank");
+ };
+ onMounted(() => {
+ setList();
+ });
+</script>
+
+<style scoped>
+ .attachment-dialog {
+ border-radius: 12px;
+ }
+
+ .toolbar {
+ margin-bottom: 16px;
+ text-align: right;
+ }
+
+ .table-container {
+ max-height: 40vh;
+ overflow-y: auto;
+ min-height: 120px;
+ padding-bottom: 16px;
+ box-sizing: border-box;
+ will-change: scroll-position;
+ transform: translateZ(0);
+ -webkit-overflow-scrolling: touch;
+ }
+
+ :deep(.el-table) {
+ margin-bottom: 0;
+ }
+
+ :deep(.el-table__body-wrapper) {
+ overflow-y: auto;
+ will-change: transform;
+ transform: translateZ(0);
+ }
+
+ :deep(.el-table__body tr) {
+ transition: none;
+ }
+
+ :deep(.el-dialog__footer) {
+ padding-top: 12px;
+ border-top: 1px solid #e9ecef;
+ }
+
+ .attachment-table {
+ border-radius: 8px;
+ }
+
+ :deep(.el-dialog__header) {
+ background-color: #f8f9fa;
+ border-bottom: 1px solid #e9ecef;
+ padding: 16px 20px;
+ }
+
+ :deep(.el-dialog__title) {
+ font-size: 16px;
+ font-weight: 600;
+ }
+
+ :deep(.el-dialog__body) {
+ padding: 16px 20px;
+ }
+
+ :deep(.el-table__empty-text) {
+ color: #999;
+ }
+</style>
\ No newline at end of file
diff --git a/src/components/Dialog/FileListDialog.vue b/src/components/Dialog/FileListDialog.vue
index fc82411..6fea795 100644
--- a/src/components/Dialog/FileListDialog.vue
+++ b/src/components/Dialog/FileListDialog.vue
@@ -77,6 +77,7 @@
@pagination="paginationSearch"
@change="handleChange" />
</el-dialog>
+<!-- // todo 闄勪欢棰勮鐩稿叧 -->
<filePreview v-if="showPreview"
ref="filePreviewRef" />
</template>
diff --git a/src/components/Dialog/FormDialog.vue b/src/components/Dialog/FormDialog.vue
index 8b657de..b60bfb4 100644
--- a/src/components/Dialog/FormDialog.vue
+++ b/src/components/Dialog/FormDialog.vue
@@ -55,7 +55,7 @@
})
// 璇︽儏妯″紡涓嶅睍绀衡�滅‘璁も�濇寜閽紝鍏跺畠绫诲瀷姝e父鏄剧ず
-const showConfirm = computed(() => props.operationType !== 'detail')
+const showConfirm = computed(() => props.operationType !== 'detail' && props.operationType !== 'view')
const computedTitle = computed(() => {
if (typeof props.title === 'function') {
diff --git a/src/views/financialManagement/assets/fixedAssets.vue b/src/views/financialManagement/assets/fixedAssets.vue
new file mode 100644
index 0000000..24b4cc3
--- /dev/null
+++ b/src/views/financialManagement/assets/fixedAssets.vue
@@ -0,0 +1,482 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="璧勪骇缂栧彿:">
+ <el-input v-model="filters.assetCode" placeholder="璇疯緭鍏ヨ祫浜х紪鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="璧勪骇鍚嶇О:">
+ <el-input v-model="filters.assetName" placeholder="璇疯緭鍏ヨ祫浜у悕绉�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="璧勪骇绫诲埆:">
+ <el-select v-model="filters.category" placeholder="璇烽�夋嫨绫诲埆" clearable style="width: 150px;">
+ <el-option label="鎴垮眿寤虹瓚" value="building" />
+ <el-option label="鏈哄櫒璁惧" value="machine" />
+ <el-option label="杩愯緭宸ュ叿" value="vehicle" />
+ <el-option label="鐢靛瓙璁惧" value="electronic" />
+ <el-option label="鍔炲叕瀹跺叿" value="furniture" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��:">
+ <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+ <el-option label="鍦ㄧ敤" value="in_use" />
+ <el-option label="闂茬疆" value="idle" />
+ <el-option label="鎶ュ簾" value="scrapped" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-statistic title="璧勪骇鍘熷�煎悎璁�" :value="totalOriginalValue" precision="2" prefix="楼" />
+ <el-statistic title="绱鎶樻棫鍚堣" :value="totalDepreciation" precision="2" prefix="楼" style="margin-left: 30px;" />
+ <el-statistic title="鍑�鍊煎悎璁�" :value="totalNetValue" precision="2" prefix="楼" style="margin-left: 30px;" />
+ </div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus">鏂板璧勪骇</el-button>
+ <el-button type="warning" @click="handleDepreciation" icon="Money">鎶樻棫璁℃彁</el-button>
+ <!-- <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button> -->
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ isSelection
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @selection-change="handleSelectionChange"
+ @pagination="changePage"
+ >
+ <template #originalValue="{ row }">
+ <span class="text-primary">楼{{ formatMoney(row.originalValue) }}</span>
+ </template>
+ <template #accumulatedDepreciation="{ row }">
+ <span class="text-warning">楼{{ formatMoney(row.accumulatedDepreciation) }}</span>
+ </template>
+ <template #netValue="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.netValue) }}</span>
+ </template>
+ <template #category="{ row }">
+ <el-tag>{{ getCategoryLabel(row.category) }}</el-tag>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button type="primary" link @click="edit(row)">缂栬緫</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
+ <el-form :model="form" :rules="rules" :disabled="isView" ref="formRef" label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璧勪骇缂栧彿" prop="assetCode">
+ <el-input v-model="form.assetCode" placeholder="绯荤粺鑷姩鐢熸垚" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璧勪骇鍚嶇О" prop="assetName">
+ <el-input v-model="form.assetName" placeholder="璇疯緭鍏ヨ祫浜у悕绉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璧勪骇绫诲埆" prop="category">
+ <el-select v-model="form.category" placeholder="璇烽�夋嫨璧勪骇绫诲埆" style="width: 100%;">
+ <el-option label="鎴垮眿寤虹瓚" value="building" />
+ <el-option label="鏈哄櫒璁惧" value="machine" />
+ <el-option label="杩愯緭宸ュ叿" value="vehicle" />
+ <el-option label="鐢靛瓙璁惧" value="electronic" />
+ <el-option label="鍔炲叕瀹跺叿" value="furniture" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙勬牸鍨嬪彿" prop="specification">
+ <el-input v-model="form.specification" placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璐疆鏃ユ湡" prop="purchaseDate">
+ <el-date-picker v-model="form.purchaseDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璧勪骇鍘熷��" prop="originalValue">
+ <el-input-number v-model="form.originalValue" :min="0" :precision="2" style="width: 100%;" @change="calculateNetValue" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浣跨敤骞撮檺" prop="usefulLife">
+ <el-input-number v-model="form.usefulLife" :min="1" :max="50" style="width: 100%;" />
+ <span style="margin-left: 10px;">骞�</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="娈嬪�肩巼" prop="residualRate">
+ <el-input-number v-model="form.residualRate" :min="0" :max="10" :precision="2" style="width: 100%;" />
+ <span style="margin-left: 10px;">%</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="绱鎶樻棫">
+ <el-input v-model="form.accumulatedDepreciation" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璧勪骇鍑�鍊�">
+ <el-input v-model="form.netValue" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀛樻斁鍦扮偣" prop="location">
+ <el-input v-model="form.location" placeholder="璇疯緭鍏ュ瓨鏀惧湴鐐�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浣跨敤閮ㄩ棬" prop="department">
+ <el-input v-model="form.department" placeholder="璇疯緭鍏ヤ娇鐢ㄩ儴闂�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="淇濈浜�" prop="keeper">
+ <el-input v-model="form.keeper" placeholder="璇疯緭鍏ヤ繚绠′汉" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%;">
+ <el-option label="鍦ㄧ敤" value="in_use" />
+ <el-option label="闂茬疆" value="idle" />
+ <el-option label="鎶ュ簾" value="scrapped" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button v-if="!isView" type="primary" @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import {
+ listFixedAssetPage,
+ addFixedAsset,
+ updateFixedAsset,
+ deleteFixedAsset,
+ depreciateFixedAsset,
+} from "@/api/financialManagement/fixedAsset";
+
+defineOptions({
+ name: "鍥哄畾璧勪骇",
+});
+
+const filters = reactive({
+ assetCode: "",
+ assetName: "",
+ category: "",
+ status: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "璧勪骇缂栧彿", prop: "assetCode", width: "130" },
+ { label: "璧勪骇鍚嶇О", prop: "assetName", width: "150" },
+ { label: "璧勪骇绫诲埆", prop: "category", dataType: "slot", slot: "category" },
+ { label: "瑙勬牸鍨嬪彿", prop: "specification", width: "120" },
+ { label: "璧勪骇鍘熷��", prop: "originalValue", dataType: "slot", slot: "originalValue" },
+ { label: "绱鎶樻棫", prop: "accumulatedDepreciation", dataType: "slot", slot: "accumulatedDepreciation" },
+ { label: "璧勪骇鍑�鍊�", prop: "netValue", dataType: "slot", slot: "netValue" },
+ { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "180", fixed: "right" },
+];
+
+const dataList = ref([]);
+const multipleList = ref([]);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isEdit = ref(false);
+const isView = ref(false);
+const currentId = ref(null);
+const selectedIds = computed(() =>
+ multipleList.value
+ .map(item => item?.id)
+ .filter(id => id !== undefined && id !== null && id !== "")
+);
+
+const createDefaultForm = () => ({
+ assetCode: "",
+ assetName: "",
+ category: "",
+ specification: "",
+ purchaseDate: "",
+ originalValue: 0,
+ usefulLife: 5,
+ residualRate: 5,
+ accumulatedDepreciation: 0,
+ netValue: 0,
+ location: "",
+ department: "",
+ keeper: "",
+ status: "in_use",
+ remark: "",
+});
+
+const form = reactive({
+ ...createDefaultForm(),
+});
+
+const rules = {
+ assetName: [{ required: true, message: "璇疯緭鍏ヨ祫浜у悕绉�", trigger: "blur" }],
+ category: [{ required: true, message: "璇烽�夋嫨璧勪骇绫诲埆", trigger: "change" }],
+ purchaseDate: [{ required: true, message: "璇烽�夋嫨璐疆鏃ユ湡", trigger: "change" }],
+ originalValue: [{ required: true, message: "璇疯緭鍏ヨ祫浜у師鍊�", trigger: "blur" }],
+ usefulLife: [{ required: true, message: "璇疯緭鍏ヤ娇鐢ㄥ勾闄�", trigger: "blur" }],
+};
+
+const totalOriginalValue = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.originalValue), 0);
+});
+
+const totalDepreciation = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.accumulatedDepreciation), 0);
+});
+
+const totalNetValue = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.netValue), 0);
+});
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getCategoryLabel = (category) => {
+ const map = {
+ building: "鎴垮眿寤虹瓚",
+ machine: "鏈哄櫒璁惧",
+ vehicle: "杩愯緭宸ュ叿",
+ electronic: "鐢靛瓙璁惧",
+ furniture: "鍔炲叕瀹跺叿",
+ };
+ return map[category] || category;
+};
+
+const getStatusLabel = (status) => {
+ const key = String(status || "").toLowerCase();
+ const map = { in_use: "鍦ㄧ敤", idle: "闂茬疆", repair: "缁翠慨涓�", scrapped: "鎶ュ簾" };
+ return map[key] || status;
+};
+
+const getStatusType = (status) => {
+ const key = String(status || "").toLowerCase();
+ const map = { in_use: "success", idle: "warning", repair: "warning", scrapped: "info" };
+ return map[key] || "";
+};
+
+const calculateNetValue = () => {
+ const originalValue = Number(form.originalValue || 0);
+ const accumulatedDepreciation = Number(form.accumulatedDepreciation || 0);
+ form.netValue = Number((originalValue - accumulatedDepreciation).toFixed(2));
+};
+
+// 鑱旇皟绾﹀畾锛氬垎椤靛弬鏁板浐瀹氫负 current/size锛岃繑鍥� data.records/data.total
+const getTableData = async () => {
+ try {
+ const { data } = await listFixedAssetPage({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ assetCode: filters.assetCode,
+ assetName: filters.assetName,
+ category: filters.category,
+ status: filters.status,
+ });
+ dataList.value = data?.records || [];
+ multipleList.value = [];
+ pagination.total = Number(data?.total || 0);
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+};
+
+const handleSelectionChange = (selectionList) => {
+ multipleList.value = selectionList;
+};
+
+const resetFilters = () => {
+ filters.assetCode = "";
+ filters.assetName = "";
+ filters.category = "";
+ filters.status = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+};
+
+const buildAssetCode = () => `GD${Date.now().toString().slice(-10)}`;
+
+const add = () => {
+ isEdit.value = false;
+ isView.value = false;
+ currentId.value = null;
+ dialogTitle.value = "鏂板鍥哄畾璧勪骇";
+ Object.assign(form, createDefaultForm(), {
+ assetCode: buildAssetCode(),
+ purchaseDate: new Date().toISOString().split('T')[0],
+ });
+ dialogVisible.value = true;
+};
+
+const edit = (row) => {
+ isEdit.value = true;
+ isView.value = false;
+ currentId.value = row.id;
+ dialogTitle.value = "缂栬緫鍥哄畾璧勪骇";
+ Object.assign(form, createDefaultForm(), row);
+ dialogVisible.value = true;
+};
+
+const view = (row) => {
+ edit(row);
+ isView.value = true;
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm("纭鍒犻櫎璇ュ浐瀹氳祫浜у悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(async () => {
+ // 鑱旇皟绾﹀畾锛氬垹闄ゆ帴鍙d娇鐢� ids=1&ids=2
+ await deleteFixedAsset([row.id]);
+ if (dataList.value.length === 1 && pagination.currentPage > 1) {
+ pagination.currentPage -= 1;
+ }
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ await getTableData();
+ });
+};
+
+const handleDepreciation = () => {
+ const ids = selectedIds.value;
+ const confirmText = ids.length
+ ? `纭瀵归�変腑鐨� ${ids.length} 鏉¤祫浜ц繘琛屾湰鏈堟姌鏃ц鎻愬悧锛焋
+ : "纭杩涜鏈湀鎶樻棫璁℃彁鍚楋紵";
+ ElMessageBox.confirm(confirmText, "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "info",
+ }).then(async () => {
+ await depreciateFixedAsset({ ids });
+ ElMessage.success("鎶樻棫璁℃彁瀹屾垚");
+ await getTableData();
+ });
+};
+
+const handleOut = () => {
+ ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+const submitForm = () => {
+ if (isView.value) {
+ dialogVisible.value = false;
+ return;
+ }
+ formRef.value.validate(async valid => {
+ if (valid) {
+ try {
+ calculateNetValue();
+ const payload = { ...form };
+ if (isEdit.value) {
+ payload.id = currentId.value;
+ await updateFixedAsset(payload);
+ ElMessage.success("缂栬緫鎴愬姛");
+ } else {
+ await addFixedAsset(payload);
+ ElMessage.success("鏂板鎴愬姛");
+ }
+ dialogVisible.value = false;
+ await getTableData();
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+ }
+ });
+};
+
+onMounted(() => {
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+
+ > div:first-child {
+ display: flex;
+ align-items: center;
+ }
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.text-warning {
+ color: #e6a23c;
+ font-weight: bold;
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+</style>
diff --git a/src/views/financialManagement/assets/intangibleAssets.vue b/src/views/financialManagement/assets/intangibleAssets.vue
new file mode 100644
index 0000000..4642166
--- /dev/null
+++ b/src/views/financialManagement/assets/intangibleAssets.vue
@@ -0,0 +1,480 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="璧勪骇缂栧彿:">
+ <el-input v-model="filters.assetCode" placeholder="璇疯緭鍏ヨ祫浜х紪鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="璧勪骇鍚嶇О:">
+ <el-input v-model="filters.assetName" placeholder="璇疯緭鍏ヨ祫浜у悕绉�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="璧勪骇绫诲埆:">
+ <el-select v-model="filters.category" placeholder="璇烽�夋嫨绫诲埆" clearable style="width: 150px;">
+ <el-option label="涓撳埄鏉�" value="patent" />
+ <el-option label="鍟嗘爣鏉�" value="trademark" />
+ <el-option label="钁椾綔鏉�" value="copyright" />
+ <el-option label="杞欢" value="software" />
+ <el-option label="鍦熷湴浣跨敤鏉�" value="land" />
+ <el-option label="鍏朵粬" value="other" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��:">
+ <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+ <el-option label="鍦ㄧ敤" value="in_use" />
+ <el-option label="闂茬疆" value="idle" />
+ <el-option label="宸叉憡閿�瀹屾瘯" value="amortized" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-statistic title="璧勪骇鍘熷�煎悎璁�" :value="totalOriginalValue" precision="2" prefix="楼" />
+ <el-statistic title="绱鎽婇攢鍚堣" :value="totalAmortization" precision="2" prefix="楼" style="margin-left: 30px;" />
+ <el-statistic title="鍑�鍊煎悎璁�" :value="totalNetValue" precision="2" prefix="楼" style="margin-left: 30px;" />
+ </div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus">鏂板璧勪骇</el-button>
+ <el-button type="warning" @click="handleAmortization" icon="Money">鎽婇攢璁℃彁</el-button>
+ <!-- <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button> -->
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ isSelection
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @selection-change="handleSelectionChange"
+ @pagination="changePage"
+ >
+ <template #originalValue="{ row }">
+ <span class="text-primary">楼{{ formatMoney(row.originalValue) }}</span>
+ </template>
+ <template #accumulatedAmortization="{ row }">
+ <span class="text-warning">楼{{ formatMoney(row.accumulatedAmortization) }}</span>
+ </template>
+ <template #netValue="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.netValue) }}</span>
+ </template>
+ <template #category="{ row }">
+ <el-tag>{{ getCategoryLabel(row.category) }}</el-tag>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button type="primary" link @click="edit(row)">缂栬緫</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
+ <el-form :model="form" :rules="rules" :disabled="isView" ref="formRef" label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璧勪骇缂栧彿" prop="assetCode">
+ <el-input v-model="form.assetCode" placeholder="绯荤粺鑷姩鐢熸垚" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璧勪骇鍚嶇О" prop="assetName">
+ <el-input v-model="form.assetName" placeholder="璇疯緭鍏ヨ祫浜у悕绉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璧勪骇绫诲埆" prop="category">
+ <el-select v-model="form.category" placeholder="璇烽�夋嫨璧勪骇绫诲埆" style="width: 100%;">
+ <el-option label="涓撳埄鏉�" value="patent" />
+ <el-option label="鍟嗘爣鏉�" value="trademark" />
+ <el-option label="钁椾綔鏉�" value="copyright" />
+ <el-option label="杞欢" value="software" />
+ <el-option label="鍦熷湴浣跨敤鏉�" value="land" />
+ <el-option label="鍏朵粬" value="other" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇佷功缂栧彿" prop="certificateNo">
+ <el-input v-model="form.certificateNo" placeholder="璇疯緭鍏ヨ瘉涔︾紪鍙�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙栧緱鏃ユ湡" prop="acquisitionDate">
+ <el-date-picker v-model="form.acquisitionDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璧勪骇鍘熷��" prop="originalValue">
+ <el-input-number v-model="form.originalValue" :min="0" :precision="2" style="width: 100%;" @change="calculateNetValue" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鎽婇攢骞撮檺" prop="amortizationPeriod">
+ <el-input-number v-model="form.amortizationPeriod" :min="1" :max="50" style="width: 100%;" />
+ <span style="margin-left: 10px;">骞�</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="娈嬪�肩巼" prop="residualRate">
+ <el-input-number v-model="form.residualRate" :min="0" :max="10" :precision="2" style="width: 100%;" />
+ <span style="margin-left: 10px;">%</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="绱鎽婇攢">
+ <el-input v-model="form.accumulatedAmortization" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璧勪骇鍑�鍊�">
+ <el-input v-model="form.netValue" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏈夋晥鏈熻嚦" prop="validityDate">
+ <el-date-picker v-model="form.validityDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%;">
+ <el-option label="鍦ㄧ敤" value="in_use" />
+ <el-option label="闂茬疆" value="idle" />
+ <el-option label="宸叉憡閿�瀹屾瘯" value="amortized" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="璧勪骇鎻忚堪" prop="description">
+ <el-input v-model="form.description" type="textarea" :rows="3" placeholder="璇疯緭鍏ヨ祫浜ф弿杩�" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button v-if="!isView" type="primary" @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import {
+ listIntangibleAssetPage,
+ addIntangibleAsset,
+ updateIntangibleAsset,
+ deleteIntangibleAsset,
+ amortizeIntangibleAsset,
+} from "@/api/financialManagement/intangibleAsset";
+
+defineOptions({
+ name: "鏃犲舰璧勪骇",
+});
+
+const filters = reactive({
+ assetCode: "",
+ assetName: "",
+ category: "",
+ status: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "璧勪骇缂栧彿", prop: "assetCode", width: "130" },
+ { label: "璧勪骇鍚嶇О", prop: "assetName", width: "150" },
+ { label: "璧勪骇绫诲埆", prop: "category", dataType: "slot", slot: "category" },
+ { label: "璇佷功缂栧彿", prop: "certificateNo", width: "150" },
+ { label: "璧勪骇鍘熷��", prop: "originalValue", dataType: "slot", slot: "originalValue" },
+ { label: "绱鎽婇攢", prop: "accumulatedAmortization", dataType: "slot", slot: "accumulatedAmortization" },
+ { label: "璧勪骇鍑�鍊�", prop: "netValue", dataType: "slot", slot: "netValue" },
+ { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "180", fixed: "right" },
+];
+
+const dataList = ref([]);
+const multipleList = ref([]);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isEdit = ref(false);
+const isView = ref(false);
+const currentId = ref(null);
+const selectedIds = computed(() =>
+ multipleList.value
+ .map(item => item?.id)
+ .filter(id => id !== undefined && id !== null && id !== "")
+);
+
+const createDefaultForm = () => ({
+ assetCode: "",
+ assetName: "",
+ category: "",
+ certificateNo: "",
+ acquisitionDate: "",
+ originalValue: 0,
+ amortizationPeriod: 10,
+ residualRate: 0,
+ accumulatedAmortization: 0,
+ netValue: 0,
+ validityDate: "",
+ status: "in_use",
+ description: "",
+ remark: "",
+});
+
+const form = reactive({
+ ...createDefaultForm(),
+});
+
+const rules = {
+ assetName: [{ required: true, message: "璇疯緭鍏ヨ祫浜у悕绉�", trigger: "blur" }],
+ category: [{ required: true, message: "璇烽�夋嫨璧勪骇绫诲埆", trigger: "change" }],
+ acquisitionDate: [{ required: true, message: "璇烽�夋嫨鍙栧緱鏃ユ湡", trigger: "change" }],
+ originalValue: [{ required: true, message: "璇疯緭鍏ヨ祫浜у師鍊�", trigger: "blur" }],
+ amortizationPeriod: [{ required: true, message: "璇疯緭鍏ユ憡閿�骞撮檺", trigger: "blur" }],
+};
+
+const totalOriginalValue = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.originalValue), 0);
+});
+
+const totalAmortization = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.accumulatedAmortization), 0);
+});
+
+const totalNetValue = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.netValue), 0);
+});
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getCategoryLabel = (category) => {
+ const map = {
+ patent: "涓撳埄鏉�",
+ trademark: "鍟嗘爣鏉�",
+ copyright: "钁椾綔鏉�",
+ software: "杞欢",
+ land: "鍦熷湴浣跨敤鏉�",
+ other: "鍏朵粬",
+ };
+ return map[category] || category;
+};
+
+const getStatusLabel = (status) => {
+ const key = String(status || "").toLowerCase();
+ const map = {
+ in_use: "鍦ㄧ敤",
+ idle: "闂茬疆",
+ expired: "宸插埌鏈�",
+ amortized: "宸叉憡閿�瀹屾瘯",
+ };
+ return map[key] || status;
+};
+
+const getStatusType = (status) => {
+ const key = String(status || "").toLowerCase();
+ const map = { in_use: "success", idle: "warning", expired: "warning", amortized: "info" };
+ return map[key] || "";
+};
+
+const calculateNetValue = () => {
+ const originalValue = Number(form.originalValue || 0);
+ const accumulatedAmortization = Number(form.accumulatedAmortization || 0);
+ form.netValue = Number((originalValue - accumulatedAmortization).toFixed(2));
+};
+
+// 鑱旇皟绾﹀畾锛氬垎椤靛弬鏁板浐瀹氫负 current/size锛岃繑鍥� data.records/data.total
+const getTableData = async () => {
+ try {
+ const { data } = await listIntangibleAssetPage({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ assetCode: filters.assetCode,
+ assetName: filters.assetName,
+ category: filters.category,
+ status: filters.status,
+ });
+ dataList.value = data?.records || [];
+ multipleList.value = [];
+ pagination.total = Number(data?.total || 0);
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+};
+
+const handleSelectionChange = (selectionList) => {
+ multipleList.value = selectionList;
+};
+
+const resetFilters = () => {
+ filters.assetCode = "";
+ filters.assetName = "";
+ filters.category = "";
+ filters.status = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+};
+
+const buildAssetCode = () => `WX${Date.now().toString().slice(-10)}`;
+
+const add = () => {
+ isEdit.value = false;
+ isView.value = false;
+ currentId.value = null;
+ dialogTitle.value = "鏂板鏃犲舰璧勪骇";
+ Object.assign(form, createDefaultForm(), {
+ assetCode: buildAssetCode(),
+ acquisitionDate: new Date().toISOString().split('T')[0],
+ });
+ dialogVisible.value = true;
+};
+
+const edit = (row) => {
+ isEdit.value = true;
+ isView.value = false;
+ currentId.value = row.id;
+ dialogTitle.value = "缂栬緫鏃犲舰璧勪骇";
+ Object.assign(form, createDefaultForm(), row);
+ dialogVisible.value = true;
+};
+
+const view = (row) => {
+ edit(row);
+ isView.value = true;
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm("纭鍒犻櫎璇ユ棤褰㈣祫浜у悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(async () => {
+ // 鑱旇皟绾﹀畾锛氬垹闄ゆ帴鍙d娇鐢� ids=1&ids=2
+ await deleteIntangibleAsset([row.id]);
+ if (dataList.value.length === 1 && pagination.currentPage > 1) {
+ pagination.currentPage -= 1;
+ }
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ await getTableData();
+ });
+};
+
+const handleAmortization = () => {
+ const ids = selectedIds.value;
+ const confirmText = ids.length
+ ? `纭瀵归�変腑鐨� ${ids.length} 鏉¤祫浜ц繘琛屾湰鏈堟憡閿�璁℃彁鍚楋紵`
+ : "纭杩涜鏈湀鎽婇攢璁℃彁鍚楋紵";
+ ElMessageBox.confirm(confirmText, "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "info",
+ }).then(async () => {
+ await amortizeIntangibleAsset({ ids });
+ ElMessage.success("鎽婇攢璁℃彁瀹屾垚");
+ await getTableData();
+ });
+};
+
+const handleOut = () => {
+ ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+const submitForm = () => {
+ if (isView.value) {
+ dialogVisible.value = false;
+ return;
+ }
+ formRef.value.validate(async valid => {
+ if (valid) {
+ try {
+ calculateNetValue();
+ const payload = { ...form };
+ if (isEdit.value) {
+ payload.id = currentId.value;
+ await updateIntangibleAsset(payload);
+ ElMessage.success("缂栬緫鎴愬姛");
+ } else {
+ await addIntangibleAsset(payload);
+ ElMessage.success("鏂板鎴愬姛");
+ }
+ dialogVisible.value = false;
+ await getTableData();
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+ }
+ });
+};
+
+onMounted(() => {
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+
+ > div:first-child {
+ display: flex;
+ align-items: center;
+ }
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.text-warning {
+ color: #e6a23c;
+ font-weight: bold;
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+</style>
diff --git a/src/views/financialManagement/generalLedger/index.vue b/src/views/financialManagement/generalLedger/index.vue
new file mode 100644
index 0000000..a7b1d30
--- /dev/null
+++ b/src/views/financialManagement/generalLedger/index.vue
@@ -0,0 +1,498 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters"
+ :inline="true">
+ <el-form-item label="绉戠洰缂栫爜:">
+ <el-input v-model="filters.subjectCode"
+ placeholder="璇疯緭鍏ョ鐩紪鐮�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="绉戠洰鍚嶇О:">
+ <el-input v-model="filters.subjectName"
+ placeholder="璇疯緭鍏ョ鐩悕绉�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="绉戠洰绫诲瀷:">
+ <el-select v-model="filters.subjectType"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 200px;">
+ <el-option label="璧勪骇绫�"
+ value="璧勪骇绫�" />
+ <el-option label="璐熷�虹被"
+ value="璐熷�虹被" />
+ <el-option label="鏉冪泭绫�"
+ value="鏉冪泭绫�" />
+ <el-option label="鎴愭湰绫�"
+ value="鎴愭湰绫�" />
+ <el-option label="鎹熺泭绫�"
+ value="鎹熺泭绫�" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary"
+ @click="add"
+ icon="Plus">鏂板</el-button>
+ <!-- <el-button @click="handleOut"
+ icon="Download">瀵煎嚭</el-button> -->
+ </div>
+ </div>
+ <el-table ref="tableRef"
+ v-loading="loading"
+ :data="dataList"
+ row-key="id"
+ :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+ height="calc(100vh - 280px)"
+ border
+ stripe
+ highlight-current-row
+ class="subject-table">
+ <el-table-column label="绉戠洰缂栫爜" prop="subjectCode" width="140">
+ <template #default="scope">
+ <span class="subject-code">{{ scope.row.subjectCode }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="绉戠洰鍚嶇О" prop="subjectName" min-width="180">
+ <template #default="scope">
+ <span class="subject-name" :class="{ 'is-parent': scope.row.children?.length > 0 }">
+ {{ scope.row.subjectName }}
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column label="绉戠洰绫诲瀷" prop="subjectType" width="100" align="center">
+ <template #default="scope">
+ <el-tag size="small" :type="getSubjectTypeType(scope.row.subjectType)">
+ {{ scope.row.subjectType }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="浣欓鏂瑰悜" prop="balanceDirection" width="100" align="center">
+ <template #default="scope">
+ <el-tag size="small" :type="scope.row.balanceDirection === '鍊熸柟' ? 'primary' : 'danger'">
+ {{ scope.row.balanceDirection }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" prop="status" width="80" align="center">
+ <template #default="scope">
+ <el-tag size="small" :type="scope.row.status === 0 || scope.row.status === '0' ? 'success' : 'info'">
+ {{ scope.row.status === 0 || scope.row.status === '0' ? '鍚敤' : '绂佺敤' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" prop="remark" show-overflow-tooltip min-width="150" />
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="240">
+ <template #default="scope">
+ <el-button link type="primary" icon="Plus" @click="addChild(scope.row)">鏂板</el-button>
+ <el-button link type="primary" icon="Edit" @click="edit(scope.row)">缂栬緫</el-button>
+ <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <FormDialog :title="dialogTitle"
+ v-model="dialogVisible"
+ width="600px"
+ @confirm="submitForm"
+ @cancel="dialogVisible = false">
+ <el-form :model="form"
+ :rules="rules"
+ ref="formRef"
+ label-width="100px">
+ <el-form-item label="鐖剁骇绉戠洰">
+ <el-input :model-value="parentSubjectLabel"
+ disabled />
+ </el-form-item>
+ <el-form-item label="绉戠洰缂栫爜"
+ prop="subjectCode">
+ <el-input v-model="form.subjectCode"
+ placeholder="璇疯緭鍏ョ鐩紪鐮�" />
+ </el-form-item>
+ <el-form-item label="绉戠洰鍚嶇О"
+ prop="subjectName">
+ <el-input v-model="form.subjectName"
+ placeholder="璇疯緭鍏ョ鐩悕绉�" />
+ </el-form-item>
+ <el-form-item label="绉戠洰绫诲瀷"
+ prop="subjectType">
+ <el-select v-model="form.subjectType"
+ placeholder="璇烽�夋嫨绉戠洰绫诲瀷"
+ style="width: 100%;">
+ <el-option label="璧勪骇绫�"
+ value="璧勪骇绫�" />
+ <el-option label="璐熷�虹被"
+ value="璐熷�虹被" />
+ <el-option label="鏉冪泭绫�"
+ value="鏉冪泭绫�" />
+ <el-option label="鎴愭湰绫�"
+ value="鎴愭湰绫�" />
+ <el-option label="鎹熺泭绫�"
+ value="鎹熺泭绫�" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浣欓鏂瑰悜"
+ prop="balanceDirection">
+ <el-radio-group v-model="form.balanceDirection">
+ <el-radio label="鍊熸柟">鍊熸柟</el-radio>
+ <el-radio label="璐锋柟">璐锋柟</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鐘舵��"
+ prop="status">
+ <el-radio-group v-model="form.status">
+ <el-radio :label="0">鍚敤</el-radio>
+ <el-radio :label="1">绂佺敤</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞"
+ prop="remark">
+ <el-input v-model="form.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary"
+ @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted, getCurrentInstance, nextTick } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import FormDialog from "@/components/Dialog/FormDialog.vue";
+ import {
+ listAccountSubject,
+ addAccountSubject,
+ updateAccountSubject,
+ delAccountSubject,
+ exportAccountSubject,
+ } from "@/api/financialManagement/accountSubject";
+
+ defineOptions({
+ name: "鎬诲笎绉戠洰",
+ });
+
+ const { proxy } = getCurrentInstance();
+
+ const filters = reactive({
+ subjectCode: "",
+ subjectName: "",
+ subjectType: "",
+ });
+
+ const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+ });
+
+ const columns = [
+ { label: "绉戠洰缂栫爜", prop: "subjectCode", width: "120" },
+ { label: "绉戠洰鍚嶇О", prop: "subjectName", width: "150" },
+ { label: "绉戠洰绫诲瀷", prop: "subjectType" },
+ {
+ label: "浣欓鏂瑰悜",
+ prop: "balanceDirection",
+ dataType: "tag",
+ formatData: value => {
+ if (value === "鍊熸柟") {
+ return "鍊熸柟";
+ }
+ return "璐锋柟";
+ },
+ formatType: value => {
+ if (value === "鍊熸柟") {
+ return "primary";
+ }
+ return "danger";
+ },
+ },
+ {
+ label: "鐘舵��",
+ prop: "status",
+ dataType: "tag",
+ formatData: value => {
+ if (value === 0 || value === "0") {
+ return "鍚敤";
+ }
+ return "绂佺敤";
+ },
+ formatType: value => {
+ if (value === 0 || value === "0") {
+ return "success";
+ }
+ return "info";
+ },
+ },
+
+ { label: "澶囨敞", prop: "remark", showOverflowTooltip: true },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: "220",
+ operation: [
+ {
+ name: "鏂板",
+ type: "primary",
+ clickFun: row => {
+ addChild(row);
+ },
+ },
+ {
+ name: "缂栬緫",
+ type: "primary",
+ clickFun: row => {
+ edit(row);
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ clickFun: row => {
+ handleDelete(row);
+ },
+ },
+ ],
+ },
+ ];
+
+ const dataList = ref([]);
+ const dialogVisible = ref(false);
+ const dialogTitle = ref("");
+ const parentSubjectLabel = ref("椤剁骇绉戠洰");
+ const formRef = ref(null);
+ const tableRef = ref(null);
+ const isEdit = ref(false);
+ const loading = ref(false);
+
+ const form = reactive({
+ id: undefined,
+ parentId: null,
+ subjectCode: "",
+ subjectName: "",
+ subjectType: "",
+ balanceDirection: "鍊熸柟",
+ status: 0,
+ remark: "",
+ });
+
+ const rules = {
+ subjectCode: [{ required: true, message: "璇疯緭鍏ョ鐩紪鐮�", trigger: "blur" }],
+ subjectName: [{ required: true, message: "璇疯緭鍏ョ鐩悕绉�", trigger: "blur" }],
+ subjectType: [
+ { required: true, message: "璇烽�夋嫨绉戠洰绫诲瀷", trigger: "change" },
+ ],
+ };
+
+ const getSubjectTypeType = type => {
+ const map = {
+ 璧勪骇绫�: "success",
+ 璐熷�虹被: "danger",
+ 鏉冪泭绫�: "warning",
+ 鎴愭湰绫�: "info",
+ 鎹熺泭绫�: "primary",
+ };
+ return map[type] || "";
+ };
+
+ const getTableData = () => {
+ loading.value = true;
+ const query = {
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ ...filters,
+ };
+ listAccountSubject(query).then(response => {
+ dataList.value = response.data.records || [];
+ loading.value = false;
+ }).catch(() => {
+ loading.value = false;
+ });
+ };
+
+ const resetFilters = () => {
+ filters.subjectCode = "";
+ filters.subjectName = "";
+ filters.subjectType = "";
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const changePage = obj => {
+ pagination.currentPage = obj.page;
+ pagination.pageSize = obj.limit;
+ getTableData();
+ };
+
+ const buildParentSubjectLabel = parentRow => {
+ if (!parentRow) {
+ return "椤剁骇绉戠洰";
+ }
+ const code = parentRow.subjectCode || "";
+ const name = parentRow.subjectName || "";
+ return `${code} ${name}`.trim();
+ };
+
+ const resetForm = ({ parentId = null, parentRow = null } = {}) => {
+ Object.assign(form, {
+ id: undefined,
+ parentId,
+ subjectCode: "",
+ subjectName: "",
+ subjectType: "",
+ balanceDirection: "鍊熸柟",
+ status: 0,
+ remark: "",
+ });
+ parentSubjectLabel.value = buildParentSubjectLabel(parentRow);
+ };
+
+ const add = () => {
+ isEdit.value = false;
+ dialogTitle.value = "鏂板绉戠洰";
+ resetForm({ parentId: null, parentRow: null });
+ dialogVisible.value = true;
+ };
+
+ const addChild = row => {
+ isEdit.value = false;
+ dialogTitle.value = "鏂板瀛愮鐩�";
+ resetForm({ parentId: row.id, parentRow: row });
+ form.subjectType = row.subjectType || "";
+ form.balanceDirection = row.balanceDirection || "鍊熸柟";
+ dialogVisible.value = true;
+ };
+
+ const findSubjectById = (nodes, id) => {
+ for (const item of nodes || []) {
+ if (item.id === id) {
+ return item;
+ }
+ if (item.children && item.children.length > 0) {
+ const found = findSubjectById(item.children, id);
+ if (found) {
+ return found;
+ }
+ }
+ }
+ return null;
+ };
+
+ const edit = row => {
+ isEdit.value = true;
+ dialogTitle.value = "缂栬緫绉戠洰";
+ Object.assign(form, row);
+ form.parentId = row.parentId ?? null;
+ const parentRow =
+ row.parentId === null || row.parentId === undefined
+ ? null
+ : findSubjectById(dataList.value, row.parentId);
+ parentSubjectLabel.value = parentRow
+ ? buildParentSubjectLabel(parentRow)
+ : row.parentId
+ ? `涓婄骇ID: ${row.parentId}`
+ : buildParentSubjectLabel(null);
+ dialogVisible.value = true;
+ };
+
+ const submitForm = () => {
+ formRef.value.validate(valid => {
+ if (valid) {
+ if (isEdit.value) {
+ updateAccountSubject(form).then(() => {
+ ElMessage.success("缂栬緫鎴愬姛");
+ dialogVisible.value = false;
+ getTableData();
+ });
+ } else {
+ addAccountSubject(form).then(() => {
+ ElMessage.success("鏂板鎴愬姛");
+ dialogVisible.value = false;
+ getTableData();
+ });
+ }
+ }
+ });
+ };
+
+ const handleDelete = row => {
+ const ids = row.id;
+ ElMessageBox.confirm("纭鍒犻櫎璇ョ鐩悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ return delAccountSubject(ids);
+ })
+ .then(() => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ });
+ };
+
+ const handleOut = () => {
+ proxy.download(
+ "accountSubject/export",
+ {
+ ...filters,
+ },
+ `account_subject_${new Date().getTime()}.xlsx`
+ );
+ };
+
+ onMounted(() => {
+ getTableData();
+ });
+</script>
+
+<style lang="scss" scoped>
+ .actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+ }
+
+ .subject-table {
+ border-radius: 8px;
+ overflow: hidden;
+
+ :deep(.el-table__row) {
+ transition: background-color 0.3s;
+ }
+
+ :deep(.el-table__row:hover) {
+ background-color: #f5f7fa;
+ }
+
+ .subject-code {
+ color: #606266;
+ }
+
+ .subject-name {
+ font-weight: 500;
+
+ &.is-parent {
+ color: #409eff;
+ }
+ }
+ }
+</style>
diff --git a/src/views/financialManagement/payable/input-invoice.vue b/src/views/financialManagement/payable/input-invoice.vue
new file mode 100644
index 0000000..86ebd09
--- /dev/null
+++ b/src/views/financialManagement/payable/input-invoice.vue
@@ -0,0 +1,945 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="鍙戠エ鍙风爜:">
+ <el-input v-model="filters.invoiceNumber" placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�:">
+ <el-select v-model="filters.supplierId" placeholder="璇烽�夋嫨渚涘簲鍟�" clearable filterable style="width: 200px;">
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="寮�绁ㄦ棩鏈�:">
+ <el-date-picker
+ v-model="filters.dateRange"
+ type="daterange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ style="width: 240px;"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��:">
+ <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+ <el-option label="姝e父" :value="0" />
+ <el-option label="浣滃簾" :value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus">褰曞叆鍙戠エ</el-button>
+ <el-button @click="handleExport" icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ >
+ <template #amount="{ row }">
+ <span class="text-primary">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ <template #taxAmount="{ row }">
+ <span class="text-danger">楼{{ formatMoney(row.taxAmount) }}</span>
+ </template>
+ <template #totalAmount="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.totalAmount) }}</span>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)" effect="light" round>
+ {{ getStatusLabel(row.status) }}
+ </el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button type="warning" link @click="handleCancel(row)" v-if="isNormalStatus(row.status)">浣滃簾</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog
+ :title="dialogTitle"
+ v-model="dialogVisible"
+ width="800px"
+ :operation-type="isView ? 'detail' : ''"
+ @confirm="submitForm"
+ @cancel="closeDialog"
+ >
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+ <el-row v-if="isView" :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐘舵��">
+ <el-tag :type="getStatusType(form.status)" effect="light" round>
+ {{ getStatusLabel(form.status) }}
+ </el-tag>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ鍙风爜" prop="invoiceNo">
+ <el-input v-model="form.invoiceNo" placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�" :disabled="isView" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="form.supplierId"
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ style="width: 100%;"
+ filterable
+ :disabled="isView"
+ @change="handleSupplierChange"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍏宠仈鍏ュ簱鍗�" prop="stockInRecordIds">
+ <el-input
+ :model-value="inboundBatchDisplayText"
+ placeholder="璇峰厛閫夋嫨渚涘簲鍟�"
+ readonly
+ :disabled="!form.supplierId || isView"
+ class="inbound-batch-input"
+ @click="handleInboundInputClick"
+ >
+ <template v-if="!isView" #append>
+ <el-button
+ :disabled="!form.supplierId"
+ :loading="inboundBatchLoading"
+ @click.stop="openInboundSelectDialog"
+ >
+ 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寮�绁ㄦ棩鏈�" prop="invoiceDate">
+ <el-date-picker
+ v-model="form.invoiceDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ :disabled="isView"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ绫诲瀷" prop="invoiceType">
+ <el-select
+ v-model="form.invoiceType"
+ placeholder="璇烽�夋嫨鍙戠エ绫诲瀷"
+ style="width: 100%;"
+ :disabled="isView"
+ >
+ <el-option label="澧炲�肩◣涓撶敤鍙戠エ" value="澧炲�肩◣涓撶敤鍙戠エ" />
+ <el-option label="澧炲�肩◣鏅�氬彂绁�" value="澧炲�肩◣鏅�氬彂绁�" />
+ <el-option label="鐢靛瓙鍙戠エ" value="鐢靛瓙鍙戠エ" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绋庣巼" prop="taxRate">
+ <el-select
+ v-model="form.taxRate"
+ placeholder="璇烽�夋嫨绋庣巼"
+ style="width: 100%;"
+ :disabled="isView"
+ @change="handleTaxRateChange"
+ >
+ <el-option
+ v-for="dict in tax_rate"
+ :key="dict.value"
+ :label="dict.label"
+ :value="Number(dict.value)"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="閲戦(涓嶅惈绋�)" prop="amount">
+ <el-input-number
+ v-model="form.amount"
+ :min="0"
+ :precision="2"
+ style="width: 100%;"
+ :disabled="isView"
+ placeholder="鏍规嵁鍏ュ簱鍗曞惈绋庨噾棰濊嚜鍔ㄦ崲绠楋紝鍙慨鏀�"
+ @change="calculateTaxFromExclusive"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="绋庨">
+ <el-input-number
+ v-model="form.taxAmount"
+ :min="0"
+ :precision="2"
+ :controls="false"
+ style="width: 100%;"
+ disabled
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浠风◣鍚堣">
+ <el-input-number
+ v-model="form.totalAmount"
+ :min="0"
+ :precision="2"
+ :controls="false"
+ style="width: 100%;"
+ disabled
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鍙戠エ鍐呭" prop="content">
+ <el-input v-model="form.content" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ彂绁ㄥ唴瀹�" :disabled="isView" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" :disabled="isView" />
+ </el-form-item>
+ </el-form>
+ <template v-if="!isView" #footer>
+ <el-button type="primary" :loading="submitLoading" @click="submitForm">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+
+ <el-dialog
+ v-model="inboundSelectVisible"
+ title="閫夋嫨鍏ュ簱鍗曞彿"
+ width="1100px"
+ append-to-body
+ destroy-on-close
+ :close-on-click-modal="false"
+ @closed="handleInboundDialogClosed"
+ >
+ <el-table
+ ref="inboundTableRef"
+ v-loading="inboundBatchLoading"
+ :data="inboundBatchList"
+ row-key="id"
+ border
+ stripe
+ max-height="480"
+ @selection-change="handleInboundDialogSelectionChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column prop="inboundBatches" label="鍏ュ簱鍗曞彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="supplierName" label="渚涘簲鍟�" min-width="120" show-overflow-tooltip />
+ <el-table-column prop="productName" label="浜у搧鍚嶇О" min-width="120" show-overflow-tooltip />
+ <el-table-column prop="specificationModel" label="瑙勬牸鍨嬪彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="purchaseContractNumber" label="閲囪喘璁㈠崟鍙�" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="inboundDate" label="鍏ュ簱鏃ユ湡" width="110" align="center" />
+ <el-table-column prop="inboundAmount" label="鍏ュ簱閲戦(鍚◣)" width="120" align="right">
+ <template #default="{ row }">楼{{ formatMoney(getInboundRowTaxInclusiveAmount(row)) }}</template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <el-button type="primary" @click="confirmInboundSelection">纭畾</el-button>
+ <el-button @click="inboundSelectVisible = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
+import {
+ getInboundBatchesBySupplier,
+ addAccountPurchaseInvoice,
+ listPageAccountPurchaseInvoice,
+ cancelAccountPurchaseInvoice,
+ deleteAccountPurchaseInvoice,
+} from "@/api/financialManagement/accountPurchaseInvoice.js";
+
+defineOptions({
+ name: "杩涢」鍙戠エ",
+});
+
+const { proxy } = getCurrentInstance();
+const { tax_rate } = proxy.useDict("tax_rate");
+
+const filters = reactive({
+ invoiceNumber: "",
+ supplierId: "",
+ dateRange: [],
+ status: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "鍙戠エ鍙风爜", prop: "invoiceNo", width: "140" },
+ { label: "渚涘簲鍟�", prop: "supplierName", width: "180" },
+ { label: "寮�绁ㄦ棩鏈�", prop: "invoiceDate", width: "120" },
+ { label: "閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+ { label: "绋庨", prop: "taxAmount", dataType: "slot", slot: "taxAmount" },
+ { label: "浠风◣鍚堣", prop: "totalAmount", dataType: "slot", slot: "totalAmount" },
+ { label: "鍙戠エ绫诲瀷", prop: "invoiceType", width: "130" },
+ { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status", width: "90", align: "center" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "200", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isView = ref(false);
+const submitLoading = ref(false);
+const supplierList = ref([]);
+
+const inboundBatchList = ref([]);
+const inboundBatchOptions = ref([]);
+const inboundBatchLoading = ref(false);
+const inboundSelectVisible = ref(false);
+const inboundTableRef = ref(null);
+const dialogInboundSelection = ref([]);
+
+const STATUS_LABEL_MAP = { 0: "姝e父", 1: "浣滃簾" };
+const STATUS_TYPE_MAP = { 0: "success", 1: "info" };
+
+const form = reactive({
+ invoiceNo: "",
+ supplierId: "",
+ invoiceDate: "",
+ invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+ taxRate: 13,
+ amount: 0,
+ taxAmount: 0,
+ totalAmount: 0,
+ content: "",
+ remark: "",
+ stockInRecordIds: [],
+ inboundBatches: "",
+ storageAttachmentId: undefined,
+ status: 0,
+});
+
+const rules = {
+ invoiceNo: [{ required: true, message: "璇疯緭鍏ュ彂绁ㄥ彿鐮�", trigger: "blur" }],
+ supplierId: [{ required: true, message: "璇烽�夋嫨渚涘簲鍟�", trigger: "change" }],
+ stockInRecordIds: [{ required: true, type: "array", min: 1, message: "璇烽�夋嫨鍏宠仈鍏ュ簱鍗�", trigger: "change" }],
+ invoiceDate: [{ required: true, message: "璇烽�夋嫨寮�绁ㄦ棩鏈�", trigger: "change" }],
+ invoiceType: [{ required: true, message: "璇烽�夋嫨鍙戠エ绫诲瀷", trigger: "change" }],
+ taxRate: [{ required: true, message: "璇烽�夋嫨绋庣巼", trigger: "change" }],
+ amount: [{ required: true, message: "璇疯緭鍏ラ噾棰�", trigger: "blur" }],
+};
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const normalizeStatus = (status) => {
+ if (status === undefined || status === null || status === "") return 0;
+ const num = Number(status);
+ return Number.isNaN(num) ? 0 : num;
+};
+
+const isNormalStatus = (status) => normalizeStatus(status) === 0;
+
+const getStatusLabel = (status) => STATUS_LABEL_MAP[normalizeStatus(status)] ?? "姝e父";
+
+const getStatusType = (status) => STATUS_TYPE_MAP[normalizeStatus(status)] ?? "success";
+
+const parseStockInRecordIds = (value) => {
+ if (!value) return [];
+ if (Array.isArray(value)) return value;
+ return String(value)
+ .split(/[,锛宂/)
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .map((s) => (/^\d+$/.test(s) ? Number(s) : s));
+};
+
+const formatInboundBatches = (value) => {
+ if (value === undefined || value === null || value === "") return "";
+ if (Array.isArray(value)) return value.filter(Boolean).join("銆�");
+ return String(value)
+ .split(/[,锛宂/)
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .join("銆�");
+};
+
+const isSameInboundId = (a, b) => String(a) === String(b);
+
+const getInboundRowId = (row) => row?.id ?? row?.stockInRecordId;
+
+/** 鍏ュ簱鍗曢噾棰濅负鍚◣浠� */
+const getInboundRowTaxInclusiveAmount = (row) =>
+ Number(row?.inboundAmount ?? row?.taxInclusivePrice ?? row?.totalAmount ?? 0);
+
+const normalizeInboundBatchOptions = (data) => {
+ const list = Array.isArray(data) ? data : [];
+ return list.map((item, index) => {
+ if (typeof item === "string" || typeof item === "number") {
+ const text = String(item);
+ return { label: text, value: text, inboundAmount: 0 };
+ }
+ const label =
+ item.inboundBatches ?? item.batchNo ?? item.inboundNo ?? item.label ?? `鍏ュ簱鍗�${index + 1}`;
+ const value = item.id ?? item.stockInRecordId ?? label;
+ return {
+ label: String(label),
+ value,
+ inboundAmount: getInboundRowTaxInclusiveAmount(item),
+ };
+ });
+};
+
+/** 涓嶅惈绋庨噾棰濆彉鏇达細绋庨銆佷环绋庡悎璁℃鍚戣绠� */
+const calculateTaxFromExclusive = () => {
+ form.taxAmount = Number((form.amount * form.taxRate / 100).toFixed(2));
+ form.totalAmount = Number((form.amount + form.taxAmount).toFixed(2));
+};
+
+/** 浠风◣鍚堣鍙樻洿锛氭寜绋庣巼鍙嶇畻涓嶅惈绋庨噾棰濄�佺◣棰� */
+const calculateTaxFromInclusive = (inclusiveTotal) => {
+ const total = Number(inclusiveTotal ?? form.totalAmount ?? 0);
+ if (total <= 0) {
+ form.amount = 0;
+ form.taxAmount = 0;
+ form.totalAmount = 0;
+ return;
+ }
+ const rate = Number(form.taxRate) / 100;
+ form.totalAmount = Number(total.toFixed(2));
+ form.amount = Number((form.totalAmount / (1 + rate)).toFixed(2));
+ form.taxAmount = Number((form.totalAmount - form.amount).toFixed(2));
+};
+
+const handleTaxRateChange = () => {
+ if (form.totalAmount > 0) {
+ calculateTaxFromInclusive(form.totalAmount);
+ } else {
+ calculateTaxFromExclusive();
+ }
+};
+
+/** 鏍规嵁宸查�夊叆搴撳崟姹囨�诲惈绋庨噾棰濓紝鍙嶇畻涓嶅惈绋庨噾棰濅笌绋庨 */
+const syncInvoiceAmount = () => {
+ const selected = form.stockInRecordIds || [];
+ const sumFromOptions = inboundBatchOptions.value
+ .filter((opt) => selected.some((id) => isSameInboundId(id, opt.value)))
+ .reduce((acc, opt) => acc + (Number(opt.inboundAmount) || 0), 0);
+
+ let taxInclusiveSum = sumFromOptions;
+ if (taxInclusiveSum <= 0 && selected.length) {
+ taxInclusiveSum = inboundBatchList.value
+ .filter((row) => selected.some((id) => isSameInboundId(id, getInboundRowId(row))))
+ .reduce((acc, row) => acc + getInboundRowTaxInclusiveAmount(row), 0);
+ }
+
+ calculateTaxFromInclusive(taxInclusiveSum > 0 ? Number(taxInclusiveSum.toFixed(2)) : 0);
+};
+
+const inboundBatchDisplayText = computed(() => {
+ if (form.inboundBatches) return form.inboundBatches;
+ const ids = form.stockInRecordIds || [];
+ if (!ids.length) return "";
+ const labels = inboundBatchOptions.value
+ .filter((opt) => ids.some((id) => isSameInboundId(id, opt.value)))
+ .map((opt) => opt.label);
+ if (labels.length) return labels.join("銆�");
+ return ids.join("銆�");
+});
+
+const normalizeTableRow = (row) => ({
+ ...row,
+ invoiceNo: row.invoiceNumber ?? row.invoiceNo,
+ invoiceDate: row.issueDate ?? row.invoiceDate,
+ amount: row.taxExclusivelPrice ?? row.amount,
+ taxAmount: row.taxPrice ?? row.taxAmount,
+ totalAmount: row.taxInclusivePrice ?? row.totalAmount,
+ content: row.invoiceContent ?? row.content,
+ status: normalizeStatus(row.status),
+ stockInRecordIds: row.stockInRecordIds ?? "",
+ inboundBatches: formatInboundBatches(row.inboundBatches),
+});
+
+const toFormNumber = (val) => {
+ const n = Number(val);
+ return Number.isFinite(n) ? n : 0;
+};
+
+const resolveFormAmounts = (row) => {
+ let amount = toFormNumber(row.taxExclusivelPrice ?? row.amount);
+ let taxAmount = toFormNumber(row.taxPrice ?? row.taxAmount);
+ let totalAmount = toFormNumber(row.taxInclusivePrice ?? row.totalAmount);
+ const taxRate = toFormNumber(row.taxRate) || 13;
+
+ if (totalAmount > 0 && amount === 0 && taxAmount === 0) {
+ amount = Number((totalAmount / (1 + taxRate / 100)).toFixed(2));
+ taxAmount = Number((totalAmount - amount).toFixed(2));
+ } else if (totalAmount > 0 && amount > 0 && taxAmount === 0) {
+ taxAmount = Number((totalAmount - amount).toFixed(2));
+ } else if (amount > 0 && taxAmount === 0 && totalAmount === 0) {
+ taxAmount = Number((amount * taxRate / 100).toFixed(2));
+ totalAmount = Number((amount + taxAmount).toFixed(2));
+ } else if (amount > 0 && taxAmount > 0 && totalAmount === 0) {
+ totalAmount = Number((amount + taxAmount).toFixed(2));
+ }
+
+ return { amount, taxAmount, totalAmount };
+};
+
+const fillFormFromRow = (row) => {
+ const stockInRecordIds = parseStockInRecordIds(row.stockInRecordIds);
+ const { amount, taxAmount, totalAmount } = resolveFormAmounts(row);
+ Object.assign(form, {
+ invoiceNo: row.invoiceNo ?? row.invoiceNumber ?? "",
+ supplierId: row.supplierId,
+ invoiceDate: row.invoiceDate ?? row.issueDate ?? "",
+ invoiceType: row.invoiceType ?? "澧炲�肩◣涓撶敤鍙戠エ",
+ taxRate: row.taxRate ?? 13,
+ amount,
+ taxAmount,
+ totalAmount,
+ content: row.content ?? row.invoiceContent ?? "",
+ remark: row.remark ?? "",
+ stockInRecordIds,
+ inboundBatches: formatInboundBatches(row.inboundBatches),
+ storageAttachmentId: row.storageAttachmentId,
+ status: normalizeStatus(row.status),
+ });
+};
+
+const buildCancelPayload = (row) => ({
+ id: row.id,
+ invoiceNumber: row.invoiceNumber ?? row.invoiceNo,
+ taxRate: row.taxRate,
+ invoiceType: row.invoiceType,
+ issueDate: row.issueDate ?? row.invoiceDate,
+ taxExclusivelPrice: row.taxExclusivelPrice ?? row.amount,
+ taxPrice: row.taxPrice ?? row.taxAmount,
+ taxInclusivePrice: row.taxInclusivePrice ?? row.totalAmount,
+ remark: row.remark ?? "",
+ invoiceContent: row.invoiceContent ?? row.content,
+ supplierId: row.supplierId,
+ storageAttachmentId: row.storageAttachmentId,
+ stockInRecordIds: row.stockInRecordIds ?? "",
+ status: 1,
+});
+
+const buildSubmitPayload = () => ({
+ invoiceNumber: form.invoiceNo,
+ supplierId: form.supplierId,
+ issueDate: form.invoiceDate,
+ invoiceType: form.invoiceType,
+ taxRate: form.taxRate,
+ taxExclusivelPrice: form.amount,
+ taxPrice: form.taxAmount,
+ taxInclusivePrice: form.totalAmount,
+ invoiceContent: form.content,
+ remark: form.remark || "",
+ stockInRecordIds: (form.stockInRecordIds || []).join(","),
+ status: 0,
+ storageAttachmentId: form.storageAttachmentId,
+});
+
+const getSupplierList = () => {
+ getOptions().then((res) => {
+ if (res.code === 200) {
+ supplierList.value = res.data ?? [];
+ }
+ });
+};
+
+const appendFilterParams = (params) => {
+ if (filters.invoiceNumber) {
+ params.invoiceNumber = filters.invoiceNumber;
+ }
+ if (filters.supplierId) {
+ params.supplierId = filters.supplierId;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ if (filters.status !== "" && filters.status != null) {
+ params.status = filters.status;
+ }
+ return params;
+};
+
+const buildListParams = () =>
+ appendFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+const buildExportParams = () => appendFilterParams({});
+
+const handleExport = () => {
+ proxy.download(
+ "/accountPurchaseInvoice/exportAccountPurchaseInvoice",
+ buildExportParams(),
+ `杩涢」鍙戠エ_${Date.now()}.xlsx`
+ );
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountPurchaseInvoice(buildListParams())
+ .then((res) => {
+ if (res.code === 200) {
+ const records = res.data?.records ?? [];
+ dataList.value = records.map(normalizeTableRow);
+ pagination.total = res.data?.total ?? 0;
+ } else {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const resetFilters = () => {
+ filters.invoiceNumber = "";
+ filters.supplierId = "";
+ filters.dateRange = [];
+ filters.status = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+};
+
+const closeDialog = () => {
+ dialogVisible.value = false;
+ isView.value = false;
+ inboundSelectVisible.value = false;
+};
+
+const resetForm = () => {
+ Object.assign(form, {
+ invoiceNo: "",
+ supplierId: "",
+ invoiceDate: new Date().toISOString().split("T")[0],
+ invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+ taxRate: 13,
+ amount: 0,
+ taxAmount: 0,
+ totalAmount: 0,
+ content: "",
+ remark: "",
+ stockInRecordIds: [],
+ inboundBatches: "",
+ storageAttachmentId: undefined,
+ status: 0,
+ });
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+};
+
+const add = () => {
+ isView.value = false;
+ dialogTitle.value = "褰曞叆鍙戠エ";
+ resetForm();
+ dialogVisible.value = true;
+};
+
+const view = (row) => {
+ isView.value = true;
+ dialogTitle.value = "鏌ョ湅鍙戠エ";
+ fillFormFromRow(row);
+ if (row.supplierId) {
+ loadInboundBatches(row.supplierId, true, false);
+ }
+ dialogVisible.value = true;
+};
+
+const handleCancel = (row) => {
+ ElMessageBox.confirm(`纭浣滃簾鍙戠エ銆�${row.invoiceNo ?? row.invoiceNumber}銆嶅悧锛焋, "浣滃簾纭", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ cancelAccountPurchaseInvoice(buildCancelPayload(row))
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("浣滃簾鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "浣滃簾澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("浣滃簾澶辫触");
+ });
+ });
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎鍙戠エ銆�${row.invoiceNo ?? row.invoiceNumber}銆嶅悧锛焋, "鍒犻櫎纭", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountPurchaseInvoice([row.id])
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+};
+
+const submitForm = () => {
+ formRef.value?.validate((valid) => {
+ if (!valid) return;
+ submitLoading.value = true;
+ addAccountPurchaseInvoice(buildSubmitPayload())
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("褰曞叆鎴愬姛");
+ closeDialog();
+ pagination.currentPage = 1;
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "褰曞叆澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("褰曞叆澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+ });
+};
+
+const ensureInboundOptionsForSelected = () => {
+ const ids = form.stockInRecordIds || [];
+ ids.forEach((id) => {
+ const exists = inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, id));
+ if (exists) return;
+ const fromList = inboundBatchList.value.find((row) => isSameInboundId(getInboundRowId(row), id));
+ if (fromList) {
+ const [option] = normalizeInboundBatchOptions([fromList]);
+ if (option) inboundBatchOptions.value.push(option);
+ return;
+ }
+ inboundBatchOptions.value.push({
+ label: String(id),
+ value: id,
+ inboundAmount: 0,
+ });
+ });
+};
+
+const restoreInboundTableSelection = () => {
+ nextTick(() => {
+ const table = inboundTableRef.value;
+ if (!table) return;
+ table.clearSelection();
+ const selectedIds = new Set((form.stockInRecordIds || []).map((id) => String(id)));
+ inboundBatchList.value.forEach((row) => {
+ const rowId = getInboundRowId(row);
+ if (rowId !== undefined && rowId !== null && selectedIds.has(String(rowId))) {
+ table.toggleRowSelection(row, true);
+ }
+ });
+ });
+};
+
+const loadInboundBatches = (supplierId, keepSelected = false, syncAmount = true) => {
+ if (!supplierId) {
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+ if (!keepSelected) {
+ form.stockInRecordIds = [];
+ form.inboundBatches = "";
+ form.amount = 0;
+ form.taxAmount = 0;
+ form.totalAmount = 0;
+ }
+ return Promise.resolve();
+ }
+ inboundBatchLoading.value = true;
+ return getInboundBatchesBySupplier({ supplierId })
+ .then((res) => {
+ if (res.code === 200) {
+ const list = res.data?.records ?? res.data ?? [];
+ inboundBatchList.value = Array.isArray(list) ? list : [];
+ inboundBatchOptions.value = normalizeInboundBatchOptions(list);
+ } else {
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+ }
+ })
+ .catch(() => {
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+ })
+ .finally(() => {
+ inboundBatchLoading.value = false;
+ if (keepSelected) {
+ ensureInboundOptionsForSelected();
+ restoreInboundTableSelection();
+ if (syncAmount && !isView.value) {
+ syncInvoiceAmount();
+ }
+ }
+ });
+};
+
+const handleSupplierChange = (supplierId) => {
+ form.stockInRecordIds = [];
+ form.inboundBatches = "";
+ form.amount = 0;
+ form.taxAmount = 0;
+ form.totalAmount = 0;
+ loadInboundBatches(supplierId);
+};
+
+const handleInboundInputClick = () => {
+ if (isView.value) return;
+ openInboundSelectDialog();
+};
+
+const openInboundSelectDialog = () => {
+ if (!form.supplierId || isView.value) return;
+ inboundSelectVisible.value = true;
+ loadInboundBatches(form.supplierId, true).then(() => {
+ restoreInboundTableSelection();
+ });
+};
+
+const handleInboundDialogSelectionChange = (selection) => {
+ dialogInboundSelection.value = selection;
+};
+
+const confirmInboundSelection = () => {
+ if (dialogInboundSelection.value.length === 0) {
+ ElMessage.warning("璇疯嚦灏戦�夋嫨涓�鏉″叆搴撳崟");
+ return;
+ }
+ form.stockInRecordIds = dialogInboundSelection.value
+ .map((row) => getInboundRowId(row))
+ .filter((id) => id !== undefined && id !== null);
+ form.inboundBatches = dialogInboundSelection.value
+ .map((row) => row.inboundBatches ?? row.batchNo ?? "")
+ .filter(Boolean)
+ .join("銆�");
+ dialogInboundSelection.value.forEach((row) => {
+ const [option] = normalizeInboundBatchOptions([row]);
+ if (option && !inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, option.value))) {
+ inboundBatchOptions.value.push(option);
+ }
+ });
+ inboundSelectVisible.value = false;
+ syncInvoiceAmount();
+ formRef.value?.validateField("stockInRecordIds");
+};
+
+const handleInboundDialogClosed = () => {
+ dialogInboundSelection.value = [];
+};
+
+onMounted(() => {
+ getSupplierList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+
+.inbound-batch-input :deep(.el-input__wrapper) {
+ cursor: pointer;
+}
+</style>
diff --git a/src/views/financialManagement/payable/payment.vue b/src/views/financialManagement/payable/payment.vue
new file mode 100644
index 0000000..18e7941
--- /dev/null
+++ b/src/views/financialManagement/payable/payment.vue
@@ -0,0 +1,299 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters"
+ :inline="true">
+ <el-form-item label="浠樻鍗曞彿:">
+ <el-input v-model="filters.paymentNumber"
+ placeholder="璇疯緭鍏ヤ粯娆惧崟鍙�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�:">
+ <el-select v-model="filters.supplierId"
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ clearable
+ filterable
+ style="width: 200px;">
+ <el-option v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠樻鏂瑰紡:">
+ <el-select v-model="filters.paymentMethod"
+ placeholder="璇烽�夋嫨浠樻鏂瑰紡"
+ clearable
+ style="width: 150px;">
+ <el-option v-for="item in checkout_payment"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠樻鏃ユ湡:">
+ <el-date-picker v-model="filters.dateRange"
+ type="daterange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ style="width: 240px;" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-statistic title="鏈〉浠樻鍚堣"
+ :value="totalPaymentAmount"
+ :precision="2"
+ prefix="楼" />
+ </div>
+ <div>
+ <el-button @click="handleExport"
+ icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage">
+ <template #amount="{ row }">
+ <span class="text-danger">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ <template #paymentMethod="{ row }">
+ <el-tag>{{ getPaymentMethodLabel(row.paymentMethod) }}</el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button :disabled="row.accountStatemen"
+ type="danger"
+ link
+ @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, computed, onMounted, getCurrentInstance } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
+ import {
+ listPageAccountPurchasePayment,
+ deleteAccountPurchasePayment,
+ } from "@/api/financialManagement/accountPurchasePayment.js";
+
+ defineOptions({
+ name: "浠樻鍗�",
+ });
+
+ const { proxy } = getCurrentInstance();
+ const { checkout_payment } = proxy.useDict("checkout_payment");
+
+ const filters = reactive({
+ paymentNumber: "",
+ supplierId: "",
+ paymentMethod: "",
+ dateRange: [],
+ });
+
+ const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+ });
+
+ const columns = [
+ { label: "浠樻鍗曞彿", prop: "paymentNumber", width: "150" },
+ { label: "鍏宠仈鐢宠鍗�", prop: "invoiceApplicationNo", width: "150" },
+ { label: "渚涘簲鍟�", prop: "supplierName", width: "180" },
+ { label: "浠樻鏃ユ湡", prop: "paymentDate", width: "120" },
+ { label: "浠樻閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+ {
+ label: "浠樻鏂瑰紡",
+ prop: "paymentMethod",
+ dataType: "slot",
+ slot: "paymentMethod",
+ width: "120",
+ },
+ { label: "澶囨敞", prop: "remark", showOverflowTooltip: true },
+ {
+ label: "鎿嶄綔",
+ prop: "operation",
+ dataType: "slot",
+ slot: "operation",
+ width: "80",
+ fixed: "right",
+ },
+ ];
+
+ const dataList = ref([]);
+ const tableLoading = ref(false);
+ const supplierList = ref([]);
+
+ const totalPaymentAmount = computed(() =>
+ dataList.value.reduce((sum, item) => sum + Number(item.amount ?? 0), 0)
+ );
+
+ const formatMoney = value => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value)
+ .toFixed(2)
+ .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+ };
+
+ const getPaymentMethodLabel = value => {
+ if (value === undefined || value === null || value === "") return "-";
+ const item = checkout_payment.value?.find(
+ m => String(m.value) === String(value)
+ );
+ return item?.label ?? value;
+ };
+
+ const normalizeTableRow = row => ({
+ ...row,
+ paymentNumber: row.paymentNumber ?? row.paymentCode,
+ invoiceApplicationNo: row.invoiceApplicationNo ?? row.applyCode ?? "",
+ amount: row.paymentAmount ?? row.amount,
+ bankAccountNum: row.bankAccountNum ?? row.bankAccount ?? "",
+ bankAccountName: row.bankAccountName ?? row.bankName ?? "",
+ });
+
+ const getSupplierList = () => {
+ getOptions().then(res => {
+ if (res.code === 200) {
+ supplierList.value = res.data ?? [];
+ }
+ });
+ };
+
+ const appendFilterParams = params => {
+ if (filters.paymentNumber) {
+ params.paymentNumber = filters.paymentNumber;
+ }
+ if (filters.supplierId) {
+ params.supplierId = filters.supplierId;
+ }
+ if (filters.paymentMethod) {
+ params.paymentMethod = filters.paymentMethod;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+ };
+
+ const buildListParams = () =>
+ appendFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+ const buildExportParams = () => appendFilterParams({});
+
+ const handleExport = () => {
+ proxy.download(
+ "/accountPurchasePayment/exportAccountPurchasePayment",
+ buildExportParams(),
+ `浠樻鍗昣${Date.now()}.xlsx`
+ );
+ };
+
+ const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountPurchasePayment(buildListParams())
+ .then(res => {
+ if (res.code === 200) {
+ dataList.value = (res.data?.records ?? []).map(normalizeTableRow);
+ pagination.total = res.data?.total ?? 0;
+ } else {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const resetFilters = () => {
+ filters.paymentNumber = "";
+ filters.supplierId = "";
+ filters.paymentMethod = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+ };
+
+ const handleDelete = row => {
+ ElMessageBox.confirm(`纭鍒犻櫎浠樻鍗曘��${row.paymentNumber}銆嶅悧锛焋, "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountPurchasePayment([row.id])
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+ };
+
+ onMounted(() => {
+ getSupplierList();
+ getTableData();
+ });
+</script>
+
+<style lang="scss" scoped>
+ .actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ }
+
+ .text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+ }
+</style>
diff --git a/src/views/financialManagement/payable/paymentApply.vue b/src/views/financialManagement/payable/paymentApply.vue
new file mode 100644
index 0000000..3937e96
--- /dev/null
+++ b/src/views/financialManagement/payable/paymentApply.vue
@@ -0,0 +1,1016 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="鐢宠鍗曞彿:">
+ <el-input v-model="filters.invoiceApplicationNo" placeholder="璇疯緭鍏ョ敵璇峰崟鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�:">
+ <el-select v-model="filters.supplierId" placeholder="璇烽�夋嫨渚涘簲鍟�" clearable filterable style="width: 200px;">
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹℃牳鐘舵��:">
+ <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+ <el-option label="寰呭鏍�" :value="0" />
+ <el-option label="瀹℃牳閫氳繃" :value="1" />
+ <el-option label="瀹℃牳涓嶉�氳繃" :value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐢宠鏃ユ湡:">
+ <el-date-picker
+ v-model="filters.dateRange"
+ type="daterange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ style="width: 240px;"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus">鏂板鐢宠</el-button>
+ <el-button @click="handleExport" icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ >
+ <template #amount="{ row }">
+ <span class="text-danger">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ <template #paymentMethod="{ row }">
+ <el-tag>{{ getPaymentMethodLabel(row.paymentMethod) }}</el-tag>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button type="primary" link @click="edit(row)" v-if="isPendingStatus(row.status)">缂栬緫</el-button>
+ <el-button type="success" link @click="handleAudit(row)" v-if="isPendingStatus(row.status)">瀹℃牳</el-button>
+ <el-button type="warning" link @click="openPaymentDialog(row)" v-if="isApprovedStatus(row.status)">浠樻</el-button>
+ <el-button type="danger" link @click="handleDelete(row)" v-if="isPendingStatus(row.status)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog
+ :title="dialogTitle"
+ v-model="dialogVisible"
+ width="800px"
+ :operation-type="isView ? 'detail' : ''"
+ @confirm="submitForm"
+ @cancel="closeDialog"
+ >
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+ <el-row v-if="isView" :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀹℃牳鐘舵��">
+ <el-tag :type="getStatusType(form.status)">{{ getStatusLabel(form.status) }}</el-tag>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐢宠鍗曞彿" prop="invoiceApplicationNo">
+ <el-input v-model="form.invoiceApplicationNo" placeholder="绯荤粺鑷姩鐢熸垚" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="form.supplierId"
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ style="width: 100%;"
+ filterable
+ :disabled="isEdit || isView"
+ @change="handleSupplierChange"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍏宠仈鍏ュ簱鍗�" prop="stockInRecordIds">
+ <el-input
+ :model-value="inboundBatchDisplayText"
+ placeholder="璇峰厛閫夋嫨渚涘簲鍟�"
+ readonly
+ :disabled="!form.supplierId || isEdit || isView"
+ class="inbound-batch-input"
+ @click="handleInboundInputClick"
+ >
+ <template v-if="!isEdit && !isView" #append>
+ <el-button
+ :disabled="!form.supplierId"
+ :loading="inboundBatchLoading"
+ @click.stop="openInboundSelectDialog"
+ >
+ 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢宠鏃ユ湡" prop="applyDate">
+ <el-date-picker
+ v-model="form.applyDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ :disabled="isView"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浠樻閲戦" prop="paymentAmount">
+ <el-input-number
+ v-model="form.paymentAmount"
+ :min="0"
+ :precision="2"
+ style="width: 100%;"
+ :disabled="isView"
+ placeholder="鏍规嵁鍏ュ簱鍗曡嚜鍔ㄦ眹鎬伙紝鍙慨鏀�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠樻鏂瑰紡" prop="paymentMethod">
+ <el-select
+ v-model="form.paymentMethod"
+ placeholder="璇烽�夋嫨浠樻鏂瑰紡"
+ style="width: 100%;"
+ :disabled="isView"
+ >
+ <el-option
+ v-for="item in checkout_payment"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="浠樻浜嬬敱" prop="paymentContent">
+ <el-input
+ v-model="form.paymentContent"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ヤ粯娆句簨鐢�"
+ :disabled="isView"
+ />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" :disabled="isView" />
+ </el-form-item>
+ </el-form>
+ <template v-if="!isView" #footer>
+ <el-button type="primary" :loading="submitLoading" @click="submitForm">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+
+ <FormDialog
+ title="浠樻"
+ v-model="paymentDialogVisible"
+ width="800px"
+ @confirm="submitPayment"
+ @cancel="paymentDialogVisible = false"
+ >
+ <el-form :model="paymentForm" :rules="paymentRules" ref="paymentFormRef" label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浠樻鍗曞彿" prop="paymentNumber">
+ <el-input v-model="paymentForm.paymentNumber" placeholder="绯荤粺鑷姩鐢熸垚" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍏宠仈鐢宠鍗�" prop="invoiceApplicationNo">
+ <el-input v-model="paymentForm.invoiceApplicationNo" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="渚涘簲鍟�">
+ <el-input v-model="paymentForm.supplierName" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠樻鏃ユ湡" prop="paymentDate">
+ <el-date-picker
+ v-model="paymentForm.paymentDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浠樻閲戦" prop="paymentAmount">
+ <el-input-number
+ v-model="paymentForm.paymentAmount"
+ :min="0"
+ :precision="2"
+ style="width: 100%;"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠樻鏂瑰紡" prop="paymentMethod">
+ <el-select v-model="paymentForm.paymentMethod" placeholder="璇烽�夋嫨浠樻鏂瑰紡" style="width: 100%;">
+ <el-option
+ v-for="item in checkout_payment"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row v-if="isBankTransferPayment(paymentForm.paymentMethod)" :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="閾惰璐﹀彿" prop="bankAccount">
+ <el-input v-model="paymentForm.bankAccount" placeholder="閾惰璐﹀彿" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寮�鎴疯" prop="bankName">
+ <el-input v-model="paymentForm.bankName" placeholder="寮�鎴疯" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="paymentForm.remark" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary" :loading="paymentSubmitLoading" @click="submitPayment">纭畾</el-button>
+ <el-button @click="paymentDialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+
+ <el-dialog
+ v-model="inboundSelectVisible"
+ title="閫夋嫨鍏ュ簱鍗曞彿"
+ width="1100px"
+ append-to-body
+ destroy-on-close
+ :close-on-click-modal="false"
+ @closed="handleInboundDialogClosed"
+ >
+ <el-table
+ ref="inboundTableRef"
+ v-loading="inboundBatchLoading"
+ :data="inboundBatchList"
+ row-key="id"
+ border
+ stripe
+ max-height="480"
+ @selection-change="handleInboundDialogSelectionChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column prop="inboundBatches" label="鍏ュ簱鍗曞彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="supplierName" label="渚涘簲鍟�" min-width="120" show-overflow-tooltip />
+ <el-table-column prop="productName" label="浜у搧鍚嶇О" min-width="120" show-overflow-tooltip />
+ <el-table-column prop="specificationModel" label="瑙勬牸鍨嬪彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="purchaseContractNumber" label="閲囪喘璁㈠崟鍙�" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="inboundDate" label="鍏ュ簱鏃ユ湡" width="110" align="center" />
+ <el-table-column prop="inboundAmount" label="鍏ュ簱閲戦(鍚◣)" width="120" align="right">
+ <template #default="{ row }">楼{{ formatMoney(getInboundRowTaxInclusiveAmount(row)) }}</template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <el-button type="primary" @click="confirmInboundSelection">纭畾</el-button>
+ <el-button @click="inboundSelectVisible = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
+import {
+ getInboundBatchesBySupplier,
+ addAccountPaymentApplication,
+ listPageAccountPaymentApplication,
+ updateAccountPaymentApplication,
+ auditAccountPaymentApplication,
+ deleteAccountPaymentApplication,
+} from "@/api/financialManagement/accountPaymentApplication.js";
+import { addAccountPurchasePayment } from "@/api/financialManagement/accountPurchasePayment.js";
+
+defineOptions({
+ name: "浠樻鐢宠",
+});
+
+const { proxy } = getCurrentInstance();
+const { checkout_payment } = proxy.useDict("checkout_payment");
+
+const filters = reactive({
+ invoiceApplicationNo: "",
+ supplierId: "",
+ status: "",
+ dateRange: [],
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "鐢宠鍗曞彿", prop: "applyCode", width: "150" },
+ { label: "渚涘簲鍟�", prop: "supplierName", width: "180" },
+ { label: "浠樻閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+ { label: "浠樻鏂瑰紡", prop: "paymentMethod", dataType: "slot", slot: "paymentMethod", width: "120" },
+ { label: "鐢宠鏃ユ湡", prop: "applyDate", width: "120" },
+ { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status", width: "100" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "260", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isEdit = ref(false);
+const isView = ref(false);
+const submitLoading = ref(false);
+const currentId = ref(null);
+const supplierList = ref([]);
+
+const inboundBatchList = ref([]);
+const inboundBatchOptions = ref([]);
+const inboundBatchLoading = ref(false);
+const inboundSelectVisible = ref(false);
+const inboundTableRef = ref(null);
+const dialogInboundSelection = ref([]);
+
+const paymentDialogVisible = ref(false);
+const paymentFormRef = ref(null);
+const paymentSubmitLoading = ref(false);
+
+const paymentForm = reactive({
+ paymentNumber: "",
+ invoiceApplicationNo: "",
+ supplierName: "",
+ supplierId: "",
+ accountPaymentApplicationId: null,
+ paymentDate: "",
+ paymentAmount: 0,
+ paymentMethod: "",
+ bankAccount: "",
+ bankName: "",
+ remark: "",
+});
+
+const paymentRules = {
+ paymentDate: [{ required: true, message: "璇烽�夋嫨浠樻鏃ユ湡", trigger: "change" }],
+ paymentAmount: [{ required: true, message: "璇疯緭鍏ヤ粯娆鹃噾棰�", trigger: "blur" }],
+ paymentMethod: [{ required: true, message: "璇烽�夋嫨浠樻鏂瑰紡", trigger: "change" }],
+};
+
+const STATUS_LABEL_MAP = { 0: "寰呭鏍�", 1: "瀹℃牳閫氳繃", 2: "瀹℃牳涓嶉�氳繃" };
+const STATUS_TYPE_MAP = { 0: "warning", 1: "success", 2: "danger" };
+
+const form = reactive({
+ invoiceApplicationNo: "",
+ supplierId: "",
+ paymentAmount: 0,
+ paymentMethod: "",
+ applyDate: "",
+ paymentContent: "",
+ remark: "",
+ stockInRecordIds: [],
+ inboundBatches: "",
+ status: 0,
+});
+
+const rules = {
+ supplierId: [{ required: true, message: "璇烽�夋嫨渚涘簲鍟�", trigger: "change" }],
+ stockInRecordIds: [{ required: true, type: "array", min: 1, message: "璇烽�夋嫨鍏宠仈鍏ュ簱鍗�", trigger: "change" }],
+ paymentAmount: [{ required: true, message: "璇疯緭鍏ヤ粯娆鹃噾棰�", trigger: "blur" }],
+ paymentMethod: [{ required: true, message: "璇烽�夋嫨浠樻鏂瑰紡", trigger: "change" }],
+ applyDate: [{ required: true, message: "璇烽�夋嫨鐢宠鏃ユ湡", trigger: "change" }],
+};
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const normalizeStatus = (status) => {
+ if (status === undefined || status === null || status === "") return 0;
+ const num = Number(status);
+ return Number.isNaN(num) ? 0 : num;
+};
+
+const isPendingStatus = (status) => normalizeStatus(status) === 0;
+
+const isApprovedStatus = (status) => normalizeStatus(status) === 1;
+
+const isBankTransferPayment = (method) => {
+ if (method === undefined || method === null || method === "") return false;
+ const item = checkout_payment.value?.find((m) => String(m.value) === String(method));
+ if (item?.label?.includes("閾惰")) return true;
+ return String(method) === "bank_transfer" || String(method).toLowerCase().includes("bank");
+};
+
+const getStatusLabel = (status) => STATUS_LABEL_MAP[normalizeStatus(status)] ?? "寰呭鏍�";
+
+const getStatusType = (status) => STATUS_TYPE_MAP[normalizeStatus(status)] ?? "warning";
+
+const getPaymentMethodLabel = (value) => {
+ if (value === undefined || value === null || value === "") return "-";
+ const item = checkout_payment.value?.find((m) => String(m.value) === String(value));
+ return item?.label ?? value;
+};
+
+const getDefaultPaymentMethod = () => checkout_payment.value?.[0]?.value ?? "";
+
+const parseStockInRecordIds = (value) => {
+ if (!value) return [];
+ if (Array.isArray(value)) return value;
+ return String(value)
+ .split(/[,锛宂/)
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .map((s) => (/^\d+$/.test(s) ? Number(s) : s));
+};
+
+const formatInboundBatches = (value) => {
+ if (value === undefined || value === null || value === "") return "";
+ if (Array.isArray(value)) return value.filter(Boolean).join("銆�");
+ return String(value)
+ .split(/[,锛宂/)
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .join("銆�");
+};
+
+const isSameInboundId = (a, b) => String(a) === String(b);
+
+const getInboundRowId = (row) => row?.id ?? row?.stockInRecordId;
+
+const getInboundRowTaxInclusiveAmount = (row) =>
+ Number(row?.inboundAmount ?? row?.taxInclusivePrice ?? row?.totalAmount ?? row?.amount ?? 0);
+
+const normalizeInboundBatchOptions = (data) => {
+ const list = Array.isArray(data) ? data : [];
+ return list.map((item, index) => {
+ const label =
+ item.inboundBatches ?? item.batchNo ?? item.inboundNo ?? `鍏ュ簱鍗�${index + 1}`;
+ const value = item.id ?? item.stockInRecordId ?? label;
+ return {
+ label: String(label),
+ value,
+ inboundAmount: getInboundRowTaxInclusiveAmount(item),
+ };
+ });
+};
+
+const syncPaymentAmount = () => {
+ const selected = form.stockInRecordIds || [];
+ let sum = inboundBatchOptions.value
+ .filter((opt) => selected.some((id) => isSameInboundId(id, opt.value)))
+ .reduce((acc, opt) => acc + (Number(opt.inboundAmount) || 0), 0);
+
+ if (sum <= 0 && selected.length) {
+ sum = inboundBatchList.value
+ .filter((row) => selected.some((id) => isSameInboundId(id, getInboundRowId(row))))
+ .reduce((acc, row) => acc + getInboundRowTaxInclusiveAmount(row), 0);
+ }
+
+ form.paymentAmount = sum > 0 ? Number(sum.toFixed(2)) : 0;
+};
+
+const inboundBatchDisplayText = computed(() => {
+ if (form.inboundBatches) return form.inboundBatches;
+ const ids = form.stockInRecordIds || [];
+ if (!ids.length) return "";
+ const labels = inboundBatchOptions.value
+ .filter((opt) => ids.some((id) => isSameInboundId(id, opt.value)))
+ .map((opt) => opt.label);
+ if (labels.length) return labels.join("銆�");
+ return ids.join("銆�");
+});
+
+const normalizeTableRow = (row) => ({
+ ...row,
+ applyCode: row.invoiceApplicationNo ?? row.applyCode,
+ amount: row.paymentAmount ?? row.amount,
+ reason: row.paymentContent ?? row.reason,
+ status: normalizeStatus(row.status),
+ stockInRecordIds: row.stockInRecordIds ?? "",
+ inboundBatches: formatInboundBatches(row.inboundBatches),
+});
+
+const fillFormFromRow = (row) => {
+ const stockInRecordIds = parseStockInRecordIds(row.stockInRecordIds);
+ Object.assign(form, {
+ invoiceApplicationNo: row.invoiceApplicationNo ?? row.applyCode ?? "",
+ supplierId: row.supplierId,
+ paymentAmount: Number(row.paymentAmount ?? row.amount ?? 0),
+ paymentMethod: row.paymentMethod ?? getDefaultPaymentMethod(),
+ applyDate: row.applyDate ?? "",
+ paymentContent: row.paymentContent ?? row.reason ?? "",
+ remark: row.remark ?? "",
+ stockInRecordIds,
+ inboundBatches: formatInboundBatches(row.inboundBatches),
+ status: normalizeStatus(row.status),
+ });
+};
+
+const buildPayloadFromRow = (row, statusOverride) => ({
+ id: row.id,
+ supplierId: row.supplierId,
+ stockInRecordIds:
+ typeof row.stockInRecordIds === "string"
+ ? row.stockInRecordIds
+ : (row.stockInRecordIds || []).join(","),
+ invoiceApplicationNo: row.invoiceApplicationNo ?? row.applyCode ?? "",
+ paymentMethod: row.paymentMethod,
+ paymentContent: row.paymentContent ?? row.reason ?? "",
+ applyDate: row.applyDate,
+ remark: row.remark ?? "",
+ status: statusOverride !== undefined ? statusOverride : normalizeStatus(row.status),
+ paymentAmount: Number(row.paymentAmount ?? row.amount ?? 0),
+});
+
+const buildSubmitPayload = (forUpdate = false) => {
+ const payload = {
+ supplierId: form.supplierId,
+ stockInRecordIds: (form.stockInRecordIds || []).join(","),
+ invoiceApplicationNo: form.invoiceApplicationNo || "",
+ paymentMethod: form.paymentMethod,
+ paymentContent: form.paymentContent || "",
+ applyDate: form.applyDate,
+ remark: form.remark || "",
+ status: 0,
+ paymentAmount: form.paymentAmount,
+ };
+ if (forUpdate) {
+ payload.id = currentId.value;
+ }
+ return payload;
+};
+
+const getSupplierList = () => {
+ getOptions().then((res) => {
+ if (res.code === 200) {
+ supplierList.value = res.data ?? [];
+ }
+ });
+};
+
+const appendFilterParams = (params) => {
+ if (filters.invoiceApplicationNo) {
+ params.invoiceApplicationNo = filters.invoiceApplicationNo;
+ }
+ if (filters.supplierId) {
+ params.supplierId = filters.supplierId;
+ }
+ if (filters.status !== "" && filters.status != null) {
+ params.status = filters.status;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+};
+
+const buildListParams = () =>
+ appendFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+const buildExportParams = () => appendFilterParams({});
+
+const handleExport = () => {
+ proxy.download(
+ "/accountPaymentApplication/exportAccountPaymentApplication",
+ buildExportParams(),
+ `浠樻鐢宠_${Date.now()}.xlsx`
+ );
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountPaymentApplication(buildListParams())
+ .then((res) => {
+ if (res.code === 200) {
+ dataList.value = (res.data?.records ?? []).map(normalizeTableRow);
+ pagination.total = res.data?.total ?? 0;
+ } else {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const resetFilters = () => {
+ filters.invoiceApplicationNo = "";
+ filters.supplierId = "";
+ filters.status = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+};
+
+const closeDialog = () => {
+ dialogVisible.value = false;
+ isView.value = false;
+ isEdit.value = false;
+ inboundSelectVisible.value = false;
+};
+
+const resetForm = () => {
+ Object.assign(form, {
+ invoiceApplicationNo: "",
+ supplierId: "",
+ paymentAmount: 0,
+ paymentMethod: getDefaultPaymentMethod(),
+ applyDate: new Date().toISOString().split("T")[0],
+ paymentContent: "",
+ remark: "",
+ stockInRecordIds: [],
+ inboundBatches: "",
+ status: 0,
+ });
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+};
+
+const add = () => {
+ isEdit.value = false;
+ isView.value = false;
+ dialogTitle.value = "鏂板浠樻鐢宠";
+ resetForm();
+ dialogVisible.value = true;
+};
+
+const edit = (row) => {
+ isEdit.value = true;
+ isView.value = false;
+ currentId.value = row.id;
+ dialogTitle.value = "缂栬緫浠樻鐢宠";
+ fillFormFromRow(row);
+ dialogVisible.value = true;
+};
+
+const view = (row) => {
+ isView.value = true;
+ isEdit.value = false;
+ dialogTitle.value = "鏌ョ湅浠樻鐢宠";
+ fillFormFromRow(row);
+ if (row.supplierId) {
+ loadInboundBatches(row.supplierId, true, false);
+ }
+ dialogVisible.value = true;
+};
+
+const submitAudit = (row, status) => {
+ auditAccountPaymentApplication(buildPayloadFromRow(row, status))
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success(status === 1 ? "瀹℃牳閫氳繃" : "瀹℃牳涓嶉�氳繃");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "瀹℃牳澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("瀹℃牳澶辫触");
+ });
+};
+
+const handleAudit = (row) => {
+ ElMessageBox.confirm("璇烽�夋嫨瀹℃牳缁撴灉", "浠樻鐢宠瀹℃牳", {
+ confirmButtonText: "瀹℃牳閫氳繃",
+ cancelButtonText: "瀹℃牳涓嶉�氳繃",
+ distinguishCancelAndClose: true,
+ type: "warning",
+ })
+ .then(() => {
+ submitAudit(row, 1);
+ })
+ .catch((action) => {
+ if (action === "cancel") {
+ submitAudit(row, 2);
+ }
+ });
+};
+
+const openPaymentDialog = (row) => {
+ Object.assign(paymentForm, {
+ paymentNumber: "",
+ invoiceApplicationNo: row.invoiceApplicationNo ?? row.applyCode ?? "",
+ supplierName: row.supplierName ?? "",
+ supplierId: row.supplierId,
+ accountPaymentApplicationId: row.id,
+ paymentDate: new Date().toISOString().split("T")[0],
+ paymentAmount: Number(row.paymentAmount ?? row.amount ?? 0),
+ paymentMethod: row.paymentMethod ?? getDefaultPaymentMethod(),
+ bankAccount: row.bankAccountNum ?? row.bankAccount ?? "",
+ bankName: row.bankAccountName ?? row.bankName ?? "",
+ remark: "",
+ });
+ paymentDialogVisible.value = true;
+ nextTick(() => {
+ paymentFormRef.value?.clearValidate();
+ });
+};
+
+const submitPayment = () => {
+ paymentFormRef.value?.validate((valid) => {
+ if (!valid) return;
+ paymentSubmitLoading.value = true;
+ addAccountPurchasePayment({
+ accountPaymentApplicationId: paymentForm.accountPaymentApplicationId,
+ supplierId: paymentForm.supplierId,
+ paymentDate: paymentForm.paymentDate,
+ paymentMethod: paymentForm.paymentMethod,
+ paymentAmount: paymentForm.paymentAmount,
+ paymentNumber: paymentForm.paymentNumber || "",
+ remark: paymentForm.remark || "",
+ })
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("浠樻鎴愬姛");
+ paymentDialogVisible.value = false;
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "浠樻澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("浠樻澶辫触");
+ })
+ .finally(() => {
+ paymentSubmitLoading.value = false;
+ });
+ });
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎鐢宠鍗曘��${row.applyCode ?? row.invoiceApplicationNo}銆嶅悧锛焋, "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountPaymentApplication([row.id])
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+};
+
+const submitForm = () => {
+ formRef.value?.validate((valid) => {
+ if (!valid) return;
+ submitLoading.value = true;
+ const request = isEdit.value
+ ? updateAccountPaymentApplication(buildSubmitPayload(true))
+ : addAccountPaymentApplication(buildSubmitPayload(false));
+
+ request
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success(isEdit.value ? "缂栬緫鎴愬姛" : "鏂板鎴愬姛");
+ closeDialog();
+ pagination.currentPage = 1;
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "淇濆瓨澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("淇濆瓨澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+ });
+};
+
+const ensureInboundOptionsForSelected = () => {
+ const ids = form.stockInRecordIds || [];
+ ids.forEach((id) => {
+ const exists = inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, id));
+ if (exists) return;
+ const fromList = inboundBatchList.value.find((row) => isSameInboundId(getInboundRowId(row), id));
+ if (fromList) {
+ const [option] = normalizeInboundBatchOptions([fromList]);
+ if (option) inboundBatchOptions.value.push(option);
+ return;
+ }
+ inboundBatchOptions.value.push({
+ label: String(id),
+ value: id,
+ inboundAmount: 0,
+ });
+ });
+};
+
+const restoreInboundTableSelection = () => {
+ nextTick(() => {
+ const table = inboundTableRef.value;
+ if (!table) return;
+ table.clearSelection();
+ const selectedIds = new Set((form.stockInRecordIds || []).map((id) => String(id)));
+ inboundBatchList.value.forEach((row) => {
+ const rowId = getInboundRowId(row);
+ if (rowId !== undefined && rowId !== null && selectedIds.has(String(rowId))) {
+ table.toggleRowSelection(row, true);
+ }
+ });
+ });
+};
+
+const loadInboundBatches = (supplierId, keepSelected = false, syncAmount = true) => {
+ if (!supplierId) {
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+ if (!keepSelected) {
+ form.stockInRecordIds = [];
+ form.inboundBatches = "";
+ form.paymentAmount = 0;
+ }
+ return Promise.resolve();
+ }
+ inboundBatchLoading.value = true;
+ return getInboundBatchesBySupplier({ supplierId })
+ .then((res) => {
+ if (res.code === 200) {
+ const list = res.data?.records ?? res.data ?? [];
+ inboundBatchList.value = Array.isArray(list) ? list : [];
+ inboundBatchOptions.value = normalizeInboundBatchOptions(list);
+ } else {
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+ }
+ })
+ .catch(() => {
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+ })
+ .finally(() => {
+ inboundBatchLoading.value = false;
+ if (keepSelected) {
+ ensureInboundOptionsForSelected();
+ restoreInboundTableSelection();
+ if (syncAmount && !isView.value) {
+ syncPaymentAmount();
+ }
+ }
+ });
+};
+
+const handleSupplierChange = (supplierId) => {
+ form.stockInRecordIds = [];
+ form.inboundBatches = "";
+ form.paymentAmount = 0;
+ loadInboundBatches(supplierId);
+};
+
+const handleInboundInputClick = () => {
+ if (isEdit.value || isView.value) return;
+ openInboundSelectDialog();
+};
+
+const openInboundSelectDialog = () => {
+ if (!form.supplierId || isEdit.value || isView.value) return;
+ inboundSelectVisible.value = true;
+ loadInboundBatches(form.supplierId, true, false).then(() => {
+ restoreInboundTableSelection();
+ });
+};
+
+const handleInboundDialogSelectionChange = (selection) => {
+ dialogInboundSelection.value = selection;
+};
+
+const confirmInboundSelection = () => {
+ if (dialogInboundSelection.value.length === 0) {
+ ElMessage.warning("璇疯嚦灏戦�夋嫨涓�鏉″叆搴撳崟");
+ return;
+ }
+ form.stockInRecordIds = dialogInboundSelection.value
+ .map((row) => getInboundRowId(row))
+ .filter((id) => id !== undefined && id !== null);
+ form.inboundBatches = dialogInboundSelection.value
+ .map((row) => row.inboundBatches ?? row.batchNo ?? "")
+ .filter(Boolean)
+ .join("銆�");
+ dialogInboundSelection.value.forEach((row) => {
+ const [option] = normalizeInboundBatchOptions([row]);
+ if (option && !inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, option.value))) {
+ inboundBatchOptions.value.push(option);
+ }
+ });
+ inboundSelectVisible.value = false;
+ syncPaymentAmount();
+ formRef.value?.validateField("stockInRecordIds");
+};
+
+const handleInboundDialogClosed = () => {
+ dialogInboundSelection.value = [];
+};
+
+onMounted(() => {
+ getSupplierList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+}
+
+.inbound-batch-input :deep(.el-input__wrapper) {
+ cursor: pointer;
+}
+</style>
diff --git a/src/views/financialManagement/payable/purchaseIn.vue b/src/views/financialManagement/payable/purchaseIn.vue
new file mode 100644
index 0000000..532bcb4
--- /dev/null
+++ b/src/views/financialManagement/payable/purchaseIn.vue
@@ -0,0 +1,212 @@
+<template>
+ <!-- 閲囪喘鍏ュ簱 -->
+ <div class="app-container">
+ <el-form :model="filters"
+ :inline="true">
+ <el-form-item label="鍏ュ簱鍗曞彿:">
+ <el-input v-model="filters.inboundBatches"
+ placeholder="璇疯緭鍏ュ叆搴撳崟鍙�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�:">
+ <el-select v-model="filters.supplierId"
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ clearable
+ filterable
+ style="width: 200px;">
+ <el-option v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏ュ簱鏃ユ湡:">
+ <el-date-picker v-model="filters.dateRange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button @click="handleOut"
+ icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage">
+ <template #inboundDate="{ row }">
+ {{ row.inboundDate ?? row.InboundDate ?? "" }}
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+ import { ElMessage } from "element-plus";
+ import { listPageAccountPurchase } from "@/api/financialManagement/accountPurchase";
+ import { listSupplier } from "@/api/basicData/supplierManageFile.js";
+
+ defineOptions({
+ name: "閲囪喘鍏ュ簱",
+ });
+
+ const { proxy } = getCurrentInstance();
+
+ const filters = reactive({
+ inboundBatches: "",
+ supplierId: "",
+ dateRange: [],
+ });
+
+ const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+ });
+
+ const columns = [
+ { label: "鍏ュ簱鍗曞彿", prop: "inboundBatches", minWidth: "150" },
+ { label: "渚涘簲鍟�", prop: "supplierName", minWidth: "180" },
+ {
+ label: "鍏ュ簱鏃ユ湡",
+ prop: "inboundDate",
+ minWidth: "170",
+ dataType: "slot",
+ slot: "inboundDate",
+ },
+ { label: "浜у搧鍚嶇О", prop: "productName", minWidth: "140" },
+ { label: "瑙勬牸鍨嬪彿", prop: "specificationModel", minWidth: "140" },
+ {
+ label: "閲戦",
+ prop: "inboundAmount",
+ minWidth: "120",
+ align: "right",
+ formatData: val =>
+ val === null || val === undefined || val === ""
+ ? ""
+ : Number(val).toLocaleString("zh-CN", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }),
+ },
+ { label: "閲囪喘璁㈠崟鍙�", prop: "purchaseContractNumber", minWidth: "150" },
+ ];
+
+ const dataList = ref([]);
+ const tableLoading = ref(false);
+ const supplierList = ref([]);
+
+ const buildFilterParams = () => {
+ const params = {};
+ if (filters.inboundBatches) {
+ params.inboundBatches = filters.inboundBatches;
+ }
+ if (filters.supplierId) {
+ params.supplierId = filters.supplierId;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+ };
+
+ const getSupplierList = () => {
+ listSupplier({ current: -1, size: -1, isWhite: 0 }).then(res => {
+ if (res.code === 200) {
+ supplierList.value = res.data?.records ?? [];
+ }
+ });
+ };
+
+ const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountPurchase({
+ ...buildFilterParams(),
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ })
+ .then(res => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = res.data.records ?? [];
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ pagination.total = 0;
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const resetFilters = () => {
+ filters.inboundBatches = "";
+ filters.supplierId = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+ };
+
+ const handleOut = () => {
+ proxy.download(
+ "/accountPurchase/exportAccountPurchaseInbound",
+ buildFilterParams(),
+ `閲囪喘鍏ュ簱_${Date.now()}.xlsx`
+ );
+ };
+
+ onMounted(() => {
+ getSupplierList();
+ getTableData();
+ });
+</script>
+
+<style lang="scss" scoped>
+ .actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+ }
+</style>
diff --git a/src/views/financialManagement/payable/purchaseReturn.vue b/src/views/financialManagement/payable/purchaseReturn.vue
new file mode 100644
index 0000000..eeec383
--- /dev/null
+++ b/src/views/financialManagement/payable/purchaseReturn.vue
@@ -0,0 +1,198 @@
+<template>
+ <!-- 閲囪喘閫�璐� -->
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="閫�璐у崟鍙�:">
+ <el-input v-model="filters.returnNo" placeholder="璇疯緭鍏ラ��璐у崟鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�:">
+ <el-select v-model="filters.supplierId" placeholder="璇烽�夋嫨渚涘簲鍟�" clearable filterable style="width: 200px;">
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫�璐ф棩鏈�:">
+ <el-date-picker
+ v-model="filters.dateRange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+import { ElMessage } from "element-plus";
+import { listPageAccountPurchaseReturn } from "@/api/financialManagement/accountPurchase";
+import { listSupplier } from "@/api/basicData/supplierManageFile.js";
+
+defineOptions({
+ name: "閲囪喘閫�璐�",
+});
+
+const { proxy } = getCurrentInstance();
+
+const filters = reactive({
+ returnNo: "",
+ supplierId: "",
+ dateRange: [],
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "閫�璐у崟鍙�", prop: "returnNo", minWidth: "150" },
+ { label: "渚涘簲鍟�", prop: "supplierName", minWidth: "180" },
+ { label: "鍏宠仈鍏ュ簱鍗曞彿", prop: "inboundBatches", minWidth: "150" },
+ { label: "閫�璐ф棩鏈�", prop: "preparedAt", minWidth: "170" },
+ {
+ label: "閫�娆炬�婚",
+ prop: "totalAmount",
+ minWidth: "150",
+ align: "right",
+ formatData: (val) =>
+ val === null || val === undefined || val === ""
+ ? ""
+ : Number(val).toLocaleString("zh-CN", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }),
+ },
+ { label: "閫�璐ф柟寮�", prop: "returnType", minWidth: "150" },
+ { label: "閲囪喘璁㈠崟鍙�", prop: "purchaseContractNumber", minWidth: "150" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const supplierList = ref([]);
+
+const buildFilterParams = () => {
+ const params = {};
+ if (filters.returnNo) {
+ params.returnNo = filters.returnNo;
+ }
+ if (filters.supplierId) {
+ params.supplierId = filters.supplierId;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+};
+
+const getSupplierList = () => {
+ listSupplier({ current: -1, size: -1, isWhite: 0 }).then((res) => {
+ if (res.code === 200) {
+ supplierList.value = res.data?.records ?? [];
+ }
+ });
+};
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountPurchaseReturn({
+ ...buildFilterParams(),
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ })
+ .then((res) => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = res.data.records ?? [];
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ pagination.total = 0;
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const resetFilters = () => {
+ filters.returnNo = "";
+ filters.supplierId = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+};
+
+const handleOut = () => {
+ proxy.download(
+ "/accountPurchase/exportAccountPurchaseReturn",
+ buildFilterParams(),
+ `閲囪喘閫�璐${Date.now()}.xlsx`
+ );
+};
+
+onMounted(() => {
+ getSupplierList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+</style>
diff --git a/src/views/financialManagement/payable/reconciliation.vue b/src/views/financialManagement/payable/reconciliation.vue
new file mode 100644
index 0000000..e749e56
--- /dev/null
+++ b/src/views/financialManagement/payable/reconciliation.vue
@@ -0,0 +1,766 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="渚涘簲鍟�:">
+ <el-select v-model="filters.supplierId" placeholder="璇烽�夋嫨渚涘簲鍟�" clearable filterable style="width: 200px;">
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀵硅处鏈熼棿:">
+ <el-date-picker v-model="filters.startMonth" type="month" placeholder="寮�濮嬫湀浠�" value-format="YYYY-MM" style="width: 140px;" />
+ <span style="margin: 0 10px;">鑷�</span>
+ <el-date-picker v-model="filters.endMonth" type="month" placeholder="缁撴潫鏈堜唤" value-format="YYYY-MM" style="width: 140px;" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-button type="primary" @click="generateStatement" icon="Document">鐢熸垚瀵硅处鍗�</el-button>
+ </div>
+ <div>
+ <el-button @click="handleOut" icon="Download">瀵煎嚭瀵硅处鍗�</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ >
+ <template #openingBalance="{ row }">
+ <span :class="row.openingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.openingBalance) }}</span>
+ </template>
+ <template #currentPlan="{ row }">
+ <span class="text-danger">楼{{ formatMoney(row.currentPlan) }}</span>
+ </template>
+ <template #currentActually="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.currentActually) }}</span>
+ </template>
+ <template #closingBalance="{ row }">
+ <span :class="row.closingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.closingBalance) }}</span>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="viewDetail(row)">鏌ョ湅鏄庣粏</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog title="瀵硅处鏄庣粏" v-model="detailDialogVisible" width="900px" @confirm="printDetail" @cancel="detailDialogVisible = false" operationType="detail">
+ <div class="statement-header">
+ <h3>{{ currentSupplier }} 搴斾粯瀵硅处鍗�</h3>
+ <p>瀵硅处鏈熼棿: {{ currentPeriod }}</p>
+ </div>
+ <el-table :data="detailData" border style="width: 100%" v-loading="detailLoading">
+ <el-table-column prop="date" label="鏃ユ湡" width="120" />
+ <el-table-column prop="type" label="绫诲瀷" width="100">
+ <template #default="{ row }">
+ <el-tag :type="row.type === '鍏ュ簱' ? 'success' : row.type === '閫�璐�' ? 'danger' : 'primary'">{{ row.type }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="code" label="鍗曟嵁缂栧彿" width="150" />
+ <el-table-column prop="debit" label="鍊熸柟(浠樻)" width="120">
+ <template #default="{ row }">
+ <span v-if="row.debit > 0" class="text-success">楼{{ formatMoney(row.debit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="credit" label="璐锋柟(搴斾粯)" width="120">
+ <template #default="{ row }">
+ <span v-if="row.credit > 0" class="text-danger">楼{{ formatMoney(row.credit) }}</span>
+ <span v-else-if="row.credit < 0" class="text-success">楼{{ formatMoney(Math.abs(row.credit)) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="balance" label="浣欓" width="120">
+ <template #default="{ row }">
+ <span :class="row.balance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.balance) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="remark" label="澶囨敞" show-overflow-tooltip />
+ </el-table>
+ <template #footer>
+ <el-button type="primary" @click="printDetail">鎵撳嵃</el-button>
+ <el-button @click="detailDialogVisible = false">鍏抽棴</el-button>
+ </template>
+ </FormDialog>
+
+ <FormDialog title="鐢熸垚瀵硅处鍗�" v-model="generateDialogVisible" width="1000px" @confirm="confirmGenerate" @cancel="generateDialogVisible = false">
+ <el-form :model="generateForm" label-width="100px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="閫夋嫨渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="generateForm.supplierId"
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ style="width: 100%;"
+ filterable
+ @change="onSupplierChange"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀵硅处鏈堜唤" prop="statementMonth">
+ <el-date-picker
+ v-model="generateForm.statementMonth"
+ type="month"
+ placeholder="閫夋嫨鏈堜唤"
+ value-format="YYYY-MM"
+ style="width: 100%;"
+ @change="onStatementMonthChange"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+
+ <div v-if="statementDetailLoaded" class="purchase-section">
+ <div v-if="purchaseData.length > 0" class="section-title">鏈湀閲囪喘鏁版嵁</div>
+ <el-table
+ v-if="purchaseData.length > 0"
+ ref="purchaseTableRef"
+ :data="purchaseData"
+ border
+ row-key="id"
+ style="width: 100%; margin-bottom: 15px;"
+ v-loading="purchaseLoading"
+ @selection-change="handlePurchaseSelectionChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column prop="occurrenceDate" label="鏃ユ湡" width="120" />
+ <el-table-column prop="receiptNumber" label="鍗曟嵁缂栧彿" width="150" />
+ <el-table-column prop="type" label="绫诲瀷" width="100">
+ <template #default="{ row }">
+ <el-tag :type="getDetailTypeTagType(row.type)">{{ row.typeLabel }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="amount" label="閲戦" width="120">
+ <template #default="{ row }">
+ <span :class="getDetailAmountClass(row.type)">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="remark" label="澶囨敞" />
+ </el-table>
+ <el-empty v-else description="璇ヤ緵搴斿晢鏈湀鏆傛棤鏄庣粏鏁版嵁" :image-size="80" />
+
+ <div class="summary-row">
+ <span>鏈熷垵浣欓: <strong class="text-primary">楼{{ formatMoney(generateForm.openingBalance) }}</strong></span>
+ <span>鏈湡搴斾粯: <strong class="text-danger">楼{{ formatMoney(generateForm.currentPlan) }}</strong></span>
+ <span>鏈湡浠樻: <strong class="text-success">楼{{ formatMoney(generateForm.currentActually) }}</strong></span>
+ <span>鏈熸湯浣欓: <strong :class="displayClosingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(displayClosingBalance) }}</strong></span>
+ </div>
+ </div>
+
+ <div v-else-if="generateForm.supplierId && generateForm.statementMonth && !purchaseLoading" class="empty-tip">
+ <el-empty description="璇ヤ緵搴斿晢鏈湀鏆傛棤閲囪喘鏁版嵁" />
+ </div>
+
+ <template #footer>
+ <el-button type="primary" @click="confirmGenerate" :disabled="!canGenerate" :loading="submitLoading">纭鐢熸垚</el-button>
+ <el-button @click="generateDialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, nextTick, getCurrentInstance } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
+import {
+ getAccountStatementDetailsByMonth,
+ addAccountStatement,
+ listPageAccountStatement,
+ deleteAccountStatement,
+} from "@/api/financialManagement/accountStatement.js";
+
+const ACCOUNT_TYPE_PAYABLE = 2;
+
+const { proxy } = getCurrentInstance();
+
+defineOptions({
+ name: "搴斾粯瀵硅处",
+});
+
+const filters = reactive({
+ supplierId: "",
+ startMonth: "",
+ endMonth: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "瀵硅处鍗曞彿", prop: "statementNumber", width: "150" },
+ { label: "渚涘簲鍟�", prop: "supplierName", width: "180" },
+ { label: "瀵硅处鏈熼棿", prop: "statementMonth", width: "150" },
+ { label: "鏈熷垵浣欓", prop: "openingBalance", dataType: "slot", slot: "openingBalance" },
+ { label: "鏈湡搴斾粯", prop: "currentPlan", dataType: "slot", slot: "currentPlan" },
+ { label: "鏈湡浠樻", prop: "currentActually", dataType: "slot", slot: "currentActually" },
+ { label: "鏈熸湯浣欓", prop: "closingBalance", dataType: "slot", slot: "closingBalance" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "200", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const submitLoading = ref(false);
+const detailDialogVisible = ref(false);
+const currentSupplier = ref("");
+const currentPeriod = ref("");
+const detailData = ref([]);
+const detailLoading = ref(false);
+
+const generateDialogVisible = ref(false);
+const purchaseLoading = ref(false);
+const statementDetailLoaded = ref(false);
+const purchaseData = ref([]);
+const selectedPurchases = ref([]);
+const purchaseTableRef = ref(null);
+const supplierList = ref([]);
+
+/** 鏄庣粏 type锛�1鍑哄簱 2鍏ュ簱 3鏀舵 4浠樻 5閫�璐� */
+const STATEMENT_DETAIL_TYPE_MAP = {
+ 1: "鍑哄簱",
+ 2: "鍏ュ簱",
+ 3: "鏀舵",
+ 4: "浠樻",
+ 5: "閫�璐�",
+};
+
+const calculateEndBalance = (openingBalance, currentPlan, currentActually) => {
+ return openingBalance + currentPlan - currentActually;
+};
+
+const getDetailTypeLabel = (type) => STATEMENT_DETAIL_TYPE_MAP[Number(type)] ?? "";
+
+const getDetailTypeTagType = (type) => {
+ const t = Number(type);
+ if (t === 2) return "success";
+ if (t === 4) return "primary";
+ if (t === 5) return "danger";
+ return "info";
+};
+
+const getDetailAmountClass = (type) => {
+ const t = Number(type);
+ if (t === 2) return "text-danger";
+ if (t === 4) return "text-success";
+ return "text-danger";
+};
+
+const generateForm = reactive({
+ supplierId: "",
+ supplierName: "",
+ statementMonth: "",
+ openingBalance: 0,
+ currentPlan: 0,
+ currentActually: 0,
+ closingBalance: 0,
+});
+
+const displayClosingBalance = computed(() =>
+ calculateEndBalance(
+ generateForm.openingBalance,
+ generateForm.currentPlan,
+ generateForm.currentActually
+ )
+);
+
+const canGenerate = computed(
+ () => generateForm.supplierId && generateForm.statementMonth && selectedPurchases.value.length > 0
+);
+
+const applyStatementSummary = (data) => {
+ generateForm.openingBalance = Number(data.openingBalance ?? 0);
+ generateForm.currentPlan = Number(data.currentPlan ?? 0);
+ generateForm.currentActually = Number(data.currentActually ?? 0);
+ generateForm.closingBalance = Number(
+ data.closingBalance ??
+ calculateEndBalance(
+ generateForm.openingBalance,
+ generateForm.currentPlan,
+ generateForm.currentActually
+ )
+ );
+};
+
+const getSupplierList = () => {
+ getOptions().then((res) => {
+ if (res.code === 200) {
+ supplierList.value = res.data ?? [];
+ }
+ });
+};
+
+const normalizePurchaseRows = (list) => {
+ const rows = Array.isArray(list) ? list : [];
+ return rows.map((item, index) => {
+ const type = Number(item.type);
+ return {
+ id: item.id ?? `detail-${index}`,
+ accountStatementId: item.accountStatementId,
+ occurrenceDate: item.occurrenceDate ?? "",
+ receiptNumber: item.receiptNumber ?? "",
+ type,
+ typeLabel: getDetailTypeLabel(type),
+ amount: Math.abs(Number(item.amount ?? 0)),
+ remark: item.remark ?? "",
+ };
+ });
+};
+
+const selectAllPurchaseRows = (keepApiSummary = false) => {
+ nextTick(() => {
+ const table = purchaseTableRef.value;
+ if (!table) return;
+ table.clearSelection();
+ purchaseData.value.forEach((row) => table.toggleRowSelection(row, true));
+ selectedPurchases.value = [...purchaseData.value];
+ if (!keepApiSummary) {
+ calculateSummary();
+ }
+ });
+};
+
+const isNumericId = (id) => id !== undefined && id !== null && id !== "" && /^\d+$/.test(String(id));
+
+const buildFilterParams = (params = {}) => {
+ const result = { ...params, accountType: ACCOUNT_TYPE_PAYABLE };
+ if (filters.supplierId) {
+ result.customerId = filters.supplierId;
+ }
+ if (filters.startMonth && filters.endMonth && filters.startMonth === filters.endMonth) {
+ result.statementMonth = filters.startMonth;
+ } else if (filters.startMonth) {
+ result.startMonth = filters.startMonth;
+ }
+ if (filters.endMonth && filters.startMonth !== filters.endMonth) {
+ result.endMonth = filters.endMonth;
+ }
+ return result;
+};
+
+const buildListParams = () =>
+ buildFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+const buildExportParams = () => buildFilterParams({});
+
+const buildDetailSubmitItem = (row) => {
+ const item = {
+ occurrenceDate: row.occurrenceDate,
+ receiptNumber: row.receiptNumber,
+ type: row.type,
+ amount: row.amount,
+ remark: row.remark ?? "",
+ };
+ if (isNumericId(row.id)) {
+ item.id = Number(row.id);
+ }
+ if (row.accountStatementId) {
+ item.accountStatementId = row.accountStatementId;
+ }
+ return item;
+};
+
+const buildAddPayload = () => ({
+ customerId: generateForm.supplierId,
+ customerName: generateForm.supplierName,
+ statementMonth: generateForm.statementMonth,
+ accountType: ACCOUNT_TYPE_PAYABLE,
+ statementNumber: "",
+ openingBalance: generateForm.openingBalance,
+ currentPlan: generateForm.currentPlan,
+ currentActually: generateForm.currentActually,
+ closingBalance: generateForm.closingBalance,
+ accountStatementDetails: selectedPurchases.value.map(buildDetailSubmitItem),
+});
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountStatement(buildListParams())
+ .then((res) => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = (res.data.records ?? []).map((row) => ({
+ ...row,
+ supplierName: row.supplierName ?? row.customerName,
+ }));
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ pagination.total = 0;
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const resetFilters = () => {
+ filters.supplierId = "";
+ filters.startMonth = "";
+ filters.endMonth = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+};
+
+const generateStatement = () => {
+ generateForm.supplierId = "";
+ generateForm.supplierName = "";
+ generateForm.statementMonth = "";
+ generateForm.openingBalance = 0;
+ generateForm.currentPlan = 0;
+ generateForm.currentActually = 0;
+ generateForm.closingBalance = 0;
+ statementDetailLoaded.value = false;
+ purchaseData.value = [];
+ selectedPurchases.value = [];
+ generateDialogVisible.value = true;
+};
+
+const onSupplierChange = (supplierId) => {
+ const supplier = supplierList.value.find((item) => item.id === supplierId);
+ generateForm.supplierName = supplier?.supplierName ?? "";
+ loadPurchaseData();
+};
+
+const onStatementMonthChange = () => {
+ loadPurchaseData();
+};
+
+const loadPurchaseData = () => {
+ if (!generateForm.supplierId || !generateForm.statementMonth) {
+ purchaseData.value = [];
+ selectedPurchases.value = [];
+ statementDetailLoaded.value = false;
+ generateForm.openingBalance = 0;
+ generateForm.currentPlan = 0;
+ generateForm.currentActually = 0;
+ generateForm.closingBalance = 0;
+ return;
+ }
+
+ purchaseLoading.value = true;
+ selectedPurchases.value = [];
+ statementDetailLoaded.value = false;
+
+ getAccountStatementDetailsByMonth({
+ accountType: ACCOUNT_TYPE_PAYABLE,
+ customerId: generateForm.supplierId,
+ statementMonth: generateForm.statementMonth,
+ })
+ .then((res) => {
+ if (res.code === 200) {
+ const data = res.data ?? {};
+ const details = data.accountStatementDetails;
+ const list = Array.isArray(details) ? details : [];
+ purchaseData.value = normalizePurchaseRows(list);
+ applyStatementSummary(data);
+ statementDetailLoaded.value = true;
+
+ if (purchaseData.value.length > 0) {
+ selectAllPurchaseRows(true);
+ }
+ } else {
+ purchaseData.value = [];
+ statementDetailLoaded.value = false;
+ ElMessage.error(res.msg || "鏌ヨ瀵硅处鏄庣粏澶辫触");
+ }
+ })
+ .catch(() => {
+ purchaseData.value = [];
+ statementDetailLoaded.value = false;
+ ElMessage.error("鏌ヨ瀵硅处鏄庣粏澶辫触");
+ })
+ .finally(() => {
+ purchaseLoading.value = false;
+ });
+};
+
+const calculateSummary = () => {
+ let payable = 0;
+ let payment = 0;
+
+ selectedPurchases.value.forEach((item) => {
+ if (item.type === 2) {
+ payable += item.amount;
+ } else if (item.type === 5) {
+ payable -= item.amount;
+ } else if (item.type === 4) {
+ payment += item.amount;
+ }
+ });
+
+ generateForm.currentPlan = payable;
+ generateForm.currentActually = payment;
+ generateForm.closingBalance = calculateEndBalance(
+ generateForm.openingBalance,
+ generateForm.currentPlan,
+ generateForm.currentActually
+ );
+};
+
+const handlePurchaseSelectionChange = (selection) => {
+ selectedPurchases.value = selection;
+ calculateSummary();
+};
+
+const confirmGenerate = () => {
+ if (!canGenerate.value) return;
+ submitLoading.value = true;
+ addAccountStatement(buildAddPayload())
+ .then((res) => {
+ if (res.code === 200) {
+ generateDialogVisible.value = false;
+ ElMessage.success("瀵硅处鍗曠敓鎴愭垚鍔�");
+ pagination.currentPage = 1;
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鐢熸垚澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鐢熸垚澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎瀵硅处鍗曘��${row.statementNumber || row.id}銆嶅悧锛焋, "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountStatement([row.id])
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+};
+
+const buildDetailTableFromApi = (data, statementMonth) => {
+ const details = Array.isArray(data.accountStatementDetails) ? data.accountStatementDetails : [];
+ let runningBalance = Number(data.openingBalance ?? 0);
+ const rows = [
+ {
+ date: statementMonth ?? "",
+ type: "鏈熷垵",
+ code: "-",
+ debit: 0,
+ credit: 0,
+ balance: runningBalance,
+ remark: "鏈熷垵浣欓",
+ },
+ ];
+
+ details.forEach((item) => {
+ const amount = Math.abs(Number(item.amount ?? 0));
+ const type = Number(item.type);
+ let debit = 0;
+ let credit = 0;
+
+ if (type === 2) {
+ credit = amount;
+ runningBalance += amount;
+ } else if (type === 4) {
+ debit = amount;
+ runningBalance -= amount;
+ } else if (type === 5) {
+ credit = -amount;
+ runningBalance -= amount;
+ }
+
+ rows.push({
+ date: item.occurrenceDate ?? "",
+ type: getDetailTypeLabel(type),
+ code: item.receiptNumber ?? "",
+ debit,
+ credit,
+ balance: runningBalance,
+ remark: item.remark ?? "",
+ });
+ });
+
+ return rows;
+};
+
+const viewDetail = (row) => {
+ const partnerId = row.customerId ?? row.supplierId;
+ if (!partnerId || !row.statementMonth) {
+ ElMessage.warning("缂哄皯渚涘簲鍟嗘垨瀵硅处鏈堜唤锛屾棤娉曟煡璇㈡槑缁�");
+ return;
+ }
+
+ currentSupplier.value = row.supplierName ?? row.customerName ?? "";
+ currentPeriod.value = row.statementMonth ?? "";
+ detailData.value = [];
+ detailDialogVisible.value = true;
+ detailLoading.value = true;
+
+ getAccountStatementDetailsByMonth({
+ accountType: ACCOUNT_TYPE_PAYABLE,
+ customerId: partnerId,
+ statementMonth: row.statementMonth,
+ })
+ .then((res) => {
+ if (res.code === 200) {
+ detailData.value = buildDetailTableFromApi(res.data ?? {}, row.statementMonth);
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ鏄庣粏澶辫触");
+ detailDialogVisible.value = false;
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鏌ヨ鏄庣粏澶辫触");
+ detailDialogVisible.value = false;
+ })
+ .finally(() => {
+ detailLoading.value = false;
+ });
+};
+
+const printDetail = () => {
+ ElMessage.info("鎵撳嵃鏄庣粏");
+};
+
+const handleOut = () => {
+ proxy.download(
+ "/accountStatement/exportAccountStatement",
+ buildExportParams(),
+ `搴斾粯瀵硅处鍗昣${Date.now()}.xlsx`
+ );
+};
+
+onMounted(() => {
+ getSupplierList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.text-success {
+ color: #67c23a;
+}
+
+.text-danger {
+ color: #f56c6c;
+}
+
+.text-primary {
+ color: #409eff;
+}
+
+.statement-header {
+ text-align: center;
+ margin-bottom: 20px;
+ h3 {
+ margin: 0 0 10px 0;
+ }
+ p {
+ color: #909399;
+ margin: 0;
+ }
+}
+
+.purchase-section {
+ margin-top: 20px;
+
+ .section-title {
+ font-size: 16px;
+ font-weight: bold;
+ margin-bottom: 15px;
+ padding-left: 10px;
+ border-left: 4px solid #409eff;
+ }
+}
+
+.summary-row {
+ display: flex;
+ justify-content: space-around;
+ padding: 15px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ margin-top: 15px;
+
+ span {
+ font-size: 14px;
+
+ strong {
+ font-size: 16px;
+ margin-left: 5px;
+ }
+ }
+}
+
+.empty-tip {
+ margin-top: 30px;
+}
+</style>
diff --git a/src/views/financialManagement/receivable/invoiceApply.vue b/src/views/financialManagement/receivable/invoiceApply.vue
new file mode 100644
index 0000000..31b6345
--- /dev/null
+++ b/src/views/financialManagement/receivable/invoiceApply.vue
@@ -0,0 +1,902 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="鐢宠鍗曞彿:">
+ <el-input v-model="filters.applyCode" placeholder="璇疯緭鍏ョ敵璇峰崟鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛:">
+ <el-select v-model="filters.customerId" placeholder="璇烽�夋嫨瀹㈡埛" clearable style="width: 200px;">
+ <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹℃牳鐘舵��:">
+ <el-select v-model="filters.status" placeholder="璇烽�夋嫨瀹℃牳鐘舵��" clearable style="width: 150px;">
+ <el-option label="寰呭鏍�" :value="0" />
+ <el-option label="瀹℃牳閫氳繃" :value="1" />
+ <el-option label="瀹℃牳涓嶉�氳繃" :value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐢宠鏃ユ湡:">
+ <el-date-picker
+ v-model="filters.dateRange"
+ type="daterange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ style="width: 240px;"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus">鏂板鐢宠</el-button>
+ <el-button type="success" @click="handleExport" icon="Download">瀵煎嚭寮�绁ㄧ敵璇�</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ isSelection
+ v-loading="tableLoading"
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @selection-change="handleSelectionChange"
+ @pagination="changePage"
+ >
+ <template #amount="{ row }">
+ <span class="text-primary">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ <template #taxRate="{ row }">
+ <span>{{ row.taxRate }}%</span>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)" effect="light" round>
+ {{ getStatusLabel(row.status) }}
+ </el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button type="primary" link @click="edit(row)" v-if="isPendingStatus(row.status)">缂栬緫</el-button>
+ <el-button type="danger" link @click="handleDelete(row)" v-if="isPendingStatus(row.status)">鍒犻櫎</el-button>
+ <el-button type="success" link @click="handleAudit(row)" v-if="isPendingStatus(row.status)">瀹℃牳</el-button>
+ <el-button type="primary" link @click="openFileDialog(row)" v-if="isApprovedStatus(row.status)">闄勪欢</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog
+ :title="dialogTitle"
+ v-model="dialogVisible"
+ width="800px"
+ :operation-type="isView ? 'detail' : ''"
+ @confirm="submitForm"
+ @cancel="closeDialog"
+ >
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+ <el-row v-if="isView" :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀹℃牳鐘舵��">
+ <el-tag :type="getStatusType(form.status)" effect="light" round>
+ {{ getStatusLabel(form.status) }}
+ </el-tag>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="鐢宠鍗曞彿" prop="applyCode">
+ <el-input v-model="form.applyCode" placeholder="绯荤粺鑷姩鐢熸垚" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="form.customerId"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ style="width: 100%;"
+ :disabled="isEdit || isView"
+ filterable
+ @change="handleCustomerChange"
+ >
+ <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍑哄簱鍗曞彿" prop="outboundBatchNos">
+ <el-input
+ :model-value="outboundBatchDisplayText"
+ placeholder="璇峰厛閫夋嫨瀹㈡埛"
+ readonly
+ :disabled="!form.customerId || isEdit || isView"
+ class="outbound-batch-input"
+ @click="handleOutboundInputClick"
+ >
+ <template v-if="!isEdit && !isView" #append>
+ <el-button
+ :disabled="!form.customerId"
+ :loading="outboundBatchLoading"
+ @click.stop="openOutboundSelectDialog"
+ >
+ 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="寮�绁ㄩ噾棰�" prop="amount">
+ <el-input-number
+ v-model="form.amount"
+ :min="0"
+ :precision="2"
+ :disabled="isView"
+ style="width: 100%;"
+ placeholder="鏍规嵁鎵�閫夊嚭搴撳崟鑷姩姹囨�伙紝鍙慨鏀�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绋庣巼" prop="taxRate">
+ <el-select v-model="form.taxRate" placeholder="璇烽�夋嫨绋庣巼" style="width: 100%;" :disabled="isView">
+ <el-option
+ v-for="dict in tax_rate"
+ :key="dict.value"
+ :label="dict.label"
+ :value="Number(dict.value)"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ绫诲瀷" prop="invoiceType">
+ <el-select v-model="form.invoiceType" placeholder="璇烽�夋嫨鍙戠エ绫诲瀷" style="width: 100%;" :disabled="isView">
+ <el-option label="澧炲�肩◣涓撶敤鍙戠エ" value="澧炲�肩◣涓撶敤鍙戠エ" />
+ <el-option label="澧炲�肩◣鏅�氬彂绁�" value="澧炲�肩◣鏅�氬彂绁�" />
+ <el-option label="鐢靛瓙鍙戠エ" value="鐢靛瓙鍙戠エ" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢宠鏃ユ湡" prop="applyDate">
+ <el-date-picker
+ v-model="form.applyDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ :disabled="isView"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鍙戠エ鍐呭" prop="content">
+ <el-input v-model="form.content" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ彂绁ㄥ唴瀹�" :disabled="isView" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" :disabled="isView" />
+ </el-form-item>
+ </el-form>
+ <template v-if="!isView" #footer>
+ <el-button type="primary" :loading="submitLoading" @click="submitForm">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+
+ <el-dialog
+ v-model="outboundSelectVisible"
+ title="閫夋嫨鍑哄簱鍗�"
+ width="1200px"
+ append-to-body
+ destroy-on-close
+ :close-on-click-modal="false"
+ @closed="handleOutboundDialogClosed"
+ >
+ <el-table
+ ref="outboundTableRef"
+ v-loading="outboundBatchLoading"
+ :data="outboundBatchList"
+ row-key="id"
+ border
+ stripe
+ max-height="480"
+ @selection-change="handleOutboundDialogSelectionChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column prop="outboundBatches" label="鍑哄簱鍗曞彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="customerName" label="瀹㈡埛鍚嶇О" min-width="120" show-overflow-tooltip />
+ <el-table-column prop="productName" label="浜у搧鍚嶇О" min-width="120" show-overflow-tooltip />
+ <el-table-column prop="specificationModel" label="瑙勬牸鍨嬪彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="salesContractNo" label="閿�鍞悎鍚屽彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="shippingNo" label="鍙戣揣鍗曞彿" min-width="130" show-overflow-tooltip />
+ <el-table-column prop="shippingDate" label="鍙戣揣鏃ユ湡" width="110" align="center" />
+ <el-table-column prop="outboundAmount" label="鍑哄簱閲戦" width="110" align="right">
+ <template #default="{ row }">楼{{ formatMoney(row.outboundAmount) }}</template>
+ </el-table-column>
+ <el-table-column prop="taxRate" label="绋庣巼" width="80" align="center">
+ <template #default="{ row }">{{ row.taxRate }}%</template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <el-button type="primary" @click="confirmOutboundSelection">纭畾</el-button>
+ <el-button @click="outboundSelectVisible = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+
+ <FileList
+ v-if="fileDialogVisible"
+ v-model:visible="fileDialogVisible"
+ record-type="account_invoice_application"
+ :record-id="currentRecordId"
+ />
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance, defineAsyncComponent } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { listCustomer } from "@/api/basicData/customer.js";
+import {
+ getOutboundBatchesByCustomer,
+ addAccountInvoiceApplication,
+ listPageAccountInvoiceApplication,
+ auditAccountInvoiceApplication,
+ updateAccountInvoiceApplication,
+ deleteAccountInvoiceApplication,
+} from "@/api/financialManagement/invoiceApply.js";
+
+const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
+
+defineOptions({
+ name: "寮�绁ㄧ敵璇�",
+});
+
+const { proxy } = getCurrentInstance();
+const { tax_rate } = proxy.useDict("tax_rate");
+
+const filters = reactive({
+ applyCode: "",
+ customerId: "",
+ status: "",
+ dateRange: [],
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "鐢宠鍗曞彿", prop: "applyCode", width: "150" },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", width: "180" },
+ { label: "寮�绁ㄩ噾棰�", prop: "amount", dataType: "slot", slot: "amount" },
+ { label: "绋庣巼", prop: "taxRate", dataType: "slot", slot: "taxRate" },
+ { label: "鍙戠エ绫诲瀷", prop: "invoiceType", width: "130" },
+ { label: "鐢宠鏃ユ湡", prop: "applyDate", width: "120" },
+ { label: "瀹℃牳鐘舵��", prop: "status", dataType: "slot", slot: "status", width: "110", align: "center" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "300", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const selectedRows = ref([]);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isEdit = ref(false);
+const isView = ref(false);
+const currentId = ref(null);
+
+const closeDialog = () => {
+ dialogVisible.value = false;
+ outboundSelectVisible.value = false;
+ isView.value = false;
+ isEdit.value = false;
+};
+
+const customerList = ref([]);
+const outboundBatchList = ref([]);
+const outboundBatchOptions = ref([]);
+const outboundBatchLoading = ref(false);
+const outboundSelectVisible = ref(false);
+const outboundTableRef = ref(null);
+const dialogOutboundSelection = ref([]);
+
+const getCustomerList = () => {
+ listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
+ if (res.code === 200) {
+ customerList.value = res.data?.records || [];
+ }
+ });
+};
+
+const normalizeOutboundBatchOptions = (data) => {
+ const list = Array.isArray(data) ? data : [];
+ return list.map((item, index) => {
+ if (typeof item === "string" || typeof item === "number") {
+ const text = String(item);
+ return { label: text, value: text, outboundAmount: 0 };
+ }
+ const label =
+ item.outboundBatches ??
+ item.batchNo ??
+ item.shippingNo ??
+ item.outboundNo ??
+ item.label ??
+ `鍑哄簱鍗�${index + 1}`;
+ const value = item.id ?? item.stockOutRecordId ?? item.stockOutRecordIds ?? label;
+ const outboundAmount = Number(item.outboundAmount) || 0;
+ const taxRate =
+ item.taxRate !== undefined && item.taxRate !== null && item.taxRate !== ""
+ ? Number(item.taxRate)
+ : undefined;
+ return { label: String(label), value, outboundAmount, taxRate };
+ });
+};
+
+const isSameOutboundId = (a, b) => String(a) === String(b);
+
+const getSelectedOutboundOptions = () => {
+ const selected = form.outboundBatchNos || [];
+ return outboundBatchOptions.value.filter((opt) =>
+ selected.some((id) => isSameOutboundId(id, opt.value))
+ );
+};
+
+/** 鏍¢獙鎵�閫夊嚭搴撳崟绋庣巼鏄惁涓�鑷达紝涓�鑷村垯鍥炲~ form.taxRate */
+const checkTaxRateConsistency = (showMessage = true) => {
+ const selected = getSelectedOutboundOptions();
+ if (selected.length === 0) return true;
+
+ const withTaxRate = selected.filter(
+ (opt) => opt.taxRate !== undefined && opt.taxRate !== null && !Number.isNaN(opt.taxRate)
+ );
+ if (withTaxRate.length === 0) return true;
+
+ const uniqueRates = [...new Set(withTaxRate.map((opt) => Number(opt.taxRate)))];
+ if (uniqueRates.length > 1) {
+ if (showMessage) {
+ const detail = withTaxRate.map((opt) => `${opt.label}(${opt.taxRate}%)`).join("銆�");
+ ElMessage.error(`鎵�閫夊嚭搴撳崟绋庣巼涓嶄竴鑷达紝鏃犳硶寮�绁細${detail}`);
+ }
+ return false;
+ }
+
+ form.taxRate = uniqueRates[0];
+ return true;
+};
+
+/** 鏍规嵁鎵�閫夊嚭搴撳崟姹囨�� outboundAmount 浣滀负寮�绁ㄩ噾棰� */
+const syncInvoiceAmount = () => {
+ const selected = form.outboundBatchNos || [];
+ const sum = outboundBatchOptions.value
+ .filter((opt) => selected.some((id) => isSameOutboundId(id, opt.value)))
+ .reduce((acc, opt) => acc + (Number(opt.outboundAmount) || 0), 0);
+ form.amount = sum > 0 ? Number(sum.toFixed(2)) : 0;
+};
+
+const getOutboundRowId = (row) => row?.id ?? row?.stockOutRecordId;
+
+const outboundBatchDisplayText = computed(() => {
+ if (isEdit.value || isView.value) {
+ return form.outboundBatches || "";
+ }
+ if (form.outboundBatches) return form.outboundBatches;
+ const ids = form.outboundBatchNos || [];
+ if (!ids.length) return "";
+ return outboundBatchOptions.value
+ .filter((opt) => ids.some((id) => isSameOutboundId(id, opt.value)))
+ .map((opt) => opt.label)
+ .join("銆�");
+});
+
+const handleOutboundInputClick = () => {
+ if (isEdit.value || isView.value) return;
+ openOutboundSelectDialog();
+};
+
+const restoreOutboundTableSelection = () => {
+ nextTick(() => {
+ const table = outboundTableRef.value;
+ if (!table) return;
+ table.clearSelection();
+ const selectedIds = new Set((form.outboundBatchNos || []).map((id) => String(id)));
+ outboundBatchList.value.forEach((row) => {
+ const rowId = getOutboundRowId(row);
+ if (rowId !== undefined && rowId !== null && selectedIds.has(String(rowId))) {
+ table.toggleRowSelection(row, true);
+ }
+ });
+ });
+};
+
+const openOutboundSelectDialog = () => {
+ if (!form.customerId || isEdit.value || isView.value) return;
+ outboundSelectVisible.value = true;
+ loadOutboundBatches(form.customerId, true).then(() => {
+ restoreOutboundTableSelection();
+ });
+};
+
+const handleOutboundDialogSelectionChange = (selection) => {
+ dialogOutboundSelection.value = selection;
+};
+
+const confirmOutboundSelection = () => {
+ if (dialogOutboundSelection.value.length === 0) {
+ ElMessage.warning("璇疯嚦灏戦�夋嫨涓�鏉″嚭搴撳崟");
+ return;
+ }
+ const prevIds = [...(form.outboundBatchNos || [])];
+ const prevBatches = form.outboundBatches;
+ form.outboundBatchNos = dialogOutboundSelection.value
+ .map((row) => getOutboundRowId(row))
+ .filter((id) => id !== undefined && id !== null);
+ form.outboundBatches = dialogOutboundSelection.value
+ .map((row) => row.outboundBatches ?? row.batchNo ?? row.shippingNo ?? "")
+ .filter(Boolean)
+ .join("銆�");
+ if (!checkTaxRateConsistency()) {
+ form.outboundBatchNos = prevIds;
+ form.outboundBatches = prevBatches;
+ return;
+ }
+ outboundSelectVisible.value = false;
+ syncInvoiceAmount();
+ formRef.value?.validateField("outboundBatchNos");
+};
+
+const handleOutboundDialogClosed = () => {
+ dialogOutboundSelection.value = [];
+};
+
+const loadOutboundBatches = (customerId, keepSelected = false) => {
+ if (!customerId) {
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ if (!keepSelected) {
+ form.outboundBatchNos = [];
+ form.amount = 0;
+ }
+ return Promise.resolve();
+ }
+ outboundBatchLoading.value = true;
+ return getOutboundBatchesByCustomer({ customerId })
+ .then((res) => {
+ if (res.code === 200) {
+ const list = res.data?.records ?? res.data ?? [];
+ outboundBatchList.value = Array.isArray(list) ? list : [];
+ outboundBatchOptions.value = normalizeOutboundBatchOptions(list);
+ } else {
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ }
+ })
+ .catch(() => {
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ })
+ .finally(() => {
+ outboundBatchLoading.value = false;
+ if (keepSelected) {
+ syncInvoiceAmount();
+ checkTaxRateConsistency(false);
+ }
+ });
+};
+
+const handleCustomerChange = (customerId) => {
+ form.outboundBatchNos = [];
+ form.outboundBatches = "";
+ form.amount = 0;
+ loadOutboundBatches(customerId);
+};
+
+const form = reactive({
+ applyCode: "",
+ customerId: "",
+ outboundBatchNos: [],
+ outboundBatches: "",
+ amount: 0,
+ taxRate: 13,
+ invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+ applyDate: "",
+ content: "",
+ remark: "",
+});
+
+const rules = {
+ customerId: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
+ outboundBatchNos: [{ required: true, type: "array", min: 1, message: "璇烽�夋嫨鍑哄簱鍗曞彿", trigger: "change" }],
+ amount: [{ required: true, message: "璇疯緭鍏ュ紑绁ㄩ噾棰�", trigger: "blur" }],
+ taxRate: [{ required: true, message: "璇烽�夋嫨绋庣巼", trigger: "change" }],
+ invoiceType: [{ required: true, message: "璇烽�夋嫨鍙戠エ绫诲瀷", trigger: "change" }],
+ applyDate: [{ required: true, message: "璇烽�夋嫨鐢宠鏃ユ湡", trigger: "change" }],
+};
+
+/** 瀹℃牳鐘舵�侊細0寰呭鏍� 1瀹℃牳閫氳繃 2瀹℃牳涓嶉�氳繃 */
+const STATUS_LABEL_MAP = {
+ 0: "寰呭鏍�",
+ 1: "瀹℃牳閫氳繃",
+ 2: "瀹℃牳涓嶉�氳繃",
+};
+
+const STATUS_TYPE_MAP = {
+ 0: "warning",
+ 1: "success",
+ 2: "danger",
+};
+
+const normalizeStatus = (status) => {
+ if (status === undefined || status === null || status === "") return status;
+ const num = Number(status);
+ return Number.isNaN(num) ? status : num;
+};
+
+const isPendingStatus = (status) => normalizeStatus(status) === 0;
+const isApprovedStatus = (status) => normalizeStatus(status) === 1;
+
+const fileDialogVisible = ref(false);
+const currentRecordId = ref(0);
+
+const openFileDialog = (row) => {
+ currentRecordId.value = row.id;
+ fileDialogVisible.value = true;
+};
+
+const formatOutboundBatches = (value) => {
+ if (value === undefined || value === null || value === "") return "";
+ if (Array.isArray(value)) return value.filter(Boolean).join("銆�");
+ return String(value)
+ .split(/[,锛宂/)
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .join("銆�");
+};
+
+const normalizeTableRow = (row) => ({
+ ...row,
+ applyCode: row.invoiceApplicationNo ?? row.applyCode,
+ amount: row.invoiceAmount ?? row.amount,
+ content: row.invoiceContent ?? row.content,
+ status: normalizeStatus(row.status ?? row.auditStatus),
+ stockOutRecordIds: row.stockOutRecordIds ?? row.stockOutRecordId ?? "",
+ outboundBatches: formatOutboundBatches(row.outboundBatches),
+});
+
+const appendFilterParams = (params) => {
+ if (filters.applyCode) {
+ params.invoiceApplicationNo = filters.applyCode;
+ }
+ if (filters.customerId) {
+ params.customerId = filters.customerId;
+ }
+ if (filters.status !== "" && filters.status != null) {
+ params.status = filters.status;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+};
+
+const buildListParams = () => {
+ return appendFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+};
+
+const buildExportParams = () => {
+ const params = appendFilterParams({});
+ if (selectedRows.value.length > 0) {
+ params.ids = selectedRows.value.map((row) => row.id).join(",");
+ }
+ return params;
+};
+
+const handleExport = () => {
+ const params = buildExportParams();
+ const filename =
+ selectedRows.value.length > 0
+ ? `寮�绁ㄧ敵璇穇宸查��${selectedRows.value.length}鏉${Date.now()}.xlsx`
+ : `寮�绁ㄧ敵璇穇${Date.now()}.xlsx`;
+ proxy.download("/accountInvoiceApplication/exportAccountInvoiceApplication", params, filename);
+};
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getStatusLabel = (status) => {
+ const num = normalizeStatus(status);
+ if (num === 0 || num === 1 || num === 2) {
+ return STATUS_LABEL_MAP[num];
+ }
+ return "-";
+};
+
+const getStatusType = (status) => {
+ const num = normalizeStatus(status);
+ if (num === 0 || num === 1 || num === 2) {
+ return STATUS_TYPE_MAP[num];
+ }
+ return "info";
+};
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountInvoiceApplication(buildListParams())
+ .then((res) => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = (res.data.records ?? []).map(normalizeTableRow);
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ pagination.total = 0;
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const resetFilters = () => {
+ filters.applyCode = "";
+ filters.customerId = "";
+ filters.status = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+};
+
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+const fillFormFromRow = (row) => {
+ const outboundBatchNos = Array.isArray(row.outboundBatchNos)
+ ? row.outboundBatchNos
+ : parseStockOutRecordIds(row.stockOutRecordIds ?? row.stockOutRecordId);
+ Object.assign(form, {
+ ...row,
+ applyCode: row.applyCode ?? row.invoiceApplicationNo ?? "",
+ amount: Number(row.amount ?? row.invoiceAmount ?? 0),
+ content: row.content ?? row.invoiceContent,
+ status: normalizeStatus(row.status ?? row.auditStatus),
+ outboundBatchNos,
+ outboundBatches: formatOutboundBatches(row.outboundBatches),
+ });
+};
+
+const add = () => {
+ isEdit.value = false;
+ isView.value = false;
+ dialogTitle.value = "鏂板寮�绁ㄧ敵璇�";
+ Object.assign(form, {
+ applyCode: "KP" + Date.now().toString().slice(-8),
+ customerId: "",
+ outboundBatchNos: [],
+ outboundBatches: "",
+ amount: 0,
+ taxRate: 13,
+ invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+ applyDate: new Date().toISOString().split("T")[0],
+ content: "",
+ remark: "",
+ });
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ dialogVisible.value = true;
+};
+
+const parseStockOutRecordIds = (value) => {
+ if (!value) return [];
+ if (Array.isArray(value)) return value;
+ return String(value)
+ .split(/[,锛宂/)
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .map((s) => (/^\d+$/.test(s) ? Number(s) : s));
+};
+
+const buildSubmitPayload = (forUpdate = false) => {
+ const payload = {
+ customerId: form.customerId,
+ stockOutRecordIds: (form.outboundBatchNos || []).join(","),
+ invoiceApplicationNo: form.applyCode || "",
+ invoiceType: form.invoiceType,
+ applyDate: form.applyDate,
+ invoiceContent: form.content,
+ remark: form.remark || "",
+ invoiceAmount: form.amount,
+ taxRate: form.taxRate,
+ status: 0,
+ };
+ if (forUpdate) {
+ payload.id = currentId.value;
+ }
+ return payload;
+};
+
+const edit = (row) => {
+ isEdit.value = true;
+ isView.value = false;
+ currentId.value = row.id;
+ dialogTitle.value = "缂栬緫寮�绁ㄧ敵璇�";
+ fillFormFromRow(row);
+ dialogVisible.value = true;
+ loadOutboundBatches(form.customerId, true);
+};
+
+const view = (row) => {
+ isView.value = true;
+ isEdit.value = false;
+ dialogTitle.value = "鏌ョ湅寮�绁ㄧ敵璇�";
+ fillFormFromRow(row);
+ dialogVisible.value = true;
+};
+
+const submitAudit = (row, status) => {
+ auditAccountInvoiceApplication({ id: row.id, status })
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success(status === 1 ? "瀹℃牳閫氳繃" : "瀹℃牳涓嶉�氳繃");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "瀹℃壒澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("瀹℃壒澶辫触");
+ });
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎鐢宠鍗曘��${row.applyCode ?? row.invoiceApplicationNo}銆嶅悧锛焋, "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountInvoiceApplication([row.id])
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+};
+
+const handleAudit = (row) => {
+ ElMessageBox.confirm("璇烽�夋嫨瀹℃壒缁撴灉", "寮�绁ㄧ敵璇峰鏍�", {
+ confirmButtonText: "瀹℃牳閫氳繃",
+ cancelButtonText: "瀹℃牳涓嶉�氳繃",
+ distinguishCancelAndClose: true,
+ type: "warning",
+ })
+ .then(() => {
+ submitAudit(row, 1);
+ })
+ .catch((action) => {
+ if (action === "cancel") {
+ submitAudit(row, 2);
+ }
+ });
+};
+
+const handleInvoice = (row) => {
+ ElMessageBox.confirm("纭宸插紑鍏峰彂绁紵", "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "info",
+ }).then(() => {
+ ElMessage.success("寮�绁ㄥ畬鎴�");
+ getTableData();
+ });
+};
+
+const handleBatchApply = () => {
+ ElMessage.success(`鎵归噺鐢宠 ${selectedRows.value.length} 鏉¤褰昤);
+};
+
+const submitLoading = ref(false);
+
+const submitForm = () => {
+ formRef.value.validate((valid) => {
+ if (!valid) return;
+ if (!checkTaxRateConsistency()) return;
+
+ submitLoading.value = true;
+ const request = isEdit.value
+ ? updateAccountInvoiceApplication(buildSubmitPayload(true))
+ : addAccountInvoiceApplication(buildSubmitPayload());
+
+ request
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success(isEdit.value ? "淇敼鎴愬姛" : "鏂板鎴愬姛");
+ closeDialog();
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || (isEdit.value ? "淇敼澶辫触" : "鏂板澶辫触"));
+ }
+ })
+ .catch(() => {
+ ElMessage.error(isEdit.value ? "淇敼澶辫触" : "鏂板澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+ });
+};
+
+onMounted(() => {
+ getCustomerList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.outbound-batch-input:not(.is-disabled) {
+ :deep(.el-input__wrapper) {
+ cursor: pointer;
+ }
+}
+</style>
diff --git a/src/views/financialManagement/receivable/outputInvoice.vue b/src/views/financialManagement/receivable/outputInvoice.vue
new file mode 100644
index 0000000..d746aea
--- /dev/null
+++ b/src/views/financialManagement/receivable/outputInvoice.vue
@@ -0,0 +1,608 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="鍙戠エ鍙风爜:">
+ <el-input v-model="filters.invoiceNumber" placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛:">
+ <el-select v-model="filters.customerId" placeholder="璇烽�夋嫨瀹㈡埛" clearable style="width: 200px;">
+ <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="寮�绁ㄦ棩鏈�:">
+ <el-date-picker
+ v-model="filters.dateRange"
+ type="daterange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ style="width: 240px;"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��:">
+ <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+ <el-option label="姝e父" :value="0" />
+ <el-option label="浣滃簾" :value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <!-- <el-button type="primary" @click="add" icon="Plus">褰曞叆鍙戠エ</el-button> -->
+ <el-button type="success" @click="handleExport" icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ v-loading="tableLoading"
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ >
+ <template #amount="{ row }">
+ <span class="text-primary">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ <template #taxAmount="{ row }">
+ <span class="text-danger">楼{{ formatMoney(row.taxAmount) }}</span>
+ </template>
+ <template #totalAmount="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.totalAmount) }}</span>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)" effect="light" round>
+ {{ getStatusLabel(row.status) }}
+ </el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button
+ type="primary"
+ link
+ @click="openFileDialog(row)"
+ v-if="row.accountInvoiceApplicationId"
+ >
+ 闄勪欢
+ </el-button>
+ <el-button type="warning" link @click="handleCancel(row)" v-if="isNormalStatus(row.status)">浣滃簾</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog
+ :title="dialogTitle"
+ v-model="dialogVisible"
+ width="800px"
+ :operation-type="isView ? 'detail' : ''"
+ @confirm="submitForm"
+ @cancel="closeDialog"
+ >
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+ <el-row v-if="isView" :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐘舵��">
+ <el-tag :type="getStatusType(form.status)" effect="light" round>
+ {{ getStatusLabel(form.status) }}
+ </el-tag>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ鍙风爜" prop="invoiceNo">
+ <el-input v-model="form.invoiceNo" placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�" :disabled="isView" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select v-model="form.customerId" placeholder="璇烽�夋嫨瀹㈡埛" style="width: 100%;" :disabled="isView">
+ <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="寮�绁ㄦ棩鏈�" prop="invoiceDate">
+ <el-date-picker
+ v-model="form.invoiceDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ :disabled="isView"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ绫诲瀷" prop="invoiceType">
+ <el-select
+ v-model="form.invoiceType"
+ placeholder="璇烽�夋嫨鍙戠エ绫诲瀷"
+ style="width: 100%;"
+ :disabled="isView"
+ @change="handleInvoiceTypeChange"
+ >
+ <el-option label="澧炲�肩◣涓撶敤鍙戠エ" value="澧炲�肩◣涓撶敤鍙戠エ" />
+ <el-option label="澧炲�肩◣鏅�氬彂绁�" value="澧炲�肩◣鏅�氬彂绁�" />
+ <el-option label="鐢靛瓙鍙戠エ" value="鐢靛瓙鍙戠エ" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="绋庣巼" prop="taxRate">
+ <el-select
+ v-model="form.taxRate"
+ placeholder="璇烽�夋嫨绋庣巼"
+ style="width: 100%;"
+ :disabled="isView"
+ @change="calculateTax"
+ >
+ <el-option
+ v-for="dict in tax_rate"
+ :key="dict.value"
+ :label="dict.label"
+ :value="Number(dict.value)"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="閲戦(涓嶅惈绋�)" prop="amount">
+ <el-input-number
+ v-model="form.amount"
+ :min="0"
+ :precision="2"
+ style="width: 100%;"
+ :disabled="isView"
+ @change="calculateTax"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="绋庨">
+ <el-input v-model="form.taxAmount" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浠风◣鍚堣">
+ <el-input v-model="form.totalAmount" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鍙戠エ鍐呭" prop="content">
+ <el-input v-model="form.content" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ彂绁ㄥ唴瀹�" :disabled="isView" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" :disabled="isView" />
+ </el-form-item>
+ </el-form>
+ <template v-if="!isView" #footer>
+ <el-button type="primary" :loading="submitLoading" @click="submitForm">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+
+ <FileList
+ v-if="fileDialogVisible"
+ v-model:visible="fileDialogVisible"
+ record-type="account_invoice_application"
+ :record-id="currentRecordId"
+ :editable="false"
+ />
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance, defineAsyncComponent } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { listCustomer } from "@/api/basicData/customer.js";
+import {
+ addAccountSalesInvoice,
+ listPageAccountSalesInvoice,
+ cancelAccountSalesInvoice,
+ deleteAccountSalesInvoice,
+} from "@/api/financialManagement/accountSalesInvoice.js";
+
+const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
+
+defineOptions({
+ name: "閿�椤瑰彂绁�",
+});
+
+const { proxy } = getCurrentInstance();
+const { tax_rate } = proxy.useDict("tax_rate");
+
+const filters = reactive({
+ invoiceNumber: "",
+ customerId: "",
+ dateRange: [],
+ status: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "鍙戠エ鍙风爜", prop: "invoiceNo", width: "140" },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", width: "180" },
+ { label: "寮�绁ㄦ棩鏈�", prop: "invoiceDate", width: "120" },
+ { label: "閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+ { label: "绋庨", prop: "taxAmount", dataType: "slot", slot: "taxAmount" },
+ { label: "浠风◣鍚堣", prop: "totalAmount", dataType: "slot", slot: "totalAmount" },
+ { label: "鍙戠エ绫诲瀷", prop: "invoiceType", width: "130" },
+ { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status", width: "90", align: "center" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "260", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isView = ref(false);
+const submitLoading = ref(false);
+
+const customerList = ref([]);
+const fileDialogVisible = ref(false);
+const currentRecordId = ref(0);
+
+const openFileDialog = (row) => {
+ if (!row.accountInvoiceApplicationId) {
+ ElMessage.warning("鏈叧鑱斿紑绁ㄧ敵璇凤紝鏃犳硶鏌ョ湅闄勪欢");
+ return;
+ }
+ currentRecordId.value = row.accountInvoiceApplicationId;
+ fileDialogVisible.value = true;
+};
+
+/** 鐘舵�侊細0姝e父 1浣滃簾 */
+const STATUS_LABEL_MAP = { 0: "姝e父", 1: "浣滃簾" };
+const STATUS_TYPE_MAP = { 0: "success", 1: "info" };
+
+const normalizeStatus = (status) => {
+ if (status === undefined || status === null || status === "") return 0;
+ const num = Number(status);
+ return Number.isNaN(num) ? 0 : num;
+};
+
+const isNormalStatus = (status) => normalizeStatus(status) === 0;
+
+const getStatusLabel = (status) => {
+ const num = normalizeStatus(status);
+ return STATUS_LABEL_MAP[num] ?? "姝e父";
+};
+
+const getStatusType = (status) => {
+ const num = normalizeStatus(status);
+ return STATUS_TYPE_MAP[num] ?? "success";
+};
+
+const form = reactive({
+ invoiceNo: "",
+ customerId: "",
+ invoiceDate: "",
+ invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+ taxRate: 13,
+ amount: 0,
+ taxAmount: 0,
+ totalAmount: 0,
+ content: "",
+ remark: "",
+ accountInvoiceApplicationId: undefined,
+ storageAttachmentId: undefined,
+});
+
+const rules = {
+ invoiceNo: [{ required: true, message: "璇疯緭鍏ュ彂绁ㄥ彿鐮�", trigger: "blur" }],
+ customerId: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
+ invoiceDate: [{ required: true, message: "璇烽�夋嫨寮�绁ㄦ棩鏈�", trigger: "change" }],
+ invoiceType: [{ required: true, message: "璇烽�夋嫨鍙戠エ绫诲瀷", trigger: "change" }],
+ taxRate: [{ required: true, message: "璇烽�夋嫨绋庣巼", trigger: "change" }],
+ amount: [{ required: true, message: "璇疯緭鍏ラ噾棰�", trigger: "blur" }],
+};
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const calculateTax = () => {
+ form.taxAmount = Number((form.amount * form.taxRate / 100).toFixed(2));
+ form.totalAmount = Number((form.amount + form.taxAmount).toFixed(2));
+};
+
+const handleInvoiceTypeChange = () => {
+ calculateTax();
+};
+
+const normalizeTableRow = (row) => ({
+ ...row,
+ invoiceNo: row.invoiceNumber ?? row.invoiceNo,
+ invoiceDate: row.issueDate ?? row.invoiceDate,
+ amount: row.taxExclusivelPrice ?? row.amount,
+ taxAmount: row.taxPrice ?? row.taxAmount,
+ totalAmount: row.taxInclusivePrice ?? row.totalAmount,
+ content: row.invoiceContent ?? row.content,
+ status: normalizeStatus(row.status),
+});
+
+const fillFormFromRow = (row) => {
+ Object.assign(form, {
+ invoiceNo: row.invoiceNo ?? row.invoiceNumber ?? "",
+ customerId: row.customerId,
+ invoiceDate: row.invoiceDate ?? row.issueDate ?? "",
+ invoiceType: row.invoiceType ?? "澧炲�肩◣涓撶敤鍙戠エ",
+ taxRate: row.taxRate ?? 13,
+ amount: row.amount ?? row.taxExclusivelPrice ?? 0,
+ taxAmount: row.taxAmount ?? row.taxPrice ?? 0,
+ totalAmount: row.totalAmount ?? row.taxInclusivePrice ?? 0,
+ content: row.content ?? row.invoiceContent ?? "",
+ remark: row.remark ?? "",
+ accountInvoiceApplicationId: row.accountInvoiceApplicationId,
+ storageAttachmentId: row.storageAttachmentId,
+ status: normalizeStatus(row.status),
+ });
+};
+
+const buildCancelPayload = (row) => ({
+ id: row.id,
+ accountInvoiceApplicationId: row.accountInvoiceApplicationId,
+ invoiceNumber: row.invoiceNumber ?? row.invoiceNo,
+ taxRate: row.taxRate,
+ invoiceType: row.invoiceType,
+ issueDate: row.issueDate ?? row.invoiceDate,
+ taxExclusivelPrice: row.taxExclusivelPrice ?? row.amount,
+ taxPrice: row.taxPrice ?? row.taxAmount,
+ taxInclusivePrice: row.taxInclusivePrice ?? row.totalAmount,
+ remark: row.remark ?? "",
+ invoiceContent: row.invoiceContent ?? row.content,
+ customerId: row.customerId,
+ storageAttachmentId: row.storageAttachmentId,
+ status: 1,
+});
+
+const buildSubmitPayload = () => ({
+ invoiceNumber: form.invoiceNo,
+ customerId: form.customerId,
+ issueDate: form.invoiceDate,
+ invoiceType: form.invoiceType,
+ taxRate: form.taxRate,
+ taxExclusivelPrice: form.amount,
+ taxPrice: form.taxAmount,
+ taxInclusivePrice: form.totalAmount,
+ invoiceContent: form.content,
+ remark: form.remark || "",
+ accountInvoiceApplicationId: form.accountInvoiceApplicationId,
+ storageAttachmentId: form.storageAttachmentId,
+});
+
+const getCustomerList = () => {
+ listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
+ if (res.code === 200) {
+ customerList.value = res.data?.records || [];
+ }
+ });
+};
+
+const appendFilterParams = (params) => {
+ if (filters.invoiceNumber) {
+ params.invoiceNumber = filters.invoiceNumber;
+ }
+ if (filters.customerId) {
+ params.customerId = filters.customerId;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ if (filters.status !== "" && filters.status != null) {
+ params.status = filters.status;
+ }
+ return params;
+};
+
+const buildListParams = () =>
+ appendFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+const buildExportParams = () => appendFilterParams({});
+
+const handleExport = () => {
+ const params = buildExportParams();
+ proxy.download("/accountSalesInvoice/exportAccountSalesInvoice", params, `閿�椤瑰彂绁╛${Date.now()}.xlsx`);
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountSalesInvoice(buildListParams())
+ .then((res) => {
+ if (res.code === 200) {
+ const records = res.data?.records ?? [];
+ dataList.value = records.map(normalizeTableRow);
+ pagination.total = res.data?.total ?? 0;
+ } else {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const resetFilters = () => {
+ filters.invoiceNumber = "";
+ filters.customerId = "";
+ filters.dateRange = [];
+ filters.status = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+};
+
+const closeDialog = () => {
+ dialogVisible.value = false;
+ isView.value = false;
+};
+
+const add = () => {
+ isView.value = false;
+ dialogTitle.value = "褰曞叆鍙戠エ";
+ Object.assign(form, {
+ invoiceNo: "",
+ customerId: "",
+ invoiceDate: new Date().toISOString().split("T")[0],
+ invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+ taxRate: 13,
+ amount: 0,
+ taxAmount: 0,
+ totalAmount: 0,
+ content: "",
+ remark: "",
+ accountInvoiceApplicationId: undefined,
+ storageAttachmentId: undefined,
+ });
+ dialogVisible.value = true;
+};
+
+const view = (row) => {
+ isView.value = true;
+ dialogTitle.value = "鏌ョ湅鍙戠エ";
+ fillFormFromRow(row);
+ dialogVisible.value = true;
+};
+
+const handleCancel = (row) => {
+ ElMessageBox.confirm(`纭浣滃簾鍙戠エ銆�${row.invoiceNo ?? row.invoiceNumber}銆嶅悧锛焋, "浣滃簾纭", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ cancelAccountSalesInvoice(buildCancelPayload(row))
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("浣滃簾鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "浣滃簾澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("浣滃簾澶辫触");
+ });
+ });
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎鍙戠エ銆�${row.invoiceNo ?? row.invoiceNumber}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙, "鍒犻櫎纭", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountSalesInvoice([row.id])
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+};
+
+const submitForm = () => {
+ formRef.value.validate((valid) => {
+ if (!valid) return;
+ submitLoading.value = true;
+ addAccountSalesInvoice(buildSubmitPayload())
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("褰曞叆鎴愬姛");
+ closeDialog();
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "褰曞叆澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("褰曞叆澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+ });
+};
+
+onMounted(() => {
+ getCustomerList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+</style>
diff --git a/src/views/financialManagement/receivable/receipt.vue b/src/views/financialManagement/receivable/receipt.vue
new file mode 100644
index 0000000..6ddb3fe
--- /dev/null
+++ b/src/views/financialManagement/receivable/receipt.vue
@@ -0,0 +1,855 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters"
+ :inline="true">
+ <el-form-item label="鏀舵鍗曞彿:">
+ <el-input v-model="filters.collectionNumber"
+ placeholder="璇疯緭鍏ユ敹娆惧崟鍙�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛:">
+ <el-select v-model="filters.customerId"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ clearable
+ filterable
+ style="width: 200px;">
+ <el-option v-for="item in customerList"
+ :key="item.id"
+ :label="item.customerName"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏀舵鏂瑰紡:">
+ <el-select v-model="filters.collectionMethod"
+ placeholder="璇烽�夋嫨鏀舵鏂瑰紡"
+ clearable
+ style="width: 150px;">
+ <el-option v-for="item in payment_methods"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏀舵鏃ユ湡:">
+ <el-date-picker v-model="filters.dateRange"
+ type="daterange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ style="width: 240px;" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-statistic title="鏈〉鏀舵鍚堣"
+ :value="totalReceiptAmount"
+ :precision="2"
+ prefix="楼" />
+ </div>
+ <div>
+ <el-button type="primary"
+ @click="add"
+ icon="Plus">鏂板鏀舵</el-button>
+ <el-button type="success"
+ @click="handleExport"
+ icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable rowKey="id"
+ v-loading="tableLoading"
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage">
+ <template #amount="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ <template #receiptMethod="{ row }">
+ <span>{{ getReceiptMethodLabel(row.receiptMethod) }}</span>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary"
+ link
+ @click="view(row)">鏌ョ湅</el-button>
+ <el-button :disabled="row.accountStatemen"
+ type="primary"
+ link
+ @click="edit(row)">缂栬緫</el-button>
+ <el-button :disabled="row.accountStatemen"
+ type="danger"
+ link
+ @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+ <FormDialog :title="dialogTitle"
+ v-model="dialogVisible"
+ width="800px"
+ :operation-type="isView ? 'detail' : ''"
+ @confirm="submitForm"
+ @cancel="closeDialog">
+ <el-form :model="form"
+ :rules="rules"
+ ref="formRef"
+ label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="鏀舵鍗曞彿"
+ prop="receiptCode">
+ <el-input v-model="form.receiptCode"
+ placeholder="绯荤粺鑷姩鐢熸垚"
+ disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛"
+ prop="customerId">
+ <el-select v-model="form.customerId"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ style="width: 100%;"
+ :disabled="isEdit || isView"
+ filterable
+ @change="handleCustomerChange">
+ <el-option v-for="item in customerList"
+ :key="item.id"
+ :label="item.customerName"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍏宠仈鍗曟嵁"
+ prop="stockOutRecordIds">
+ <el-input :model-value="outboundBatchDisplayText"
+ placeholder="璇峰厛閫夋嫨瀹㈡埛"
+ readonly
+ :disabled="!form.customerId || isEdit || isView"
+ class="outbound-batch-input"
+ @click="handleOutboundInputClick">
+ <template v-if="!isEdit && !isView"
+ #append>
+ <el-button :disabled="!form.customerId"
+ :loading="outboundBatchLoading"
+ @click.stop="openOutboundSelectDialog">
+ 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏀舵鏃ユ湡"
+ prop="receiptDate">
+ <el-date-picker v-model="form.receiptDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ :disabled="isView" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏀舵閲戦"
+ prop="amount">
+ <el-input-number v-model="form.amount"
+ :min="0"
+ :precision="2"
+ style="width: 100%;"
+ :disabled="isView"
+ placeholder="鏍规嵁鍏宠仈鍗曟嵁鑷姩姹囨�伙紝鍙慨鏀�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏀舵鏂瑰紡"
+ prop="receiptMethod">
+ <el-select v-model="form.receiptMethod"
+ placeholder="璇烽�夋嫨鏀舵鏂瑰紡"
+ style="width: 100%;"
+ :disabled="isView">
+ <el-option v-for="item in payment_methods"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="澶囨敞"
+ prop="remark">
+ <el-input v-model="form.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ娉�"
+ :disabled="isView" />
+ </el-form-item>
+ </el-form>
+ <template v-if="!isView"
+ #footer>
+ <el-button type="primary"
+ :loading="submitLoading"
+ @click="submitForm">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+ <el-dialog v-model="outboundSelectVisible"
+ title="閫夋嫨鍏宠仈鍗曟嵁"
+ width="1200px"
+ append-to-body
+ destroy-on-close
+ :close-on-click-modal="false"
+ @closed="handleOutboundDialogClosed">
+ <el-table ref="outboundTableRef"
+ v-loading="outboundBatchLoading"
+ :data="outboundBatchList"
+ row-key="id"
+ border
+ stripe
+ max-height="480"
+ @selection-change="handleOutboundDialogSelectionChange">
+ <el-table-column type="selection"
+ width="55"
+ align="center" />
+ <el-table-column prop="outboundBatches"
+ label="鍑哄簱鍗曞彿"
+ min-width="140"
+ show-overflow-tooltip />
+ <el-table-column prop="customerName"
+ label="瀹㈡埛鍚嶇О"
+ min-width="120"
+ show-overflow-tooltip />
+ <el-table-column prop="productName"
+ label="浜у搧鍚嶇О"
+ min-width="120"
+ show-overflow-tooltip />
+ <el-table-column prop="specificationModel"
+ label="瑙勬牸鍨嬪彿"
+ min-width="140"
+ show-overflow-tooltip />
+ <el-table-column prop="salesContractNo"
+ label="閿�鍞悎鍚屽彿"
+ min-width="140"
+ show-overflow-tooltip />
+ <el-table-column prop="shippingNo"
+ label="鍙戣揣鍗曞彿"
+ min-width="130"
+ show-overflow-tooltip />
+ <el-table-column prop="shippingDate"
+ label="鍙戣揣鏃ユ湡"
+ width="110"
+ align="center" />
+ <el-table-column prop="outboundAmount"
+ label="鍑哄簱閲戦"
+ width="110"
+ align="right">
+ <template #default="{ row }">楼{{ formatMoney(row.outboundAmount) }}</template>
+ </el-table-column>
+ <el-table-column prop="taxRate"
+ label="绋庣巼"
+ width="80"
+ align="center">
+ <template #default="{ row }">{{ row.taxRate }}%</template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <el-button type="primary"
+ @click="confirmOutboundSelection">纭畾</el-button>
+ <el-button @click="outboundSelectVisible = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import {
+ ref,
+ reactive,
+ computed,
+ onMounted,
+ nextTick,
+ getCurrentInstance,
+ } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import FormDialog from "@/components/Dialog/FormDialog.vue";
+ import { listCustomer } from "@/api/basicData/customer.js";
+ import {
+ getOutboundBatchesByCustomer,
+ addAccountSalesCollection,
+ listPageAccountSalesCollection,
+ updateAccountSalesCollection,
+ deleteAccountSalesCollection,
+ } from "@/api/financialManagement/accountSalesCollection.js";
+
+ defineOptions({
+ name: "鏀舵鍗�",
+ });
+
+ const { proxy } = getCurrentInstance();
+ const { payment_methods } = proxy.useDict("payment_methods");
+
+ const filters = reactive({
+ collectionNumber: "",
+ customerId: "",
+ collectionMethod: "",
+ dateRange: [],
+ });
+
+ const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+ });
+
+ const columns = [
+ { label: "鏀舵鍗曞彿", prop: "receiptCode", width: "150" },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", width: "180" },
+ { label: "鏀舵鏃ユ湡", prop: "receiptDate", width: "120" },
+ { label: "鏀舵閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+ {
+ label: "鏀舵鏂瑰紡",
+ prop: "receiptMethod",
+ dataType: "slot",
+ slot: "receiptMethod",
+ width: "120",
+ },
+ { label: "澶囨敞", prop: "remark", showOverflowTooltip: true },
+ {
+ label: "鎿嶄綔",
+ prop: "operation",
+ dataType: "slot",
+ slot: "operation",
+ width: "200",
+ fixed: "right",
+ },
+ ];
+
+ const dataList = ref([]);
+ const tableLoading = ref(false);
+ const dialogVisible = ref(false);
+ const dialogTitle = ref("");
+ const formRef = ref(null);
+ const isEdit = ref(false);
+ const isView = ref(false);
+ const currentId = ref(null);
+ const submitLoading = ref(false);
+
+ const customerList = ref([]);
+ const outboundBatchList = ref([]);
+ const outboundBatchOptions = ref([]);
+ const outboundBatchLoading = ref(false);
+ const outboundSelectVisible = ref(false);
+ const outboundTableRef = ref(null);
+ const dialogOutboundSelection = ref([]);
+
+ const getReceiptMethodLabel = value => {
+ if (value === undefined || value === null || value === "") return "-";
+ const item = payment_methods.value?.find(
+ m => String(m.value) === String(value)
+ );
+ return item?.label ?? value;
+ };
+
+ const getDefaultReceiptMethod = () => payment_methods.value?.[0]?.value ?? "";
+
+ const form = reactive({
+ receiptCode: "",
+ customerId: "",
+ receiptDate: "",
+ amount: 0,
+ receiptMethod: "",
+ stockOutRecordIds: [],
+ outboundBatches: "",
+ remark: "",
+ });
+
+ const rules = {
+ customerId: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
+ stockOutRecordIds: [
+ {
+ required: true,
+ type: "array",
+ min: 1,
+ message: "璇烽�夋嫨鍏宠仈鍗曟嵁",
+ trigger: "change",
+ },
+ ],
+ receiptDate: [
+ { required: true, message: "璇烽�夋嫨鏀舵鏃ユ湡", trigger: "change" },
+ ],
+ amount: [{ required: true, message: "璇疯緭鍏ユ敹娆鹃噾棰�", trigger: "blur" }],
+ receiptMethod: [
+ { required: true, message: "璇烽�夋嫨鏀舵鏂瑰紡", trigger: "change" },
+ ],
+ };
+
+ const totalReceiptAmount = computed(() =>
+ dataList.value.reduce((sum, item) => sum + Number(item.amount || 0), 0)
+ );
+
+ const formatMoney = value => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value)
+ .toFixed(2)
+ .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+ };
+
+ const parseStockOutRecordIds = value => {
+ if (!value) return [];
+ if (Array.isArray(value)) return value;
+ return String(value)
+ .split(/[,锛宂/)
+ .map(s => s.trim())
+ .filter(Boolean)
+ .map(s => (/^\d+$/.test(s) ? Number(s) : s));
+ };
+
+ const formatOutboundBatches = value => {
+ if (value === undefined || value === null || value === "") return "";
+ if (Array.isArray(value)) return value.filter(Boolean).join("銆�");
+ return String(value)
+ .split(/[,锛宂/)
+ .map(s => s.trim())
+ .filter(Boolean)
+ .join("銆�");
+ };
+
+ const normalizeTableRow = row => ({
+ ...row,
+ receiptCode: row.collectionNumber ?? row.receiptCode,
+ receiptDate: row.collectionDate ?? row.receiptDate,
+ amount: row.collectionAmount ?? row.amount,
+ receiptMethod: row.collectionMethod ?? row.receiptMethod ?? "",
+ stockOutRecordIds: row.stockOutRecordIds ?? row.stockOutRecordId ?? "",
+ outboundBatches: formatOutboundBatches(row.outboundBatches),
+ });
+
+ const getCustomerList = () => {
+ listCustomer({ current: -1, size: -1, type: 0 }).then(res => {
+ if (res.code === 200) {
+ customerList.value = res.data?.records || [];
+ }
+ });
+ };
+
+ const normalizeOutboundBatchOptions = data => {
+ const list = Array.isArray(data) ? data : [];
+ return list.map((item, index) => {
+ if (typeof item === "string" || typeof item === "number") {
+ const text = String(item);
+ return { label: text, value: text, outboundAmount: 0 };
+ }
+ const label =
+ item.outboundBatches ??
+ item.batchNo ??
+ item.shippingNo ??
+ item.outboundNo ??
+ item.label ??
+ `鍑哄簱鍗�${index + 1}`;
+ const value = item.id ?? item.stockOutRecordId ?? label;
+ return {
+ label: String(label),
+ value,
+ outboundAmount: Number(item.outboundAmount) || 0,
+ };
+ });
+ };
+
+ const isSameOutboundId = (a, b) => String(a) === String(b);
+
+ const getOutboundRowId = row => row?.id ?? row?.stockOutRecordId;
+
+ const outboundBatchDisplayText = computed(() => {
+ if (isEdit.value || isView.value) {
+ return form.outboundBatches || "";
+ }
+ if (form.outboundBatches) return form.outboundBatches;
+ const ids = form.stockOutRecordIds || [];
+ if (!ids.length) return "";
+ const labels = outboundBatchOptions.value
+ .filter(opt => ids.some(id => isSameOutboundId(id, opt.value)))
+ .map(opt => opt.label);
+ if (labels.length) return labels.join("銆�");
+ return ids.join("銆�");
+ });
+
+ const handleOutboundInputClick = () => {
+ if (isEdit.value || isView.value) return;
+ openOutboundSelectDialog();
+ };
+
+ /** 涓哄凡閫� ID 琛ュ叏閫夐」锛堢紪杈�/鏌ョ湅鍥炴樉锛� */
+ const ensureOutboundOptionsForSelected = () => {
+ const ids = form.stockOutRecordIds || [];
+ ids.forEach(id => {
+ const exists = outboundBatchOptions.value.some(opt =>
+ isSameOutboundId(opt.value, id)
+ );
+ if (exists) return;
+ const fromList = outboundBatchList.value.find(row =>
+ isSameOutboundId(getOutboundRowId(row), id)
+ );
+ if (fromList) {
+ const [option] = normalizeOutboundBatchOptions([fromList]);
+ if (option) outboundBatchOptions.value.push(option);
+ return;
+ }
+ outboundBatchOptions.value.push({
+ label: String(id),
+ value: id,
+ outboundAmount: 0,
+ });
+ });
+ };
+
+ const syncCollectionAmount = () => {
+ const selected = form.stockOutRecordIds || [];
+ const sum = outboundBatchOptions.value
+ .filter(opt => selected.some(id => isSameOutboundId(id, opt.value)))
+ .reduce((acc, opt) => acc + (Number(opt.outboundAmount) || 0), 0);
+ form.amount = sum > 0 ? Number(sum.toFixed(2)) : 0;
+ };
+
+ const restoreOutboundTableSelection = () => {
+ nextTick(() => {
+ const table = outboundTableRef.value;
+ if (!table) return;
+ table.clearSelection();
+ const selectedIds = new Set(
+ (form.stockOutRecordIds || []).map(id => String(id))
+ );
+ outboundBatchList.value.forEach(row => {
+ const rowId = getOutboundRowId(row);
+ if (
+ rowId !== undefined &&
+ rowId !== null &&
+ selectedIds.has(String(rowId))
+ ) {
+ table.toggleRowSelection(row, true);
+ }
+ });
+ });
+ };
+
+ const loadOutboundBatches = (customerId, keepSelected = false) => {
+ if (!customerId) {
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ if (!keepSelected) {
+ form.stockOutRecordIds = [];
+ form.amount = 0;
+ }
+ return Promise.resolve();
+ }
+ outboundBatchLoading.value = true;
+ return getOutboundBatchesByCustomer({ customerId })
+ .then(res => {
+ if (res.code === 200) {
+ const list = res.data?.records ?? res.data ?? [];
+ outboundBatchList.value = Array.isArray(list) ? list : [];
+ outboundBatchOptions.value = normalizeOutboundBatchOptions(list);
+ } else {
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ }
+ })
+ .catch(() => {
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ })
+ .finally(() => {
+ outboundBatchLoading.value = false;
+ if (keepSelected) {
+ ensureOutboundOptionsForSelected();
+ restoreOutboundTableSelection();
+ }
+ });
+ };
+
+ const handleCustomerChange = customerId => {
+ form.stockOutRecordIds = [];
+ form.outboundBatches = "";
+ form.amount = 0;
+ loadOutboundBatches(customerId);
+ };
+
+ const openOutboundSelectDialog = () => {
+ if (!form.customerId || isEdit.value || isView.value) return;
+ outboundSelectVisible.value = true;
+ loadOutboundBatches(form.customerId, true).then(() => {
+ restoreOutboundTableSelection();
+ });
+ };
+
+ const handleOutboundDialogSelectionChange = selection => {
+ dialogOutboundSelection.value = selection;
+ };
+
+ const confirmOutboundSelection = () => {
+ if (dialogOutboundSelection.value.length === 0) {
+ ElMessage.warning("璇疯嚦灏戦�夋嫨涓�鏉″叧鑱斿崟鎹�");
+ return;
+ }
+ form.stockOutRecordIds = dialogOutboundSelection.value
+ .map(row => getOutboundRowId(row))
+ .filter(id => id !== undefined && id !== null);
+ form.outboundBatches = dialogOutboundSelection.value
+ .map(row => row.outboundBatches ?? row.batchNo ?? row.shippingNo ?? "")
+ .filter(Boolean)
+ .join("銆�");
+ outboundSelectVisible.value = false;
+ syncCollectionAmount();
+ formRef.value?.validateField("stockOutRecordIds");
+ };
+
+ const handleOutboundDialogClosed = () => {
+ dialogOutboundSelection.value = [];
+ };
+
+ const appendFilterParams = params => {
+ if (filters.collectionNumber) {
+ params.collectionNumber = filters.collectionNumber;
+ }
+ if (filters.customerId) {
+ params.customerId = filters.customerId;
+ }
+ if (filters.collectionMethod) {
+ params.collectionMethod = filters.collectionMethod;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+ };
+
+ const buildListParams = () =>
+ appendFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+ const buildExportParams = () => appendFilterParams({});
+
+ const buildSubmitPayload = (forUpdate = false) => {
+ const payload = {
+ customerId: form.customerId,
+ collectionDate: form.receiptDate,
+ collectionAmount: form.amount,
+ collectionMethod: form.receiptMethod,
+ collectionNumber: form.receiptCode || "",
+ remark: form.remark || "",
+ stockOutRecordIds: (form.stockOutRecordIds || []).join(","),
+ };
+ if (forUpdate) {
+ payload.id = currentId.value;
+ }
+ return payload;
+ };
+
+ const fillFormFromRow = row => {
+ const stockOutRecordIds = parseStockOutRecordIds(
+ row.stockOutRecordIds ?? row.stockOutRecordId
+ );
+ Object.assign(form, {
+ receiptCode: row.receiptCode ?? row.collectionNumber ?? "",
+ customerId: row.customerId,
+ receiptDate: row.receiptDate ?? row.collectionDate ?? "",
+ amount: Number(row.amount ?? row.collectionAmount ?? 0),
+ receiptMethod: row.receiptMethod ?? row.collectionMethod ?? "",
+ stockOutRecordIds,
+ outboundBatches: formatOutboundBatches(row.outboundBatches),
+ remark: row.remark ?? "",
+ });
+ };
+
+ const closeDialog = () => {
+ dialogVisible.value = false;
+ outboundSelectVisible.value = false;
+ isView.value = false;
+ isEdit.value = false;
+ };
+
+ const handleExport = () => {
+ const params = buildExportParams();
+ proxy.download(
+ "/accountSalesCollection/exportAccountSalesCollection",
+ params,
+ `鏀舵鍗昣${Date.now()}.xlsx`
+ );
+ };
+
+ const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountSalesCollection(buildListParams())
+ .then(res => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = (res.data.records ?? []).map(normalizeTableRow);
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ pagination.total = 0;
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const resetFilters = () => {
+ filters.collectionNumber = "";
+ filters.customerId = "";
+ filters.collectionMethod = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+ };
+
+ const add = () => {
+ isEdit.value = false;
+ isView.value = false;
+ dialogTitle.value = "鏂板鏀舵";
+ Object.assign(form, {
+ receiptCode: "SK" + Date.now().toString().slice(-8),
+ customerId: "",
+ receiptDate: new Date().toISOString().split("T")[0],
+ amount: 0,
+ receiptMethod: getDefaultReceiptMethod(),
+ stockOutRecordIds: [],
+ outboundBatches: "",
+ remark: "",
+ });
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ dialogVisible.value = true;
+ };
+
+ const edit = row => {
+ isEdit.value = true;
+ isView.value = false;
+ currentId.value = row.id;
+ dialogTitle.value = "缂栬緫鏀舵";
+ fillFormFromRow(row);
+ dialogVisible.value = true;
+ };
+
+ const view = row => {
+ isView.value = true;
+ isEdit.value = false;
+ dialogTitle.value = "鏌ョ湅鏀舵";
+ fillFormFromRow(row);
+ dialogVisible.value = true;
+ };
+
+ const handleDelete = row => {
+ ElMessageBox.confirm(
+ `纭鍒犻櫎鏀舵鍗曘��${row.receiptCode ?? row.collectionNumber}銆嶅悧锛焋,
+ "鎻愮ず",
+ {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ ).then(() => {
+ deleteAccountSalesCollection([row.id])
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+ };
+
+ const submitForm = () => {
+ formRef.value.validate(valid => {
+ if (!valid) return;
+ submitLoading.value = true;
+ const request = isEdit.value
+ ? updateAccountSalesCollection(buildSubmitPayload(true))
+ : addAccountSalesCollection(buildSubmitPayload());
+ request
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success(isEdit.value ? "淇敼鎴愬姛" : "鏂板鎴愬姛");
+ closeDialog();
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || (isEdit.value ? "淇敼澶辫触" : "鏂板澶辫触"));
+ }
+ })
+ .catch(() => {
+ ElMessage.error(isEdit.value ? "淇敼澶辫触" : "鏂板澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+ });
+ };
+
+ onMounted(() => {
+ getCustomerList();
+ getTableData();
+ });
+</script>
+
+<style lang="scss" scoped>
+ .actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ }
+
+ .text-success {
+ color: #67c23a;
+ font-weight: bold;
+ }
+
+ .outbound-batch-input:not(.is-disabled) {
+ :deep(.el-input__wrapper) {
+ cursor: pointer;
+ }
+ }
+</style>
diff --git a/src/views/financialManagement/receivable/reconciliation.vue b/src/views/financialManagement/receivable/reconciliation.vue
new file mode 100644
index 0000000..b1bff0e
--- /dev/null
+++ b/src/views/financialManagement/receivable/reconciliation.vue
@@ -0,0 +1,738 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="瀹㈡埛:">
+ <el-select v-model="filters.customerId" placeholder="璇烽�夋嫨瀹㈡埛" clearable filterable style="width: 200px;">
+ <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀵硅处鏈熼棿:">
+ <el-date-picker v-model="filters.startMonth" type="month" placeholder="寮�濮嬫湀浠�" value-format="YYYY-MM" style="width: 140px;" />
+ <span style="margin: 0 10px;">鑷�</span>
+ <el-date-picker v-model="filters.endMonth" type="month" placeholder="缁撴潫鏈堜唤" value-format="YYYY-MM" style="width: 140px;" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-button type="primary" @click="generateStatement" icon="Document">鐢熸垚瀵硅处鍗�</el-button>
+ </div>
+ <div>
+ <el-button @click="handleOut" icon="Download">瀵煎嚭瀵硅处鍗�</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ >
+ <template #openingBalance="{ row }">
+ <span :class="row.openingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.openingBalance) }}</span>
+ </template>
+ <template #currentPlan="{ row }">
+ <span class="text-primary">楼{{ formatMoney(row.currentPlan) }}</span>
+ </template>
+ <template #currentActually="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.currentActually) }}</span>
+ </template>
+ <template #closingBalance="{ row }">
+ <span :class="row.closingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.closingBalance) }}</span>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="viewDetail(row)">鏌ョ湅鏄庣粏</el-button>
+ <!-- <el-button type="primary" link @click="printStatement(row)">鎵撳嵃</el-button> -->
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog title="瀵硅处鏄庣粏" v-model="detailDialogVisible" width="900px" @confirm="printDetail" @cancel="detailDialogVisible = false" operationType="detail">
+ <div class="statement-header">
+ <h3>{{ currentCustomer }} 搴旀敹瀵硅处鍗�</h3>
+ <p>瀵硅处鏈熼棿: {{ currentPeriod }}</p>
+ </div>
+ <el-table :data="detailData" border style="width: 100%" v-loading="detailLoading">
+ <el-table-column prop="date" label="鏃ユ湡" width="120" />
+ <el-table-column prop="type" label="绫诲瀷" width="100">
+ <template #default="{ row }">
+ <el-tag :type="row.type === '鍑哄簱' ? 'success' : row.type === '閫�璐�' ? 'danger' : 'primary'">{{ row.type }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="code" label="鍗曟嵁缂栧彿" width="150" />
+ <el-table-column prop="debit" label="鍊熸柟(搴旀敹)" width="120">
+ <template #default="{ row }">
+ <span v-if="row.debit > 0" class="text-danger">楼{{ formatMoney(row.debit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="credit" label="璐锋柟(鏀舵)" width="120">
+ <template #default="{ row }">
+ <span v-if="row.credit > 0" class="text-success">楼{{ formatMoney(row.credit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="balance" label="浣欓" width="120">
+ <template #default="{ row }">
+ <span :class="row.balance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.balance) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="remark" label="澶囨敞" show-overflow-tooltip />
+ </el-table>
+ <template #footer>
+ <el-button type="primary" @click="printDetail">鎵撳嵃</el-button>
+ <el-button @click="detailDialogVisible = false">鍏抽棴</el-button>
+ </template>
+ </FormDialog>
+
+ <FormDialog title="鐢熸垚瀵硅处鍗�" v-model="generateDialogVisible" width="1000px" @confirm="confirmGenerate" @cancel="generateDialogVisible = false">
+ <el-form :model="generateForm" label-width="100px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="閫夋嫨瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="generateForm.customerId"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ style="width: 100%;"
+ filterable
+ @change="onCustomerChange"
+ >
+ <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀵硅处鏈堜唤" prop="statementMonth">
+ <el-date-picker v-model="generateForm.statementMonth" type="month" placeholder="閫夋嫨鏈堜唤" value-format="YYYY-MM" style="width: 100%;" @change="onStatementMonthChange" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+
+ <div v-if="statementDetailLoaded" class="sales-section">
+ <div v-if="salesData.length > 0" class="section-title">鏈湀閿�鍞暟鎹�</div>
+ <el-table
+ v-if="salesData.length > 0"
+ ref="salesTableRef"
+ :data="salesData"
+ border
+ row-key="id"
+ style="width: 100%; margin-bottom: 15px;"
+ v-loading="salesLoading"
+ @selection-change="handleSalesSelectionChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column prop="occurrenceDate" label="鏃ユ湡" width="120" />
+ <el-table-column prop="receiptNumber" label="鍗曟嵁缂栧彿" width="150" />
+ <el-table-column prop="type" label="绫诲瀷" width="100">
+ <template #default="{ row }">
+ <el-tag :type="getDetailTypeTagType(row.type)">{{ row.typeLabel }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="amount" label="閲戦" width="120">
+ <template #default="{ row }">
+ <span :class="getDetailAmountClass(row.type)">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="remark" label="澶囨敞" />
+ </el-table>
+ <el-empty v-else description="璇ュ鎴锋湰鏈堟殏鏃犳槑缁嗘暟鎹�" :image-size="80" />
+
+ <div class="summary-row">
+ <span>鏈熷垵浣欓: <strong class="text-primary">楼{{ formatMoney(generateForm.openingBalance) }}</strong></span>
+ <span>鏈湡搴旀敹: <strong class="text-primary">楼{{ formatMoney(generateForm.currentPlan) }}</strong></span>
+ <span>鏈湡鏀舵: <strong class="text-success">楼{{ formatMoney(generateForm.currentActually) }}</strong></span>
+ <span>鏈熸湯浣欓: <strong :class="displayClosingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(displayClosingBalance) }}</strong></span>
+ </div>
+ </div>
+
+ <div v-else-if="generateForm.customerId && generateForm.statementMonth && !salesLoading" class="empty-tip">
+ <el-empty description="璇ュ鎴锋湰鏈堟殏鏃犻攢鍞暟鎹�" />
+ </div>
+
+ <template #footer>
+ <el-button type="primary" @click="confirmGenerate" :disabled="!canGenerate" :loading="submitLoading">纭鐢熸垚</el-button>
+ <el-button @click="generateDialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, nextTick, getCurrentInstance } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { listCustomer } from "@/api/basicData/customer.js";
+import {
+ getAccountStatementDetailsByMonth,
+ addAccountStatement,
+ listPageAccountStatement,
+ deleteAccountStatement,
+} from "@/api/financialManagement/accountStatement.js";
+
+const ACCOUNT_TYPE_RECEIVABLE = 1;
+
+const { proxy } = getCurrentInstance();
+
+defineOptions({
+ name: "搴旀敹瀵硅处",
+});
+
+const filters = reactive({
+ customerId: "",
+ startMonth: "",
+ endMonth: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "瀵硅处鍗曞彿", prop: "statementNumber", width: "150" },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", width: "180" },
+ { label: "瀵硅处鏈熼棿", prop: "statementMonth", width: "150" },
+ { label: "鏈熷垵浣欓", prop: "openingBalance", dataType: "slot", slot: "openingBalance" },
+ { label: "鏈湡搴旀敹", prop: "currentPlan", dataType: "slot", slot: "currentPlan" },
+ { label: "鏈湡鏀舵", prop: "currentActually", dataType: "slot", slot: "currentActually" },
+ { label: "鏈熸湯浣欓", prop: "closingBalance", dataType: "slot", slot: "closingBalance" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "200", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const submitLoading = ref(false);
+const detailDialogVisible = ref(false);
+const currentCustomer = ref("");
+const currentPeriod = ref("");
+const detailData = ref([]);
+const detailLoading = ref(false);
+
+const generateDialogVisible = ref(false);
+const salesLoading = ref(false);
+const statementDetailLoaded = ref(false);
+const salesData = ref([]);
+const selectedSales = ref([]);
+const salesTableRef = ref(null);
+const customerList = ref([]);
+
+/** 鏄庣粏 type锛�1鍑哄簱 2鍏ュ簱 3鏀舵 4浠樻 5閫�璐� */
+const STATEMENT_DETAIL_TYPE_MAP = {
+ 1: "鍑哄簱",
+ 2: "鍏ュ簱",
+ 3: "鏀舵",
+ 4: "浠樻",
+ 5: "閫�璐�",
+};
+
+const calculateEndBalance = (openingBalance, currentPlan, currentActually) => {
+ return openingBalance + currentPlan - currentActually;
+};
+
+const getDetailTypeLabel = (type) => STATEMENT_DETAIL_TYPE_MAP[Number(type)] ?? "";
+
+const getDetailTypeTagType = (type) => {
+ const t = Number(type);
+ if (t === 1) return "success";
+ if (t === 3) return "primary";
+ if (t === 5) return "danger";
+ return "info";
+};
+
+const getDetailAmountClass = (type) => {
+ const t = Number(type);
+ if (t === 1) return "text-primary";
+ if (t === 3) return "text-success";
+ return "text-danger";
+};
+
+const generateForm = reactive({
+ customerId: "",
+ customerName: "",
+ statementMonth: "",
+ openingBalance: 0,
+ currentPlan: 0,
+ currentActually: 0,
+ closingBalance: 0,
+});
+
+const displayClosingBalance = computed(() => {
+ return calculateEndBalance(
+ generateForm.openingBalance,
+ generateForm.currentPlan,
+ generateForm.currentActually
+ );
+});
+
+const canGenerate = computed(() => {
+ return generateForm.customerId && generateForm.statementMonth && selectedSales.value.length > 0;
+});
+
+const applyStatementSummary = (data) => {
+ generateForm.openingBalance = Number(data.openingBalance ?? 0);
+ generateForm.currentPlan = Number(data.currentPlan ?? 0);
+ generateForm.currentActually = Number(data.currentActually ?? 0);
+ generateForm.closingBalance = Number(
+ data.closingBalance ??
+ calculateEndBalance(
+ generateForm.openingBalance,
+ generateForm.currentPlan,
+ generateForm.currentActually
+ )
+ );
+};
+
+const getCustomerList = () => {
+ listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
+ if (res.code === 200) {
+ customerList.value = res.data?.records || [];
+ }
+ });
+};
+
+const normalizeSalesRows = (list) => {
+ const rows = Array.isArray(list) ? list : [];
+ return rows.map((item, index) => {
+ const type = Number(item.type);
+ return {
+ id: item.id ?? `detail-${index}`,
+ accountStatementId: item.accountStatementId,
+ occurrenceDate: item.occurrenceDate ?? "",
+ receiptNumber: item.receiptNumber ?? "",
+ type,
+ typeLabel: getDetailTypeLabel(type),
+ amount: Math.abs(Number(item.amount ?? 0)),
+ remark: item.remark ?? "",
+ };
+ });
+};
+
+const selectAllSalesRows = (keepApiSummary = false) => {
+ nextTick(() => {
+ const table = salesTableRef.value;
+ if (!table) return;
+ table.clearSelection();
+ salesData.value.forEach((row) => table.toggleRowSelection(row, true));
+ selectedSales.value = [...salesData.value];
+ if (!keepApiSummary) {
+ calculateSummary();
+ }
+ });
+};
+
+const isNumericId = (id) => id !== undefined && id !== null && id !== "" && /^\d+$/.test(String(id));
+
+const buildFilterParams = (params = {}) => {
+ const result = { ...params, accountType: ACCOUNT_TYPE_RECEIVABLE };
+ if (filters.customerId) {
+ result.customerId = filters.customerId;
+ }
+ if (filters.startMonth && filters.endMonth && filters.startMonth === filters.endMonth) {
+ result.statementMonth = filters.startMonth;
+ } else if (filters.startMonth) {
+ result.startMonth = filters.startMonth;
+ }
+ if (filters.endMonth && filters.startMonth !== filters.endMonth) {
+ result.endMonth = filters.endMonth;
+ }
+ return result;
+};
+
+const buildListParams = () =>
+ buildFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+const buildExportParams = () => buildFilterParams({});
+
+const buildDetailSubmitItem = (row) => {
+ const item = {
+ occurrenceDate: row.occurrenceDate,
+ receiptNumber: row.receiptNumber,
+ type: row.type,
+ amount: row.amount,
+ remark: row.remark ?? "",
+ };
+ if (isNumericId(row.id)) {
+ item.id = Number(row.id);
+ }
+ if (row.accountStatementId) {
+ item.accountStatementId = row.accountStatementId;
+ }
+ return item;
+};
+
+const buildAddPayload = () => ({
+ customerId: generateForm.customerId,
+ customerName: generateForm.customerName,
+ statementMonth: generateForm.statementMonth,
+ accountType: ACCOUNT_TYPE_RECEIVABLE,
+ statementNumber: "",
+ openingBalance: generateForm.openingBalance,
+ currentPlan: generateForm.currentPlan,
+ currentActually: generateForm.currentActually,
+ closingBalance: generateForm.closingBalance,
+ accountStatementDetails: selectedSales.value.map(buildDetailSubmitItem),
+});
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountStatement(buildListParams())
+ .then((res) => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = res.data.records ?? [];
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ pagination.total = 0;
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const resetFilters = () => {
+ filters.customerId = "";
+ filters.startMonth = "";
+ filters.endMonth = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+};
+
+const generateStatement = () => {
+ generateForm.customerId = "";
+ generateForm.customerName = "";
+ generateForm.statementMonth = "";
+ generateForm.openingBalance = 0;
+ generateForm.currentPlan = 0;
+ generateForm.currentActually = 0;
+ generateForm.closingBalance = 0;
+ statementDetailLoaded.value = false;
+ salesData.value = [];
+ selectedSales.value = [];
+ generateDialogVisible.value = true;
+};
+
+const onCustomerChange = (customerId) => {
+ const customer = customerList.value.find((item) => item.id === customerId);
+ generateForm.customerName = customer?.customerName ?? "";
+ loadSalesData();
+};
+
+const onStatementMonthChange = () => {
+ loadSalesData();
+};
+
+const loadSalesData = () => {
+ if (!generateForm.customerId || !generateForm.statementMonth) {
+ salesData.value = [];
+ selectedSales.value = [];
+ statementDetailLoaded.value = false;
+ generateForm.openingBalance = 0;
+ generateForm.currentPlan = 0;
+ generateForm.currentActually = 0;
+ generateForm.closingBalance = 0;
+ return;
+ }
+
+ salesLoading.value = true;
+ selectedSales.value = [];
+ statementDetailLoaded.value = false;
+
+ getAccountStatementDetailsByMonth({
+ accountType: ACCOUNT_TYPE_RECEIVABLE,
+ customerId: generateForm.customerId,
+ statementMonth: generateForm.statementMonth,
+ })
+ .then((res) => {
+ if (res.code === 200) {
+ const data = res.data ?? {};
+ const details = data.accountStatementDetails;
+ const list = Array.isArray(details) ? details : [];
+ salesData.value = normalizeSalesRows(list);
+ applyStatementSummary(data);
+ statementDetailLoaded.value = true;
+
+ if (salesData.value.length > 0) {
+ selectAllSalesRows(true);
+ }
+ } else {
+ salesData.value = [];
+ statementDetailLoaded.value = false;
+ ElMessage.error(res.msg || "鏌ヨ瀵硅处鏄庣粏澶辫触");
+ }
+ })
+ .catch(() => {
+ salesData.value = [];
+ statementDetailLoaded.value = false;
+ ElMessage.error("鏌ヨ瀵硅处鏄庣粏澶辫触");
+ })
+ .finally(() => {
+ salesLoading.value = false;
+ });
+};
+
+const calculateSummary = () => {
+ let receivable = 0;
+ let receipt = 0;
+
+ selectedSales.value.forEach((item) => {
+ if (item.type === 1) {
+ receivable += item.amount;
+ } else if (item.type === 5) {
+ receivable -= item.amount;
+ } else if (item.type === 3) {
+ receipt += item.amount;
+ }
+ });
+
+ generateForm.currentPlan = receivable;
+ generateForm.currentActually = receipt;
+ generateForm.closingBalance = calculateEndBalance(
+ generateForm.openingBalance,
+ generateForm.currentPlan,
+ generateForm.currentActually
+ );
+};
+
+const handleSalesSelectionChange = (selection) => {
+ selectedSales.value = selection;
+ calculateSummary();
+};
+
+const confirmGenerate = () => {
+ if (!canGenerate.value) return;
+ submitLoading.value = true;
+ addAccountStatement(buildAddPayload())
+ .then((res) => {
+ if (res.code === 200) {
+ generateDialogVisible.value = false;
+ ElMessage.success("瀵硅处鍗曠敓鎴愭垚鍔�");
+ pagination.currentPage = 1;
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鐢熸垚澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鐢熸垚澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎瀵硅处鍗曘��${row.statementNumber || row.id}銆嶅悧锛焋, "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountStatement([row.id])
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+};
+
+const buildDetailTableFromApi = (data, statementMonth) => {
+ const details = Array.isArray(data.accountStatementDetails) ? data.accountStatementDetails : [];
+ let runningBalance = Number(data.openingBalance ?? 0);
+ const rows = [
+ {
+ date: statementMonth ?? "",
+ type: "鏈熷垵",
+ code: "-",
+ debit: 0,
+ credit: 0,
+ balance: runningBalance,
+ remark: "鏈熷垵浣欓",
+ },
+ ];
+
+ details.forEach((item) => {
+ const amount = Math.abs(Number(item.amount ?? 0));
+ const type = Number(item.type);
+ let debit = 0;
+ let credit = 0;
+
+ if (type === 1) {
+ debit = amount;
+ runningBalance += amount;
+ } else if (type === 3 || type === 5) {
+ credit = amount;
+ runningBalance -= amount;
+ }
+
+ rows.push({
+ date: item.occurrenceDate ?? "",
+ type: getDetailTypeLabel(type),
+ code: item.receiptNumber ?? "",
+ debit,
+ credit,
+ balance: runningBalance,
+ remark: item.remark ?? "",
+ });
+ });
+
+ return rows;
+};
+
+const viewDetail = (row) => {
+ if (!row.customerId || !row.statementMonth) {
+ ElMessage.warning("缂哄皯瀹㈡埛鎴栧璐︽湀浠斤紝鏃犳硶鏌ヨ鏄庣粏");
+ return;
+ }
+
+ currentCustomer.value = row.customerName ?? "";
+ currentPeriod.value = row.statementMonth ?? "";
+ detailData.value = [];
+ detailDialogVisible.value = true;
+ detailLoading.value = true;
+
+ getAccountStatementDetailsByMonth({
+ accountType: ACCOUNT_TYPE_RECEIVABLE,
+ customerId: row.customerId,
+ statementMonth: row.statementMonth,
+ })
+ .then((res) => {
+ if (res.code === 200) {
+ detailData.value = buildDetailTableFromApi(res.data ?? {}, row.statementMonth);
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ鏄庣粏澶辫触");
+ detailDialogVisible.value = false;
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鏌ヨ鏄庣粏澶辫触");
+ detailDialogVisible.value = false;
+ })
+ .finally(() => {
+ detailLoading.value = false;
+ });
+};
+
+const printStatement = (row) => {
+ ElMessage.info(`鎵撳嵃瀵硅处鍗�: ${row.statementNumber}`);
+};
+
+const printDetail = () => {
+ ElMessage.info("鎵撳嵃鏄庣粏");
+};
+
+const handleOut = () => {
+ const params = buildExportParams();
+ proxy.download("/accountStatement/exportAccountStatement", params, `搴旀敹瀵硅处鍗昣${Date.now()}.xlsx`);
+};
+
+onMounted(() => {
+ getCustomerList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.text-success {
+ color: #67c23a;
+}
+
+.text-danger {
+ color: #f56c6c;
+}
+
+.text-primary {
+ color: #409eff;
+}
+
+.statement-header {
+ text-align: center;
+ margin-bottom: 20px;
+ h3 {
+ margin: 0 0 10px 0;
+ }
+ p {
+ color: #909399;
+ margin: 0;
+ }
+}
+
+.sales-section {
+ margin-top: 20px;
+
+ .section-title {
+ font-size: 16px;
+ font-weight: bold;
+ margin-bottom: 15px;
+ padding-left: 10px;
+ border-left: 4px solid #409eff;
+ }
+}
+
+.summary-row {
+ display: flex;
+ justify-content: space-around;
+ padding: 15px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ margin-top: 15px;
+
+ span {
+ font-size: 14px;
+
+ strong {
+ font-size: 16px;
+ margin-left: 5px;
+ }
+ }
+}
+
+.empty-tip {
+ margin-top: 30px;
+}
+</style>
diff --git a/src/views/financialManagement/receivable/salesOut.vue b/src/views/financialManagement/receivable/salesOut.vue
new file mode 100644
index 0000000..0e24b37
--- /dev/null
+++ b/src/views/financialManagement/receivable/salesOut.vue
@@ -0,0 +1,180 @@
+<template>
+ <!-- 閿�鍞嚭搴� -->
+ <div class="app-container">
+ <el-form :model="filters"
+ :inline="true">
+ <el-form-item label="鍑哄簱鍗曞彿:">
+ <el-input v-model="filters.outboundBatches"
+ placeholder="璇疯緭鍏ュ嚭搴撳崟鍙�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛鍚嶇О:">
+ <el-input v-model="filters.customerName"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="鍑哄簱鏃ユ湡:">
+ <el-date-picker v-model="filters.dateRange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button @click="handleOut"
+ icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+ import { ElMessage } from "element-plus";
+ import { listPageAccountSales } from "@/api/financialManagement/accountSales";
+
+ defineOptions({
+ name: "閿�鍞嚭搴�",
+ });
+
+ const { proxy } = getCurrentInstance();
+
+ const filters = reactive({
+ outboundBatches: "",
+ customerName: "",
+ dateRange: [],
+ });
+
+ const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+ });
+
+ const columns = [
+ { label: "鍑哄簱鍗曞彿", prop: "outboundBatches", minWidth: "150" },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", minWidth: "180" },
+ { label: "鍑哄簱鏃ユ湡", prop: "shippingDate", width: "170" },
+ { label: "浜у搧鍚嶇О", prop: "productName", minWidth: "140" },
+ { label: "瑙勬牸鍨嬪彿", prop: "specificationModel", minWidth: "140" },
+ {
+ label: "閲戦",
+ prop: "outboundAmount",
+ minWidth: "120",
+ align: "right",
+ formatData: val =>
+ val === null || val === undefined || val === ""
+ ? ""
+ : Number(val).toLocaleString("zh-CN", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }),
+ },
+ { label: "鍙戣揣缂栧彿", prop: "shippingNo", minWidth: "140" },
+ { label: "閿�鍞鍗曞彿", prop: "salesContractNo", minWidth: "150" },
+ ];
+
+ const dataList = ref([]);
+ const tableLoading = ref(false);
+
+ function buildFilterParams() {
+ const params = {
+ outboundBatches: filters.outboundBatches || undefined,
+ customerName: filters.customerName || undefined,
+ };
+ if (filters.dateRange && filters.dateRange.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+ }
+
+ const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountSales({
+ ...buildFilterParams(),
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ })
+ .then(res => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = res.data.records ?? [];
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const resetFilters = () => {
+ filters.outboundBatches = "";
+ filters.customerName = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+ };
+
+ const handleOut = () => {
+ proxy.download(
+ "/accountSales/exportAccountSalesOutbound",
+ buildFilterParams(),
+ `閿�鍞嚭搴揰${new Date().getTime()}.xlsx`
+ );
+ };
+
+ onMounted(() => {
+ getTableData();
+ });
+</script>
+
+<style lang="scss" scoped>
+ .actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+ }
+</style>
diff --git a/src/views/financialManagement/receivable/salesReturn.vue b/src/views/financialManagement/receivable/salesReturn.vue
new file mode 100644
index 0000000..c58d330
--- /dev/null
+++ b/src/views/financialManagement/receivable/salesReturn.vue
@@ -0,0 +1,171 @@
+<template>
+ <!-- 閿�鍞��璐� -->
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="閫�璐у崟鍙�:">
+ <el-input v-model="filters.returnNo" placeholder="璇疯緭鍏ラ��璐у崟鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛鍚嶇О:">
+ <el-input v-model="filters.customerName" placeholder="璇疯緭鍏ュ鎴峰悕绉�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="閫�璐ф棩鏈�:">
+ <el-date-picker
+ v-model="filters.dateRange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+import { ElMessage } from "element-plus";
+import { listPageAccountSalesReturn } from "@/api/financialManagement/accountSales";
+
+defineOptions({
+ name: "閿�鍞��璐�",
+});
+
+const { proxy } = getCurrentInstance();
+
+const filters = reactive({
+ returnNo: "",
+ customerName: "",
+ dateRange: [],
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "閫�璐у崟鍙�", prop: "returnNo", minWidth: "150" },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", minWidth: "180" },
+ { label: "鍏宠仈鍙戣揣鍗曞彿", prop: "shippingNo", minWidth: "150" },
+ { label: "閫�璐ф棩鏈�", prop: "makeTime", minWidth: "170" },
+ {
+ label: "閫�娆炬�婚",
+ prop: "refundAmount",
+ minWidth: "120",
+ align: "right",
+ formatData: (val) =>
+ val === null || val === undefined || val === ""
+ ? ""
+ : Number(val).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
+ },
+ { label: "閫�璐у師鍥�", prop: "returnReason", minWidth: "150", showOverflowTooltip: true },
+ { label: "閿�鍞鍗曞彿", prop: "salesContractNo", minWidth: "150" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+
+function buildFilterParams() {
+ const params = {
+ returnNo: filters.returnNo || undefined,
+ customerName: filters.customerName || undefined,
+ };
+ if (filters.dateRange && filters.dateRange.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+}
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountSalesReturn({
+ ...buildFilterParams(),
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ })
+ .then((res) => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = res.data.records ?? [];
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const resetFilters = () => {
+ filters.returnNo = "";
+ filters.customerName = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+};
+
+const handleOut = () => {
+ proxy.download(
+ "/accountSales/exportAccountSalesReturn",
+ buildFilterParams(),
+ `閿�鍞��璐${new Date().getTime()}.xlsx`
+ );
+};
+
+onMounted(() => {
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+</style>
diff --git a/src/views/financialManagement/voucher/detailLedger.vue b/src/views/financialManagement/voucher/detailLedger.vue
new file mode 100644
index 0000000..c07574c
--- /dev/null
+++ b/src/views/financialManagement/voucher/detailLedger.vue
@@ -0,0 +1,309 @@
+<template>
+ <div class="app-container ledger-page">
+ <div class="ledger-layout">
+ <aside class="subject-panel">
+ <el-input v-model="subjectKeyword" placeholder="璇疯緭鍏ョ鐩悕绉�/缂栧彿" clearable prefix-icon="Search" />
+ <el-scrollbar class="subject-tree-scroll">
+ <el-tree
+ ref="subjectTreeRef"
+ :data="subjectOptions"
+ node-key="code"
+ :props="{ label: 'name', children: 'children' }"
+ highlight-current
+ default-expand-all
+ :expand-on-click-node="false"
+ :filter-node-method="filterSubjectNode"
+ @node-click="handleSubjectClick"
+ >
+ <template #default="{ data }">
+ <span class="subject-node">{{ data.code }} {{ data.name }}</span>
+ </template>
+ </el-tree>
+ </el-scrollbar>
+ </aside>
+
+ <section class="ledger-content">
+ <el-form :model="filters" :inline="true" class="filter-form">
+ <el-form-item label="鏈熼棿:">
+ <el-date-picker v-model="filters.startMonth" type="month" placeholder="寮�濮嬫湀浠�" value-format="YYYY-MM" style="width: 140px;" />
+ <span style="margin: 0 10px;">鑷�</span>
+ <el-date-picker v-model="filters.endMonth" type="month" placeholder="缁撴潫鏈堜唤" value-format="YYYY-MM" style="width: 140px;" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鏌ヨ</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+<!-- <el-button @click="handlePrint" icon="Printer">鎵撳嵃</el-button>-->
+ <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button>
+ </el-form-item>
+ </el-form>
+
+ <div class="table_list">
+ <el-table :data="dataList" border style="width: 100%">
+ <el-table-column prop="date" label="鏃ユ湡" width="120" />
+ <el-table-column prop="voucherNo" label="鍑瘉瀛楀彿" width="120" />
+ <el-table-column prop="summary" label="鎽樿" min-width="200" show-overflow-tooltip />
+ <el-table-column prop="debit" label="鍊熸柟" width="150">
+ <template #default="{ row }">
+ <span v-if="row.debit > 0" class="text-danger">楼{{ formatMoney(row.debit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="credit" label="璐锋柟" width="150">
+ <template #default="{ row }">
+ <span v-if="row.credit > 0" class="text-success">楼{{ formatMoney(row.credit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏂瑰悜" width="80">
+ <template #default="{ row }">
+ <el-tag :type="row.direction === '鍊�' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="浣欓" width="150">
+ <template #default="{ row }">
+ <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">楼{{ formatMoney(Math.abs(row.balance)) }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <el-empty v-if="!currentSubject" description="璇烽�夋嫨浼氳绉戠洰鏌ヨ" style="margin-top: 50px;" />
+ </section>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, watch, nextTick } from "vue";
+import { ElMessage } from "element-plus";
+import { listAccountSubject } from "@/api/financialManagement/accountSubject";
+import { getDetailLedger } from "@/api/financialManagement/ledger";
+
+defineOptions({
+ name: "绉戠洰鏄庣粏璐�",
+});
+
+const filters = reactive({
+ subject: "",
+ startMonth: "",
+ endMonth: "",
+});
+
+const dataList = ref([]);
+const subjectOptions = ref([]);
+const subjectKeyword = ref("");
+const subjectTreeRef = ref();
+
+const getPreviousMonth = () => {
+ const date = new Date();
+ date.setDate(1);
+ date.setMonth(date.getMonth() - 1);
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ return `${year}-${month}`;
+};
+
+const defaultMonth = getPreviousMonth();
+filters.startMonth = defaultMonth;
+filters.endMonth = defaultMonth;
+
+const fallbackSubjects = [
+ { code: "1122", name: "搴旀敹璐︽" },
+ { code: "2202", name: "搴斾粯璐︽" },
+ { code: "6602", name: "绠$悊璐圭敤" },
+];
+
+const toTree = (nodes = []) =>
+ nodes
+ .filter(item => item.subjectCode && item.subjectName)
+ .map(item => ({
+ code: item.subjectCode,
+ name: item.subjectName,
+ children: toTree(item.children || []),
+ }));
+
+const findSubject = (options, code) => {
+ for (const item of options) {
+ if (item.code === code) return item;
+ if (item.children && item.children.length > 0) {
+ const found = findSubject(item.children, code);
+ if (found) return found;
+ }
+ }
+ return null;
+};
+
+const currentSubject = computed(() => {
+ if (!filters.subject) return null;
+ return findSubject(subjectOptions.value, filters.subject);
+});
+
+const getFirstSubjectCode = (nodes = []) => {
+ for (const item of nodes) {
+ if (item.code) return item.code;
+ if (item.children && item.children.length > 0) {
+ const childCode = getFirstSubjectCode(item.children);
+ if (childCode) return childCode;
+ }
+ }
+ return "";
+};
+
+const setDefaultSubjectSelection = async () => {
+ const firstCode = getFirstSubjectCode(subjectOptions.value);
+ if (!firstCode) {
+ filters.subject = "";
+ subjectTreeRef.value?.setCurrentKey(null);
+ return;
+ }
+ filters.subject = firstCode;
+ await nextTick();
+ subjectTreeRef.value?.setCurrentKey(firstCode);
+};
+
+const filterSubjectNode = (value, data) => {
+ const keyword = value?.trim();
+ if (!keyword) return true;
+ return `${data.code}${data.name}`.includes(keyword);
+};
+
+watch(subjectKeyword, (value) => {
+ subjectTreeRef.value?.filter(value || "");
+});
+
+const handleSubjectClick = async (data) => {
+ filters.subject = data.code;
+ await getTableData();
+};
+
+const loadSubjectOptions = async () => {
+ let options = [];
+ try {
+ const { data } = await listAccountSubject({
+ current: 1,
+ size: 1000,
+ });
+ options = toTree(data?.records || []);
+ } catch (error) {
+ // 鍏ㄥ眬鎷︽埅鍣ㄥ凡鎻愮ず锛屼笅闈㈣蛋鍏滃簳绉戠洰
+ }
+ if (options.length === 0) {
+ options = fallbackSubjects.map(item => ({ ...item, children: [] }));
+ }
+ subjectOptions.value = options;
+ await setDefaultSubjectSelection();
+ if (filters.subject) {
+ await getTableData();
+ }
+};
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+// 鑱旇皟绾﹀畾锛氭槑缁嗚处鎸夌鐩笌鏈熼棿杩囨护
+const getTableData = async () => {
+ if (!currentSubject.value) {
+ dataList.value = [];
+ return;
+ }
+ try {
+ const { data } = await getDetailLedger({
+ subjectCode: currentSubject.value.code,
+ startMonth: filters.startMonth,
+ endMonth: filters.endMonth,
+ });
+ dataList.value = Array.isArray(data) ? data : data?.records || [];
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+};
+
+const resetFilters = async () => {
+ filters.startMonth = defaultMonth;
+ filters.endMonth = defaultMonth;
+ dataList.value = [];
+ subjectKeyword.value = "";
+ subjectTreeRef.value?.filter("");
+ await setDefaultSubjectSelection();
+ if (filters.subject) {
+ await getTableData();
+ }
+};
+
+const handlePrint = () => {
+ ElMessage.info("鎵撳嵃鍔熻兘");
+};
+
+const handleOut = () => {
+ ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+onMounted(async () => {
+ await loadSubjectOptions();
+});
+</script>
+
+<style lang="scss" scoped>
+.ledger-layout {
+ display: flex;
+ gap: 16px;
+}
+
+.subject-panel {
+ width: 260px;
+ flex-shrink: 0;
+ padding: 12px;
+ border: 1px solid #e4e7ed;
+ border-radius: 8px;
+ background-color: #fff;
+}
+
+.subject-tree-scroll {
+ height: 600px;
+ margin-top: 12px;
+}
+
+.subject-node {
+ display: inline-flex;
+ align-items: center;
+}
+
+.ledger-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.filter-form {
+ margin-bottom: 12px;
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+
+.text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+}
+
+.text-warning {
+ color: #e6a23c;
+ font-weight: bold;
+}
+
+.subject-panel :deep(.el-tree-node__content) {
+ height: 34px;
+}
+
+.subject-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
+ background-color: #f0f7ff;
+}
+</style>
diff --git a/src/views/financialManagement/voucher/generalLedger.vue b/src/views/financialManagement/voucher/generalLedger.vue
new file mode 100644
index 0000000..b362279
--- /dev/null
+++ b/src/views/financialManagement/voucher/generalLedger.vue
@@ -0,0 +1,312 @@
+<template>
+ <div class="app-container ledger-page">
+ <div class="ledger-layout">
+ <aside class="subject-panel">
+ <el-input v-model="subjectKeyword" placeholder="璇疯緭鍏ョ鐩悕绉�/缂栧彿" clearable prefix-icon="Search" />
+ <el-scrollbar class="subject-tree-scroll">
+ <el-tree
+ ref="subjectTreeRef"
+ :data="subjectOptions"
+ node-key="code"
+ :props="{ label: 'name', children: 'children' }"
+ highlight-current
+ default-expand-all
+ :expand-on-click-node="false"
+ :filter-node-method="filterSubjectNode"
+ @node-click="handleSubjectClick"
+ >
+ <template #default="{ data }">
+ <span class="subject-node">{{ data.code }} {{ data.name }}</span>
+ </template>
+ </el-tree>
+ </el-scrollbar>
+ </aside>
+
+ <section class="ledger-content">
+ <el-form :model="filters" :inline="true" class="filter-form">
+ <el-form-item label="鏈熼棿:">
+ <el-date-picker v-model="filters.startMonth" type="month" placeholder="寮�濮嬫湀浠�" value-format="YYYY-MM" style="width: 140px;" />
+ <span style="margin: 0 10px;">鑷�</span>
+ <el-date-picker v-model="filters.endMonth" type="month" placeholder="缁撴潫鏈堜唤" value-format="YYYY-MM" style="width: 140px;" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鏌ヨ</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+<!-- <el-button @click="handlePrint" icon="Printer">鎵撳嵃</el-button>-->
+ <!-- <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button> -->
+ </el-form-item>
+ </el-form>
+
+ <div class="table_list">
+ <el-table :data="dataList" border style="width: 100%">
+ <el-table-column prop="date" label="鏃ユ湡"/>
+ <!-- <el-table-column prop="voucherNo" label="鍑瘉瀛楀彿" width="120" /> -->
+ <!-- <el-table-column prop="summary" label="鎽樿" min-width="200" show-overflow-tooltip /> -->
+ <el-table-column prop="debit" label="鍊熸柟">
+ <template #default="{ row }">
+ <span v-if="row.debit > 0" class="text-danger">楼{{ formatMoney(row.debit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="credit" label="璐锋柟">
+ <template #default="{ row }">
+ <span v-if="row.credit > 0" class="text-success">楼{{ formatMoney(row.credit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏂瑰悜">
+ <template #default="{ row }">
+ <el-tag :type="row.direction === '鍊�' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="浣欓">
+ <template #default="{ row }">
+ <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">楼{{ formatMoney(Math.abs(row.balance)) }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <el-empty v-if="!currentSubject" description="璇烽�夋嫨浼氳绉戠洰鏌ヨ" style="margin-top: 50px;" />
+ </section>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, watch, nextTick } from "vue";
+import { ElMessage } from "element-plus";
+import { listAccountSubject } from "@/api/financialManagement/accountSubject";
+import { getGeneralLedger } from "@/api/financialManagement/ledger";
+
+defineOptions({
+ name: "绉戠洰鎬昏处",
+});
+
+const filters = reactive({
+ subject: "",
+ startMonth: "",
+ endMonth: "",
+});
+
+const dataList = ref([]);
+const subjectOptions = ref([]);
+const subjectKeyword = ref("");
+const subjectTreeRef = ref();
+
+const getPreviousMonth = () => {
+ const date = new Date();
+ date.setDate(1);
+ date.setMonth(date.getMonth() - 1);
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ return `${year}-${month}`;
+};
+
+const defaultMonth = getPreviousMonth();
+filters.startMonth = defaultMonth;
+filters.endMonth = defaultMonth;
+
+const fallbackSubjects = [
+ { code: "1001", name: "搴撳瓨鐜伴噾" },
+ { code: "1002", name: "閾惰瀛樻" },
+ { code: "1122", name: "搴旀敹璐︽" },
+ { code: "2202", name: "搴斾粯璐︽" },
+ { code: "6001", name: "涓昏惀涓氬姟鏀跺叆" },
+];
+
+const toTree = (nodes = []) =>
+ nodes
+ .filter(item => item.subjectCode && item.subjectName)
+ .map(item => ({
+ code: item.subjectCode,
+ name: item.subjectName,
+ children: toTree(item.children || []),
+ }));
+
+const findSubject = (options, code) => {
+ for (const item of options) {
+ if (item.code === code) return item;
+ if (item.children && item.children.length > 0) {
+ const found = findSubject(item.children, code);
+ if (found) return found;
+ }
+ }
+ return null;
+};
+
+const currentSubject = computed(() => {
+ if (!filters.subject) return null;
+ return findSubject(subjectOptions.value, filters.subject);
+});
+
+const getFirstSubjectCode = (nodes = []) => {
+ for (const item of nodes) {
+ if (item.code) return item.code;
+ if (item.children && item.children.length > 0) {
+ const childCode = getFirstSubjectCode(item.children);
+ if (childCode) return childCode;
+ }
+ }
+ return "";
+};
+
+const setDefaultSubjectSelection = async () => {
+ const firstCode = getFirstSubjectCode(subjectOptions.value);
+ if (!firstCode) {
+ filters.subject = "";
+ subjectTreeRef.value?.setCurrentKey(null);
+ return;
+ }
+ filters.subject = firstCode;
+ await nextTick();
+ subjectTreeRef.value?.setCurrentKey(firstCode);
+};
+
+const filterSubjectNode = (value, data) => {
+ const keyword = value?.trim();
+ if (!keyword) return true;
+ return `${data.code}${data.name}`.includes(keyword);
+};
+
+watch(subjectKeyword, (value) => {
+ subjectTreeRef.value?.filter(value || "");
+});
+
+const handleSubjectClick = async (data) => {
+ filters.subject = data.code;
+ await getTableData();
+};
+
+const loadSubjectOptions = async () => {
+ let options = [];
+ try {
+ const { data } = await listAccountSubject({
+ current: 1,
+ size: 1000,
+ status: 0,
+ });
+ options = toTree(data?.records || []);
+ } catch (error) {
+ // 鍏ㄥ眬鎷︽埅鍣ㄥ凡鎻愮ず锛屼笅闈㈣蛋鍏滃簳绉戠洰
+ }
+ if (options.length === 0) {
+ options = fallbackSubjects.map(item => ({ ...item, children: [] }));
+ }
+ subjectOptions.value = options;
+ await setDefaultSubjectSelection();
+ if (filters.subject) {
+ await getTableData();
+ }
+};
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+// 鑱旇皟绾﹀畾锛氭�昏处鎺ュ彛杩斿洖琛屾暟缁勶紙rowType/date/voucherNo/summary/debit/credit/direction/balance锛�
+const getTableData = async () => {
+ if (!currentSubject.value) {
+ dataList.value = [];
+ return;
+ }
+ try {
+ const { data } = await getGeneralLedger({
+ subjectCode: currentSubject.value.code,
+ startMonth: filters.startMonth,
+ endMonth: filters.endMonth,
+ });
+ dataList.value = Array.isArray(data) ? data : data?.records || [];
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+};
+
+const resetFilters = async () => {
+ filters.startMonth = defaultMonth;
+ filters.endMonth = defaultMonth;
+ dataList.value = [];
+ subjectKeyword.value = "";
+ subjectTreeRef.value?.filter("");
+ await setDefaultSubjectSelection();
+ if (filters.subject) {
+ await getTableData();
+ }
+};
+
+const handlePrint = () => {
+ ElMessage.info("鎵撳嵃鍔熻兘");
+};
+
+const handleOut = () => {
+ ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+onMounted(async () => {
+ await loadSubjectOptions();
+});
+</script>
+
+<style lang="scss" scoped>
+.ledger-layout {
+ display: flex;
+ gap: 16px;
+}
+
+.subject-panel {
+ width: 260px;
+ flex-shrink: 0;
+ padding: 12px;
+ border: 1px solid #e4e7ed;
+ border-radius: 8px;
+ background-color: #fff;
+}
+
+.subject-tree-scroll {
+ height: 600px;
+ margin-top: 12px;
+}
+
+.subject-node {
+ display: inline-flex;
+ align-items: center;
+}
+
+.ledger-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.filter-form {
+ margin-bottom: 12px;
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+
+.text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+}
+
+.text-warning {
+ color: #e6a23c;
+ font-weight: bold;
+}
+
+.subject-panel :deep(.el-tree-node__content) {
+ height: 34px;
+}
+
+.subject-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
+ background-color: #f0f7ff;
+}
+</style>
diff --git a/src/views/financialManagement/voucher/index.vue b/src/views/financialManagement/voucher/index.vue
new file mode 100644
index 0000000..b4ee561
--- /dev/null
+++ b/src/views/financialManagement/voucher/index.vue
@@ -0,0 +1,1186 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="鍑瘉瀛楀彿:">
+ <el-input v-model="filters.voucherNo" placeholder="璇疯緭鍏ュ嚟璇佸瓧鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="鍑瘉鏃ユ湡:">
+ <el-date-picker v-model="filters.dateRange" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange" range-separator="鑷�" start-placeholder="寮�濮嬫棩鏈�" end-placeholder="缁撴潫鏃ユ湡" clearable />
+ </el-form-item>
+ <el-form-item label="鍒跺崟浜�:">
+ <el-select v-model="filters.creator" placeholder="璇烽�夋嫨鍒跺崟浜�" clearable style="width: 150px;">
+ <el-option
+ v-for="item in creatorOptions"
+ :key="item"
+ :label="item"
+ :value="item"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��:">
+ <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+ <el-option label="鏈繃璐�" value="unposted" />
+ <el-option label="宸茶繃璐�" value="posted" />
+ <el-option label="宸蹭綔搴�" value="cancelled" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-statistic title="鍊熸柟鍚堣" :value="totalDebit" :precision="2" prefix="楼" />
+ <el-statistic title="璐锋柟鍚堣" :value="totalCredit" :precision="2" prefix="楼" style="margin-left: 30px;" />
+ </div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus">鏂板鍑瘉</el-button>
+ <!-- <el-button @click="handleImport" icon="Upload">瀵煎叆</el-button> -->
+ <!-- <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button> -->
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ >
+ <template #debit="{ row }">
+ <span class="text-danger" v-if="row.debit > 0">楼{{ formatMoney(row.debit) }}</span>
+ <span v-else>-</span>
+ </template>
+ <template #credit="{ row }">
+ <span class="text-success" v-if="row.credit > 0">楼{{ formatMoney(row.credit) }}</span>
+ <span v-else>-</span>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button type="primary" link @click="edit(row)" v-if="canEditVoucher(row.status)">缂栬緫</el-button>
+ <el-button type="success" link @click="handlePost(row)" v-if="canEditVoucher(row.status)">杩囪处</el-button>
+ <el-button type="danger" link @click="handleCancel(row)" v-if="canEditVoucher(row.status)">浣滃簾</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog :title="dialogTitle" v-model="dialogVisible" width="1200px" @confirm="submitForm" @cancel="dialogVisible = false">
+ <div class="voucher-container">
+ <div class="voucher-header">
+ <h2 class="voucher-title">璁拌处鍑瘉</h2>
+ <div class="voucher-period">{{ form.voucherDate ? form.voucherDate.substring(0, 7) + '鏈�' : '' }}</div>
+ </div>
+ <el-form :model="form" :rules="rules" :disabled="isViewMode" ref="formRef" label-width="0">
+ <div class="voucher-info">
+ <div class="voucher-no-section">
+ <span class="label">鍑瘉瀛楋細</span>
+ <el-select v-model="form.voucherPrefix" :disabled="isViewMode" style="width: 70px;">
+ <el-option label="璁�" value="璁�" />
+ <el-option label="鐜�" value="鐜�" />
+ <el-option label="閾�" value="閾�" />
+ <el-option label="杞�" value="杞�" />
+ <el-option label="鏀�" value="鏀�" />
+ <el-option label="浠�" value="浠�" />
+ </el-select>
+ <el-input v-model="form.voucherNum" :disabled="isViewMode" style="width: 60px;" />
+ <span class="label" style="margin-left: 5px;">鍙�</span>
+ </div>
+ <div class="voucher-date-section">
+ <span class="label">鏃ユ湡锛�</span>
+ <el-date-picker v-model="form.voucherDate" :disabled="isViewMode" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 140px;" />
+ </div>
+ <div class="voucher-attachment-section">
+ <span class="label">闄勪欢锛�</span>
+ <el-input-number v-model="form.attachmentCount" :disabled="isViewMode" :min="0" :controls="false" style="width: 60px;" />
+ <span class="label" style="margin-left: 5px;">寮�</span>
+ </div>
+ </div>
+ <div class="voucher-table">
+ <table class="accounting-voucher">
+ <thead>
+ <tr>
+ <th class="col-summary" rowspan="2">鎽樿</th>
+ <th class="col-subject" rowspan="2">浼氳绉戠洰</th>
+ <th class="col-debit-header" colspan="11">鍊熸柟</th>
+ <th class="col-credit-header" colspan="11">璐锋柟</th>
+ <th class="col-action" rowspan="2">鎿嶄綔</th>
+ </tr>
+ <tr class="amount-header">
+ <th>浜�</th>
+ <th>鍗�</th>
+ <th>鐧�</th>
+ <th>鍗�</th>
+ <th>涓�</th>
+ <th>鍗�</th>
+ <th>鐧�</th>
+ <th>鍗�</th>
+ <th>鍏�</th>
+ <th>瑙�</th>
+ <th>鍒�</th>
+ <th>浜�</th>
+ <th>鍗�</th>
+ <th>鐧�</th>
+ <th>鍗�</th>
+ <th>涓�</th>
+ <th>鍗�</th>
+ <th>鐧�</th>
+ <th>鍗�</th>
+ <th>鍏�</th>
+ <th>瑙�</th>
+ <th>鍒�</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(entry, rowIndex) in form.entries" :key="rowIndex" @click="selectRow(rowIndex)" :class="{ 'selected-row': selectedRowIndex === rowIndex }">
+ <td class="col-summary">
+ <el-input v-model="entry.summary" :disabled="isViewMode" placeholder="璇疯緭鍏ユ憳瑕�" @focus="selectRow(rowIndex)" />
+ </td>
+ <td class="col-subject">
+ <el-tree-select
+ v-model="entry.subjectCode"
+ :data="subjectTreeOptions"
+ :props="subjectTreeSelectProps"
+ :disabled="isViewMode"
+ placeholder="閫夋嫨绉戠洰"
+ filterable
+ check-strictly
+ clearable
+ :render-after-expand="false"
+ @change="(val) => handleSubjectChange(val, rowIndex)"
+ @focus="selectRow(rowIndex)"
+ />
+ <!-- <div class="subject-name">{{ entry.subjectName }}</div> -->
+ </td>
+ <!-- 鍊熸柟11鍒� -->
+ <template v-if="editingCell.row === rowIndex && editingCell.type === 'debit'">
+ <td colspan="11" class="debit-input-cell">
+ <el-input-number ref="amountInputRef" v-model="entry.debit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" :value-on-clear="undefined" size="small" @blur="finishEdit" class="full-width-input" />
+ </td>
+ </template>
+ <template v-else>
+ <td v-for="(digit, dIndex) in getAmountDigits(entry.debit, 11)" :key="'debit-'+dIndex" class="amount-cell debit-cell" @click="openAmountInput(rowIndex, 'debit')">
+ <span :class="{ 'text-primary': digit !== '', 'zero': digit === '' }">{{ digit || '' }}</span>
+ </td>
+ </template>
+ <!-- 璐锋柟11鍒� -->
+ <template v-if="editingCell.row === rowIndex && editingCell.type === 'credit'">
+ <td colspan="11" class="credit-input-cell">
+ <el-input-number ref="amountInputRef" v-model="entry.credit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" :value-on-clear="undefined" size="small" @blur="finishEdit" class="full-width-input" />
+ </td>
+ </template>
+ <template v-else>
+ <td v-for="(digit, dIndex) in getAmountDigits(entry.credit, 11)" :key="'credit-'+dIndex" class="amount-cell credit-cell" @click="openAmountInput(rowIndex, 'credit')">
+ <span :class="{ 'text-danger': digit !== '', 'zero': digit === '' }">{{ digit || '' }}</span>
+ </td>
+ </template>
+ <td class="col-action">
+ <el-button type="danger" link size="small" @click="removeEntry(rowIndex)" icon="Delete" :disabled="isViewMode || form.entries.length <= 2">鍒犻櫎</el-button>
+ </td>
+ </tr>
+ <tr class="total-row">
+ <td class="col-summary" colspan="2" style="text-align: center; font-weight: bold;">鍚堣锛�</td>
+ <td v-for="(digit, index) in getAmountDigits(totalDebitEntry, 11)" :key="'total-debit-'+index" class="amount-cell total-debit-cell">
+ <span :class="{ 'text-primary': digit !== '' }">{{ digit }}</span>
+ </td>
+ <td v-for="(digit, index) in getAmountDigits(totalCreditEntry, 11)" :key="'total-credit-'+index" class="amount-cell total-credit-cell">
+ <span :class="{ 'text-danger': digit !== '' }">{{ digit }}</span>
+ </td>
+ <td class="col-action"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="voucher-toolbar">
+ <el-button type="primary" link @click="addEntry" icon="Plus" :disabled="isViewMode">鏂板琛�</el-button>
+ </div>
+ <div class="voucher-footer">
+ <div class="creator-section">
+ <span class="label">鍒跺崟浜猴細</span>
+ <el-select
+ v-model="form.creator"
+ :disabled="isViewMode"
+ placeholder="璇烽�夋嫨鍒跺崟浜�"
+ filterable
+ clearable
+ style="width: 200px;"
+ >
+ <el-option
+ v-for="item in creatorOptions"
+ :key="item"
+ :label="item"
+ :value="item"
+ />
+ </el-select>
+ </div>
+ </div>
+ <!-- 闄勪欢鏉愭枡 -->
+ <div class="voucher-attachment-section">
+ <div class="attachment-label">闄勪欢鏉愭枡锛�</div>
+ <el-upload
+ v-model:file-list="form.attachments"
+ :action="upload.url"
+ multiple
+ ref="fileUpload"
+ auto-upload
+ :headers="upload.headers"
+ :before-upload="handleBeforeUpload"
+ :on-error="handleUploadError"
+ :on-success="handleUploadSuccess"
+ :on-remove="handleRemove"
+ :on-preview="handlePreview"
+ :disabled="isViewMode">
+ <el-button type="primary" v-if="!isViewMode">涓婁紶</el-button>
+ <template #tip v-if="!isViewMode">
+ <div class="el-upload__tip">
+ 鏂囦欢鏍煎紡鏀寔 doc锛宒ocx锛寈ls锛寈lsx锛宲pt锛宲ptx锛宲df锛宼xt锛寈ml锛宩pg锛宩peg锛宲ng锛実if锛宐mp锛宺ar锛寊ip锛�7z
+ </div>
+ </template>
+ </el-upload>
+ </div>
+ </el-form>
+ </div>
+ <template #footer>
+ <div>
+ <el-button v-if="!isViewMode" type="primary" @click="submitForm" :disabled="!isBalanced">淇濆瓨</el-button>
+ <el-button @click="dialogVisible = false">{{ isViewMode ? '鍏抽棴' : '鍙栨秷' }}</el-button>
+ </div>
+ </template>
+ </FormDialog>
+ <!-- 鏂囦欢棰勮缁勪欢 -->
+ <FilePreview ref="filePreviewRef" />
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, nextTick } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import FilePreview from "@/components/filePreview/index.vue";
+import download from "@/plugins/download.js";
+import { getToken } from "@/utils/auth";
+import useUserStore from "@/store/modules/user";
+import { userListNoPageByTenantId } from "@/api/system/user";
+import { listAccountSubject } from "@/api/financialManagement/accountSubject";
+import {
+ listVoucherPage,
+ addVoucher,
+ updateVoucher,
+ postVoucher,
+ cancelVoucher,
+ getVoucherDetail,
+} from "@/api/financialManagement/voucher";
+
+defineOptions({
+ name: "鍑瘉绠$悊",
+});
+
+const userStore = useUserStore();
+const getDefaultCreator = () => userStore.nickName || userStore.name || "寮犱笁";
+
+const filters = reactive({
+ voucherNo: "",
+ dateRange: [],
+ creator: "",
+ status: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "鍑瘉瀛楀彿", prop: "voucherNo", width: "120" },
+ { label: "鍑瘉鏃ユ湡", prop: "voucherDate", width: "120" },
+ { label: "鎽樿", prop: "summary", showOverflowTooltip: true },
+ { label: "鍊熸柟閲戦", prop: "debit", dataType: "slot", slot: "debit" },
+ { label: "璐锋柟閲戦", prop: "credit", dataType: "slot", slot: "credit" },
+ { label: "鍒跺崟浜�", prop: "creator", width: "100" },
+ { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "220", fixed: "right" },
+];
+
+const dataList = ref([]);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const dialogMode = ref("add");
+const isEdit = ref(false);
+const currentId = ref(null);
+const isViewMode = computed(() => dialogMode.value === "view");
+const filePreviewRef = ref(null);
+
+// 涓婁紶鐩稿叧閰嶇疆
+const upload = reactive({
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/file/upload?type=14",
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+});
+const fileUpload = ref(null);
+
+const fallbackSubjectTree = [
+ { subjectCode: "1001", subjectName: "搴撳瓨鐜伴噾", balanceDirection: "鍊熸柟", children: [] },
+ { subjectCode: "1002", subjectName: "閾惰瀛樻", balanceDirection: "鍊熸柟", children: [] },
+ { subjectCode: "1122", subjectName: "搴旀敹璐︽", balanceDirection: "鍊熸柟", children: [] },
+ { subjectCode: "2202", subjectName: "搴斾粯璐︽", balanceDirection: "璐锋柟", children: [] },
+ { subjectCode: "5001", subjectName: "鐢熶骇鎴愭湰", balanceDirection: "鍊熸柟", children: [] },
+ { subjectCode: "6001", subjectName: "涓昏惀涓氬姟鏀跺叆", balanceDirection: "璐锋柟", children: [] },
+ { subjectCode: "6401", subjectName: "涓昏惀涓氬姟鎴愭湰", balanceDirection: "鍊熸柟", children: [] },
+];
+
+const subjectTreeOptions = ref([]);
+const subjectList = ref([]);
+const subjectTreeSelectProps = {
+ children: "children",
+ label: "label",
+ value: "value",
+};
+
+const buildSubjectTreeOptions = (nodes = [], flatList = []) =>
+ (nodes || [])
+ .filter(item => item.subjectCode && item.subjectName)
+ .map(item => {
+ const balanceDirection = item.balanceDirection || "";
+ const flatItem = {
+ code: item.subjectCode,
+ name: item.subjectName,
+ balanceDirection,
+ };
+ flatList.push(flatItem);
+ return {
+ value: flatItem.code,
+ label: `${flatItem.code} ${flatItem.name}${balanceDirection ? ` [${balanceDirection}]` : ""}`,
+ children: buildSubjectTreeOptions(item.children || [], flatList),
+ };
+ });
+
+const createEmptyEntry = () => ({
+ subjectCode: "",
+ subjectName: "",
+ balanceDirection: "",
+ summary: "",
+ debit: undefined,
+ credit: undefined,
+});
+
+const createDefaultForm = () => ({
+ voucherNo: "",
+ voucherPrefix: "璁�",
+ voucherNum: "",
+ voucherDate: "",
+ attachmentCount: 0,
+ attachments: [],
+ entries: [createEmptyEntry(), createEmptyEntry()],
+ creator: getDefaultCreator(),
+ remark: "",
+});
+
+const form = reactive({
+ ...createDefaultForm(),
+});
+
+const userOptions = ref([]);
+
+const creatorOptions = computed(() => {
+ const source = [
+ ...userOptions.value.map(item => item.nickName || item.userName || item.name),
+ getDefaultCreator(),
+ form.creator,
+ filters.creator,
+ ];
+ return [...new Set(source.filter(Boolean))];
+});
+
+const selectedRowIndex = ref(-1);
+const editingCell = reactive({
+ row: -1,
+ type: "",
+});
+const amountInputRef = ref(null);
+
+const isBalanced = computed(() => {
+ return totalDebitEntry.value === totalCreditEntry.value && totalDebitEntry.value > 0;
+});
+
+const rules = {
+ voucherDate: [{ required: true, message: "璇烽�夋嫨鍑瘉鏃ユ湡", trigger: "change" }],
+};
+
+const totalDebit = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.debit), 0);
+});
+
+const totalCredit = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.credit), 0);
+});
+
+const totalDebitEntry = computed(() => {
+ return form.entries.reduce((sum, item) => sum + Number(item.debit || 0), 0);
+});
+
+const totalCreditEntry = computed(() => {
+ return form.entries.reduce((sum, item) => sum + Number(item.credit || 0), 0);
+});
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const normalizeVoucherStatus = status => String(status || "").toLowerCase();
+
+const canEditVoucher = status => {
+ const key = normalizeVoucherStatus(status);
+ return key === "unposted" || status === "鏈繃璐�";
+};
+
+const getStatusLabel = (status) => {
+ const key = normalizeVoucherStatus(status);
+ const map = { unposted: "鏈繃璐�", posted: "宸茶繃璐�", cancelled: "宸蹭綔搴�" };
+ return map[key] || status;
+};
+
+const getStatusType = (status) => {
+ const key = normalizeVoucherStatus(status);
+ const map = { unposted: "warning", posted: "success", cancelled: "info" };
+ return map[key] || "";
+};
+
+// 鑱旇皟绾﹀畾锛氬垎椤靛弬鏁颁娇鐢� current/size锛屾棩鏈熻寖鍥存媶鍒嗕负 startDate/endDate
+const getTableData = async () => {
+ try {
+ const [startDate, endDate] =
+ filters.dateRange && filters.dateRange.length === 2 ? filters.dateRange : ["", ""];
+ const { data } = await listVoucherPage({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ voucherNo: filters.voucherNo,
+ creator: filters.creator,
+ status: filters.status,
+ startDate,
+ endDate,
+ });
+ dataList.value = data?.records || [];
+ pagination.total = Number(data?.total || 0);
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+};
+
+// 鍑瘉鍒嗗綍閲岀殑绉戠洰涓嬫媺涓庢�昏处绉戠洰淇濇寔涓�鑷达紝閬垮厤鎻愪氦涓嶅瓨鍦ㄧ鐩�
+const loadSubjectList = async () => {
+ try {
+ const { data } = await listAccountSubject({
+ current: 1,
+ size: 1000,
+ status: 0
+ });
+ const flatList = [];
+ const treeOptions = buildSubjectTreeOptions(data?.records || [], flatList);
+ if (treeOptions.length > 0) {
+ subjectTreeOptions.value = treeOptions;
+ subjectList.value = flatList;
+ return;
+ }
+ const fallbackFlatList = [];
+ subjectTreeOptions.value = buildSubjectTreeOptions(fallbackSubjectTree, fallbackFlatList);
+ subjectList.value = fallbackFlatList;
+ } catch (error) {
+ // 鍏ㄥ眬鎷︽埅鍣ㄥ凡鎻愮ず閿欒锛岃繖閲屼繚鐣欓粯璁ょ鐩綔涓哄厹搴�
+ const fallbackFlatList = [];
+ subjectTreeOptions.value = buildSubjectTreeOptions(fallbackSubjectTree, fallbackFlatList);
+ subjectList.value = fallbackFlatList;
+ }
+};
+
+const loadUserOptions = async () => {
+ try {
+ const { data } = await userListNoPageByTenantId();
+ userOptions.value = Array.isArray(data) ? data : [];
+ } catch (error) {
+ userOptions.value = [];
+ }
+};
+
+const resetFilters = () => {
+ filters.voucherNo = "";
+ filters.dateRange = [];
+ filters.creator = "";
+ filters.status = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+};
+
+const addEntry = () => {
+ if (isViewMode.value) {
+ return;
+ }
+ form.entries.push(createEmptyEntry());
+};
+
+const handleAttachmentChange = (fileList) => {
+ form.attachmentCount = fileList?.length || 0;
+};
+
+// 浣跨敤椤圭洰灏佽鐨� filePreview 缁勪欢棰勮鏂囦欢
+const previewFile = (row) => {
+ const url = row.previewURL || row.previewUrl || row.url;
+ if (url && filePreviewRef.value) {
+ filePreviewRef.value.open(url);
+ } else {
+ ElMessage.warning('鏂囦欢鍦板潃鏃犳晥锛屾棤娉曢瑙�');
+ }
+};
+
+// 浣跨敤椤圭洰灏佽鐨� download 鎻掍欢涓嬭浇鏂囦欢
+const downloadFile = (row) => {
+ const url = row.downloadURL || row.downloadUrl || row.url;
+ if (url) {
+ const filename = row.originalFilename || row.name || row.fileName || 'download';
+ download.byUrl(url, filename);
+ } else {
+ ElMessage.warning('鏂囦欢鍦板潃鏃犳晥锛屾棤娉曚笅杞�');
+ }
+};
+
+const selectRow = (index) => {
+ selectedRowIndex.value = index;
+};
+
+const openAmountInput = (index, type) => {
+ if (isViewMode.value) {
+ return;
+ }
+ editingCell.row = index;
+ editingCell.type = type;
+ nextTick(() => {
+ if (amountInputRef.value) {
+ amountInputRef.value.focus();
+ }
+ });
+};
+
+const finishEdit = () => {
+ editingCell.row = -1;
+ editingCell.type = "";
+};
+
+const getAmountDigits = (amount, length) => {
+ if (!amount || amount === 0) {
+ return new Array(length).fill('');
+ }
+
+ const amountStr = Number(amount).toFixed(2);
+ const [intPart, decPart] = amountStr.split('.');
+ const fullAmount = intPart + decPart;
+
+ // 宸﹀~鍏�0鍒版寚瀹氶暱搴�
+ const paddedAmount = fullAmount.padStart(length, '0');
+ const digits = paddedAmount.split('');
+
+ // 鎵惧埌绗竴涓潪闆舵暟瀛楃殑浣嶇疆
+ let firstNonZeroIndex = 0;
+ for (let i = 0; i < digits.length; i++) {
+ if (digits[i] !== '0') {
+ firstNonZeroIndex = i;
+ break;
+ }
+ }
+
+ // 鍙殣钘忓墠瀵奸浂锛堢涓�涓潪闆舵暟瀛椾箣鍓嶇殑闆讹級
+ return digits.map((d, index) => {
+ if (index < firstNonZeroIndex) {
+ return ''; // 鍓嶅闆舵樉绀轰负绌�
+ }
+ return d; // 淇濈暀閲戦涓殑闆�
+ });
+};
+
+const removeEntry = (index) => {
+ if (isViewMode.value) {
+ return;
+ }
+ if (form.entries.length <= 2) {
+ return;
+ }
+ form.entries.splice(index, 1);
+};
+
+const handleSubjectChange = (val, index) => {
+ const subject = subjectList.value.find(item => item.code === val);
+ if (subject) {
+ form.entries[index].subjectName = subject.name;
+ form.entries[index].balanceDirection = subject.balanceDirection || "";
+ } else {
+ form.entries[index].subjectName = "";
+ form.entries[index].balanceDirection = "";
+ }
+};
+
+const add = () => {
+ dialogMode.value = "add";
+ isEdit.value = false;
+ currentId.value = null;
+ dialogTitle.value = "鏂板鍑瘉";
+ const nextNum = String((pagination.total || 0) + 1).padStart(4, "0");
+ Object.assign(form, createDefaultForm(), {
+ voucherPrefix: "璁�",
+ voucherNum: nextNum,
+ voucherNo: `璁�-${nextNum}`,
+ voucherDate: new Date().toISOString().split('T')[0],
+ });
+ selectedRowIndex.value = 0;
+ dialogVisible.value = true;
+};
+
+const openVoucherDialog = async (row, mode = "edit") => {
+ try {
+ dialogMode.value = mode;
+ isEdit.value = mode === "edit";
+ currentId.value = row.id;
+ dialogTitle.value = mode === "view" ? "鏌ョ湅鍑瘉" : "缂栬緫鍑瘉";
+ const { data } = await getVoucherDetail(row.id);
+ const detail = data || row;
+ const parts = (detail.voucherNo || "").split("-");
+ const attachments = detail.salesLedgerFiles || detail.storageBlobVOList || detail.storageBlobDTOs || detail.attachments || [];
+ Object.assign(form, createDefaultForm(), {
+ ...detail,
+ voucherPrefix: parts[0] || "璁�",
+ voucherNum: parts[1] || "",
+ creator: detail.creator || getDefaultCreator(),
+ attachments,
+ entries:
+ detail.entries?.map(item => ({
+ subjectCode: item.subjectCode || "",
+ subjectName: item.subjectName || "",
+ balanceDirection: item.balanceDirection || "",
+ summary: item.summary || "",
+ debit: Number(item.debit || 0),
+ credit: Number(item.credit || 0),
+ })) || [],
+ });
+ if (form.entries.length < 2) {
+ while (form.entries.length < 2) {
+ form.entries.push(createEmptyEntry());
+ }
+ }
+ selectedRowIndex.value = 0;
+ dialogVisible.value = true;
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+};
+
+const edit = async row => {
+ await openVoucherDialog(row, "edit");
+};
+
+const view = async row => {
+ await openVoucherDialog(row, "view");
+};
+
+const handlePost = (row) => {
+ ElMessageBox.confirm("纭杩囪处璇ュ嚟璇佸悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "info",
+ }).then(async () => {
+ await postVoucher({ id: row.id });
+ ElMessage.success("杩囪处鎴愬姛");
+ await getTableData();
+ });
+};
+
+const handleCancel = (row) => {
+ ElMessageBox.confirm("纭浣滃簾璇ュ嚟璇佸悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(async () => {
+ await cancelVoucher({ id: row.id });
+ ElMessage.success("浣滃簾鎴愬姛");
+ await getTableData();
+ });
+};
+
+// 鏂囦欢涓婁紶鍓嶆牎楠�
+const handleBeforeUpload = (file) => {
+ const allowedTypes = [
+ 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf', 'txt', 'xml',
+ 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'rar', 'zip', '7z'
+ ];
+ const fileExt = file.name.split('.').pop().toLowerCase();
+ const isAllowed = allowedTypes.includes(fileExt);
+ const isLt50M = file.size / 1024 / 1024 < 50;
+
+ if (!isAllowed) {
+ ElMessage.error('鏂囦欢鏍煎紡涓嶆敮鎸侊紒');
+ return false;
+ }
+ if (!isLt50M) {
+ ElMessage.error('鏂囦欢澶у皬涓嶈兘瓒呰繃 50MB锛�');
+ return false;
+ }
+ return true;
+};
+
+// 鏂囦欢涓婁紶鎴愬姛
+const handleUploadSuccess = (response, file, fileList) => {
+ if (response.code === 200) {
+ ElMessage.success('涓婁紶鎴愬姛');
+ // 鏇存柊闄勪欢鍒楄〃锛屼娇鐢ㄥ悗绔繑鍥炵殑鏁版嵁
+ const index = form.attachments.findIndex(item => item.uid === file.uid);
+ if (index !== -1) {
+ const resData = response.data || {};
+ form.attachments[index] = {
+ ...form.attachments[index],
+ ...resData,
+ name: resData.originalFilename || resData.fileName || file.name,
+ url: resData.url || resData.previewUrl || resData.downloadUrl,
+ tempId: resData.tempId || resData.id
+ };
+ }
+ } else {
+ ElMessage.error(response.msg || '涓婁紶澶辫触');
+ }
+};
+
+// 鏂囦欢涓婁紶澶辫触
+const handleUploadError = (error, file, fileList) => {
+ ElMessage.error('鏂囦欢涓婁紶澶辫触锛岃閲嶈瘯');
+ console.error('涓婁紶澶辫触:', error);
+};
+
+// 鏂囦欢绉婚櫎
+const handleRemove = (file, fileList) => {
+ form.attachments = fileList;
+ ElMessage.success('鏂囦欢宸茬Щ闄�');
+};
+
+// 鏂囦欢棰勮
+const handlePreview = (file) => {
+ const fileData = {
+ name: file.name || file.originalFilename || file.fileName,
+ url: file.url || file.previewUrl || file.downloadUrl,
+ id: file.id || file.tempId
+ };
+ if (filePreviewRef.value) {
+ filePreviewRef.value.open(fileData);
+ }
+};
+
+const handleImport = () => {
+ ElMessage.info("瀵煎叆鍔熻兘");
+};
+
+const handleOut = () => {
+ ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+const submitForm = () => {
+ if (isViewMode.value) {
+ dialogVisible.value = false;
+ return;
+ }
+ formRef.value.validate(async valid => {
+ if (valid) {
+ // 鍓嶇疆鏍¢獙锛氫笌鍚庣瑙勫垯瀵归綈锛屽噺灏戞棤鏁堣姹�
+ if (!isBalanced.value) {
+ ElMessage.error("鍊熻捶涓嶅钩琛★紝璇锋鏌ュ垎褰�");
+ return;
+ }
+
+ const validEntries = form.entries.filter(
+ entry => entry.subjectCode && (Number(entry.debit) > 0 || Number(entry.credit) > 0)
+ );
+ if (validEntries.length === 0) {
+ ElMessage.error("璇疯嚦灏戝~鍐欎竴鏉℃湁鏁堝垎褰�");
+ return;
+ }
+
+ const invalidEntry = validEntries.find(
+ entry => Number(entry.debit) > 0 && Number(entry.credit) > 0
+ );
+ if (invalidEntry) {
+ ElMessage.error("鍚屼竴鍒嗗綍涓嶈兘鍚屾椂濉啓鍊熸柟鍜岃捶鏂�");
+ return;
+ }
+
+ const summary = validEntries.find(e => e.debit > 0)?.summary || "";
+
+ // 鎻愬彇闄勪欢鐨� tempFileIds
+ const tempFileIds = (form.attachments || [])
+ .filter(item => item.tempId || item.id)
+ .map(item => item.tempId || item.id);
+
+ const voucherNo = `${form.voucherPrefix}-${form.voucherNum}`;
+ const dataToSave = {
+ voucherNo,
+ voucherDate: form.voucherDate,
+ summary,
+ creator: form.creator,
+ attachmentCount: Number(form.attachmentCount || 0),
+ remark: form.remark,
+ debit: totalDebitEntry.value,
+ credit: totalCreditEntry.value,
+ tempFileIds: tempFileIds,
+ entries: validEntries.map(entry => ({
+ subjectCode: entry.subjectCode,
+ subjectName: entry.subjectName,
+ summary: entry.summary,
+ debit: Number(entry.debit || 0),
+ credit: Number(entry.credit || 0),
+ })),
+ };
+
+ try {
+ if (isEdit.value) {
+ await updateVoucher({
+ id: currentId.value,
+ ...dataToSave,
+ });
+ ElMessage.success("缂栬緫鎴愬姛");
+ } else {
+ await addVoucher(dataToSave);
+ ElMessage.success("鏂板鎴愬姛");
+ }
+ dialogVisible.value = false;
+ await getTableData();
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+ }
+ });
+};
+
+onMounted(async () => {
+ await loadUserOptions();
+ await loadSubjectList();
+ await getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+
+ > div:first-child {
+ display: flex;
+ align-items: center;
+ }
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+
+.text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+}
+
+.text-primary {
+ color: #409eff;
+}
+
+.voucher-container {
+ background: #fff;
+ padding: 20px;
+}
+
+.voucher-header {
+ text-align: center;
+ margin-bottom: 15px;
+
+ .voucher-title {
+ font-size: 22px;
+ font-weight: bold;
+ margin: 0 0 5px 0;
+ color: #303133;
+ }
+
+ .voucher-period {
+ font-size: 14px;
+ color: #909399;
+ }
+}
+
+.voucher-info {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ padding: 0 10px;
+
+ .label {
+ font-size: 14px;
+ color: #606266;
+ }
+
+ .voucher-no-section,
+ .voucher-date-section,
+ .voucher-attachment-section {
+ display: flex;
+ align-items: center;
+ }
+}
+
+.voucher-attachment-section {
+ margin-top: 15px;
+ padding: 0 10px;
+
+ .attachment-label {
+ font-size: 14px;
+ color: #606266;
+ margin-bottom: 10px;
+ font-weight: 500;
+ }
+
+ :deep(.el-upload-list) {
+ max-height: 200px;
+ overflow-y: auto;
+ }
+}
+
+.voucher-table {
+ border: 1px solid #dcdfe6;
+ border-right: none;
+ margin-bottom: 15px;
+}
+
+.accounting-voucher {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 13px;
+
+ th,
+ td {
+ border: 1px solid #dcdfe6;
+ text-align: center;
+ padding: 0;
+ height: 36px;
+ }
+
+ & th:last-child,
+ & td:last-child {
+ border-right: none !important;
+ }
+
+ thead {
+ background-color: #f5f7fa;
+
+ th {
+ font-weight: normal;
+ color: #606266;
+ font-size: 12px;
+ }
+
+ .col-summary,
+ .col-subject {
+ font-weight: bold;
+ font-size: 13px;
+ }
+
+ .col-debit-header,
+ .col-credit-header {
+ background-color: #ecf5ff;
+ color: #409eff;
+ font-weight: bold;
+ }
+ }
+
+ .amount-header {
+ th {
+ font-size: 11px;
+ padding: 2px 0;
+ background-color: #f5f7fa;
+ }
+ }
+
+ .col-summary {
+ width: 160px;
+ min-width: 160px;
+ }
+
+ .col-subject {
+ width: 180px;
+ min-width: 180px;
+ }
+
+ .col-action {
+ width: 60px;
+ min-width: 60px;
+ text-align: center;
+ }
+
+ .amount-cell {
+ width: 24px;
+ min-width: 24px;
+ max-width: 24px;
+ padding: 0;
+ font-size: 13px;
+ font-family: 'Courier New', monospace;
+ cursor: pointer;
+ text-align: center;
+
+ &:hover {
+ background-color: #f5f7fa;
+ }
+
+ span {
+ display: block;
+ width: 100%;
+ height: 100%;
+ line-height: 36px;
+
+ &.zero {
+ color: #c0c4cc;
+ }
+ }
+ }
+
+ .debit-input-cell,
+ .credit-input-cell {
+ padding: 0;
+ background-color: #ecf5ff;
+
+ .full-width-input {
+ width: 100%;
+
+ :deep(.el-input__wrapper) {
+ padding: 0 10px;
+ box-shadow: none;
+ background-color: transparent;
+ }
+
+ input {
+ text-align: right;
+ font-size: 14px;
+ height: 34px;
+ }
+ }
+ }
+
+ tbody {
+ tr {
+ &:hover {
+ background-color: #f5f7fa;
+ }
+
+ &.selected-row {
+ background-color: #ecf5ff;
+ }
+ }
+
+ td {
+ .el-input {
+ .el-input__wrapper {
+ box-shadow: none;
+ padding: 0 5px;
+ }
+
+ input {
+ text-align: center;
+ height: 34px;
+ }
+ }
+
+ .el-select {
+ width: 100%;
+
+ .el-input__wrapper {
+ box-shadow: none;
+ }
+
+ input {
+ text-align: center;
+ height: 34px;
+ }
+ }
+ }
+
+ .col-summary {
+ .el-input input {
+ text-align: left;
+ padding-left: 10px;
+ }
+ }
+
+ .col-subject {
+ position: relative;
+
+ .el-select,
+ .el-tree-select {
+ .el-input input {
+ font-size: 12px;
+ }
+ }
+
+ .subject-name {
+ font-size: 11px;
+ color: #909399;
+ margin-top: 2px;
+ line-height: 1.2;
+ }
+ }
+ }
+
+ .total-row {
+ background-color: #fdf6ec;
+
+ td {
+ font-weight: bold;
+ }
+
+ .total-cell {
+ background-color: #fdf6ec;
+ font-weight: bold;
+ }
+ }
+}
+
+.voucher-toolbar {
+ display: flex;
+ justify-content: flex-start;
+ padding: 10px 0;
+ margin-top: 5px;
+}
+
+.voucher-footer {
+ display: flex;
+ justify-content: flex-end;
+ padding: 0 10px;
+ margin-top: 10px;
+
+ .creator-section {
+ .label {
+ font-size: 14px;
+ color: #606266;
+ }
+ }
+}
+
+:deep(.el-dialog__body) {
+ padding: 10px 20px;
+}
+</style>
--
Gitblit v1.9.3