| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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', |
| | | }, |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // é件页颿¥å£ |
| | | 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 |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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", |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // åºå®èµäº§å页æ¥è¯¢ï¼current/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, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // æ å½¢èµäº§å页æ¥è¯¢ï¼current/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, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // åè¯å页æ¥è¯¢ï¼current/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", |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | @pagination="paginationSearch" |
| | | @change="handleChange" /> |
| | | </el-dialog> |
| | | <!-- // todo éä»¶é¢è§ç¸å
³ --> |
| | | <filePreview v-if="showPreview" |
| | | ref="filePreviewRef" /> |
| | | </template> |
| | |
| | | }) |
| | | |
| | | // 详æ
模å¼ä¸å±ç¤ºâ确认âæé®ï¼å
¶å®ç±»åæ£å¸¸æ¾ç¤º |
| | | const showConfirm = computed(() => props.operationType !== 'detail') |
| | | const showConfirm = computed(() => props.operationType !== 'detail' && props.operationType !== 'view') |
| | | |
| | | const computedTitle = computed(() => { |
| | | if (typeof props.title === 'function') { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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 () => { |
| | | // èè°çº¦å®ï¼å 餿¥å£ä½¿ç¨ 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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 () => { |
| | | // èè°çº¦å®ï¼å 餿¥å£ä½¿ç¨ 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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="æ£å¸¸" :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: "æ£å¸¸", 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)] ?? "æ£å¸¸"; |
| | | |
| | | 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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="æ£å¸¸" :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æ£å¸¸ 1ä½åº */ |
| | | const STATUS_LABEL_MAP = { 0: "æ£å¸¸", 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] ?? "æ£å¸¸"; |
| | | }; |
| | | |
| | | 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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ï¼docxï¼xlsï¼xlsxï¼pptï¼pptxï¼pdfï¼txtï¼xmlï¼jpgï¼jpegï¼pngï¼gifï¼bmpï¼rarï¼zipï¼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> |