From b4128f0da8ecae56af47e805cf729a0e553f97a8 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 26 五月 2026 10:39:56 +0800
Subject: [PATCH] 富边电子 1.迁移财务模块

---
 src/views/financialManagement/receivable/salesOut.vue       |  180 
 src/components/AttachmentUpload/image/index.vue             |  335 +
 src/views/financialManagement/payable/payment.vue           |  299 +
 src/api/financialManagement/voucher.js                      |   54 
 src/views/financialManagement/generalLedger/index.vue       |  498 ++
 src/views/financialManagement/voucher/index.vue             | 1186 ++++++
 src/api/financialManagement/ledger.js                       |   19 
 src/views/financialManagement/payable/input-invoice.vue     |  945 +++++
 src/components/Dialog/FileList.vue                          |  263 +
 src/views/financialManagement/assets/intangibleAssets.vue   |  480 ++
 src/views/financialManagement/receivable/reconciliation.vue |  738 +++
 src/components/Dialog/FileListDialog.vue                    |    1 
 src/views/financialManagement/voucher/detailLedger.vue      |  309 +
 src/views/financialManagement/voucher/generalLedger.vue     |  312 +
 src/api/financialManagement/accountSubject.js               |   46 
 src/api/financialManagement/fixedAsset.js                   |   50 
 src/views/financialManagement/receivable/receipt.vue        |  855 ++++
 src/views/financialManagement/receivable/invoiceApply.vue   |  902 ++++
 src/views/financialManagement/assets/fixedAssets.vue        |  482 ++
 src/views/financialManagement/receivable/salesReturn.vue    |  171 
 src/views/financialManagement/payable/paymentApply.vue      | 1016 +++++
 src/views/financialManagement/payable/reconciliation.vue    |  766 ++++
 src/api/basicData/common.js                                 |   25 
 src/views/financialManagement/payable/purchaseIn.vue        |  212 +
 src/api/financialManagement/intangibleAsset.js              |   50 
 src/api/basicData/storageAttachment.js                      |   29 
 src/components/AttachmentUpload/file/index.vue              |  309 +
 src/views/financialManagement/receivable/outputInvoice.vue  |  608 +++
 src/components/Dialog/FormDialog.vue                        |    2 
 src/views/financialManagement/payable/purchaseReturn.vue    |  198 +
 30 files changed, 11,339 insertions(+), 1 deletions(-)

diff --git a/src/api/basicData/common.js b/src/api/basicData/common.js
new file mode 100644
index 0000000..547c1e1
--- /dev/null
+++ b/src/api/basicData/common.js
@@ -0,0 +1,25 @@
+import request from '@/utils/request'
+
+// 閫氱敤涓婁紶鎺ュ彛锛屾敮鎸� FormData 鎵归噺浼犳枃浠�
+export function uploadFile(data) {
+  return request({
+    url: '/common/upload',
+    method: 'post',
+    data,
+    headers: {
+      'Content-Type': 'multipart/form-data',
+    },
+  })
+}
+
+// 閫氱敤涓婁紶鎺ュ彛锛屾敮鎸� FormData 鎵归噺浼犳枃浠�,姘镐笉杩囨湡锛屾厧鐢�
+export function uploadPublicFile(data) {
+  return request({
+    url: '/common/public/upload',
+    method: 'post',
+    data,
+    headers: {
+      'Content-Type': 'multipart/form-data',
+    },
+  })
+}
diff --git a/src/api/basicData/storageAttachment.js b/src/api/basicData/storageAttachment.js
new file mode 100644
index 0000000..3e241f6
--- /dev/null
+++ b/src/api/basicData/storageAttachment.js
@@ -0,0 +1,29 @@
+// 闄勪欢椤甸潰鎺ュ彛
+import request from '@/utils/request'
+
+// 闄勪欢鏌ヨ
+export function attachmentList(query) {
+    return request({
+        url: '/storageAttachment/list',
+        method: 'get',
+        params: query
+    })
+}
+
+// 闄勪欢鏂板
+export function createAttachment(data) {
+    return request({
+        url: '/storageAttachment/add',
+        method: 'post',
+        data
+    })
+}
+
+// 闄勪欢鍒犻櫎
+export function deleteAttachment(data) {
+    return request({
+        url: '/storageAttachment/delete',
+        method: 'delete',
+        data
+    })
+}
diff --git a/src/api/financialManagement/accountSubject.js b/src/api/financialManagement/accountSubject.js
new file mode 100644
index 0000000..e54de63
--- /dev/null
+++ b/src/api/financialManagement/accountSubject.js
@@ -0,0 +1,46 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鎬诲笎绉戠洰鍒楄〃
+export function listAccountSubject(query) {
+  return request({
+    url: "/accountSubject/list",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏂板鎬诲笎绉戠洰
+export function addAccountSubject(data) {
+  return request({
+    url: "/accountSubject/add",
+    method: "post",
+    data: data,
+  });
+}
+
+// 淇敼鎬诲笎绉戠洰
+export function updateAccountSubject(data) {
+  return request({
+    url: "/accountSubject/edit",
+    method: "put",
+    data: data,
+  });
+}
+
+// 鍒犻櫎鎬诲笎绉戠洰
+export function delAccountSubject(ids) {
+  return request({
+    url: "/accountSubject/remove/" + ids,
+    method: "delete",
+  });
+}
+
+// 瀵煎嚭鎬诲笎绉戠洰
+export function exportAccountSubject(data) {
+  return request({
+    url: "/accountSubject/export",
+    method: "post",
+    data: data,
+    responseType: "blob",
+  });
+}
diff --git a/src/api/financialManagement/fixedAsset.js b/src/api/financialManagement/fixedAsset.js
new file mode 100644
index 0000000..5c28db4
--- /dev/null
+++ b/src/api/financialManagement/fixedAsset.js
@@ -0,0 +1,50 @@
+import request from "@/utils/request";
+
+// 鍥哄畾璧勪骇鍒嗛〉鏌ヨ锛坈urrent/size锛�
+export function listFixedAssetPage(params) {
+  return request({
+    url: "/financial/fixedAsset/page",
+    method: "get",
+    params,
+  });
+}
+
+// 鏂板鍥哄畾璧勪骇
+export function addFixedAsset(data) {
+  return request({
+    url: "/financial/fixedAsset/add",
+    method: "post",
+    data,
+  });
+}
+
+// 淇敼鍥哄畾璧勪骇
+export function updateFixedAsset(data) {
+  return request({
+    url: "/financial/fixedAsset/update",
+    method: "put",
+    data,
+  });
+}
+
+// 鍒犻櫎鍥哄畾璧勪骇锛堝悗绔姹� ids=1&ids=2 褰㈠紡锛�
+export function deleteFixedAsset(ids) {
+  const idList = Array.isArray(ids) ? ids : [ids];
+  const query = idList
+    .filter(id => id !== undefined && id !== null && id !== "")
+    .map(id => `ids=${encodeURIComponent(id)}`)
+    .join("&");
+  return request({
+    url: `/financial/fixedAsset/delete?${query}`,
+    method: "delete",
+  });
+}
+
+// 鎶樻棫璁℃彁锛坽} 琛ㄧず鍏ㄩ儴鍦ㄧ敤璧勪骇锛�
+export function depreciateFixedAsset(data = {}) {
+  return request({
+    url: "/financial/fixedAsset/depreciate",
+    method: "post",
+    data,
+  });
+}
diff --git a/src/api/financialManagement/intangibleAsset.js b/src/api/financialManagement/intangibleAsset.js
new file mode 100644
index 0000000..802e649
--- /dev/null
+++ b/src/api/financialManagement/intangibleAsset.js
@@ -0,0 +1,50 @@
+import request from "@/utils/request";
+
+// 鏃犲舰璧勪骇鍒嗛〉鏌ヨ锛坈urrent/size锛�
+export function listIntangibleAssetPage(params) {
+  return request({
+    url: "/financial/intangibleAsset/page",
+    method: "get",
+    params,
+  });
+}
+
+// 鏂板鏃犲舰璧勪骇
+export function addIntangibleAsset(data) {
+  return request({
+    url: "/financial/intangibleAsset/add",
+    method: "post",
+    data,
+  });
+}
+
+// 淇敼鏃犲舰璧勪骇
+export function updateIntangibleAsset(data) {
+  return request({
+    url: "/financial/intangibleAsset/update",
+    method: "put",
+    data,
+  });
+}
+
+// 鍒犻櫎鏃犲舰璧勪骇锛堝悗绔姹� ids=1&ids=2 褰㈠紡锛�
+export function deleteIntangibleAsset(ids) {
+  const idList = Array.isArray(ids) ? ids : [ids];
+  const query = idList
+    .filter(id => id !== undefined && id !== null && id !== "")
+    .map(id => `ids=${encodeURIComponent(id)}`)
+    .join("&");
+  return request({
+    url: `/financial/intangibleAsset/delete?${query}`,
+    method: "delete",
+  });
+}
+
+// 鎽婇攢璁℃彁锛坽} 琛ㄧず鍏ㄩ儴鍦ㄧ敤璧勪骇锛�
+export function amortizeIntangibleAsset(data = {}) {
+  return request({
+    url: "/financial/intangibleAsset/amortize",
+    method: "post",
+    data,
+  });
+}
diff --git a/src/api/financialManagement/ledger.js b/src/api/financialManagement/ledger.js
new file mode 100644
index 0000000..17e62fc
--- /dev/null
+++ b/src/api/financialManagement/ledger.js
@@ -0,0 +1,19 @@
+import request from "@/utils/request";
+
+// 绉戠洰鎬昏处
+export function getGeneralLedger(params) {
+  return request({
+    url: "/financial/ledger/general",
+    method: "get",
+    params,
+  });
+}
+
+// 绉戠洰鏄庣粏璐�
+export function getDetailLedger(params) {
+  return request({
+    url: "/financial/ledger/detail",
+    method: "get",
+    params,
+  });
+}
diff --git a/src/api/financialManagement/voucher.js b/src/api/financialManagement/voucher.js
new file mode 100644
index 0000000..ccb0908
--- /dev/null
+++ b/src/api/financialManagement/voucher.js
@@ -0,0 +1,54 @@
+import request from "@/utils/request";
+
+// 鍑瘉鍒嗛〉鏌ヨ锛坈urrent/size + 杩囨护鏉′欢锛�
+export function listVoucherPage(params) {
+  return request({
+    url: "/financial/voucher/page",
+    method: "get",
+    params,
+  });
+}
+
+// 鏂板鍑瘉
+export function addVoucher(data) {
+  return request({
+    url: "/financial/voucher/add",
+    method: "post",
+    data,
+  });
+}
+
+// 淇敼鍑瘉锛堜粎鏈繃璐︼級
+export function updateVoucher(data) {
+  return request({
+    url: "/financial/voucher/update",
+    method: "put",
+    data,
+  });
+}
+
+// 杩囪处
+export function postVoucher(data) {
+  return request({
+    url: "/financial/voucher/post",
+    method: "post",
+    data,
+  });
+}
+
+// 浣滃簾
+export function cancelVoucher(data) {
+  return request({
+    url: "/financial/voucher/cancel",
+    method: "post",
+    data,
+  });
+}
+
+// 璇︽儏
+export function getVoucherDetail(id) {
+  return request({
+    url: `/financial/voucher/detail/${id}`,
+    method: "get",
+  });
+}
diff --git a/src/components/AttachmentUpload/file/index.vue b/src/components/AttachmentUpload/file/index.vue
new file mode 100644
index 0000000..1e4508c
--- /dev/null
+++ b/src/components/AttachmentUpload/file/index.vue
@@ -0,0 +1,309 @@
+<script setup>
+import { UploadFilled } from '@element-plus/icons-vue'
+import { uploadFile } from '@/api/basicData/common'
+
+const props = defineProps({
+  fileList: {
+    type: Array,
+    default: () => [],
+  },
+  index: {
+    type: Number,
+    default: -1,
+  },
+  childrenKey: {
+    type: String,
+    default: 'files',
+  },
+  limit: {
+    type: Number,
+    default: 10,
+  },
+  fileSize: {
+    type: Number,
+    default: 50,
+  },
+  fileType: {
+    type: Array,
+    default: () => [],
+  },
+  buttonText: {
+    type: String,
+    default: '鍗曞嚮閫夋嫨鏂囦欢',
+  },
+  disabled: {
+    type: Boolean,
+    default: false,
+  },
+  uploadFieldName: {
+    type: String,
+    default: 'files',
+  },
+})
+
+const emit = defineEmits(['update:fileList', 'change'])
+const { proxy } = getCurrentInstance()
+
+const uploadRef = ref()
+const uploadQueueTimer = ref(null)
+const uploading = ref(false)
+const queuedUidSet = ref(new Set())
+const innerList = ref([])
+
+function readListFromProps() {
+  if (props.index > -1) {
+    const row = props.fileList?.[props.index]
+    return Array.isArray(row?.[props.childrenKey]) ? row[props.childrenKey] : []
+  }
+  return Array.isArray(props.fileList) ? props.fileList : []
+}
+
+watch(
+  () => props.fileList,
+  () => {
+    innerList.value = [...readListFromProps()]
+  },
+  { deep: true, immediate: true },
+)
+
+const currentList = computed({
+  get() {
+    return innerList.value
+  },
+  set(value) {
+    const nextList = Array.isArray(value) ? value : []
+    innerList.value = nextList
+
+    if (props.index > -1) {
+      const nextModelValue = Array.isArray(props.fileList) ? [...props.fileList] : []
+      const currentRow = nextModelValue[props.index] || {}
+      nextModelValue[props.index] = {
+        ...currentRow,
+        [props.childrenKey]: nextList,
+      }
+      emit('update:fileList', nextModelValue)
+      emit('change', nextList, nextModelValue)
+      return
+    }
+
+    emit('update:fileList', nextList)
+    emit('change', nextList, nextList)
+  },
+})
+
+const displayFileList = computed(() => {
+  return currentList.value.map((item, index) => ({
+    uid: getItemUid(item, index),
+    name: getItemName(item, index),
+    url: getItemUrl(item),
+    status: 'success',
+    rawData: item,
+  }))
+})
+
+const uploadTip = computed(() => {
+  if (!props.fileType.length) return `鍗曚釜鏂囦欢涓嶈秴杩� ${props.fileSize}MB`
+  return `鏀寔 ${props.fileType.join('/')}锛屽崟涓枃浠朵笉瓒呰繃 ${props.fileSize}MB`
+})
+
+function getItemUid(item, index) {
+  if (item?.id !== undefined && item?.id !== null) return `${item.id}`
+  return `${getItemName(item, index)}-${getItemUrl(item) || index}`
+}
+
+function getItemUrl(item) {
+  if (!item) return ''
+  if (typeof item === 'string') return item
+  return item.url || item.downloadURL || item.previewURL || item.previewUrl || ''
+}
+
+function getItemName(item, index = 0) {
+  if (!item) return `file-${index + 1}`
+  if (typeof item === 'string') return `file-${index + 1}`
+  return item.name || item.originalFilename || item.fileName || item.uidFilename || `file-${index + 1}`
+}
+
+function normalizeResponseItem(item, index) {
+  if (typeof item === 'string') {
+    return {
+      name: `file-${currentList.value.length + index + 1}`,
+      url: item,
+    }
+  }
+  return Object.assign({}, item, {
+    url: item.url || item.downloadURL || item.previewURL || item.previewUrl || '',
+    name: item.name || item.originalFilename || item.fileName || item.uidFilename || `file-${currentList.value.length + index + 1}`,
+  })
+}
+
+function extractResponseArray(response) {
+  if (Array.isArray(response)) return response
+  if (Array.isArray(response?.data)) return response.data
+  if (Array.isArray(response?.data?.data)) return response.data.data
+  if (Array.isArray(response?.payload)) return response.payload
+  if (Array.isArray(response?.payload?.data)) return response.payload.data
+  if (Array.isArray(response?.rows)) return response.rows
+  if (Array.isArray(response?.result)) return response.result
+  return []
+}
+
+function validateFile(rawFile) {
+  const extension = rawFile.name.includes('.')
+    ? rawFile.name.slice(rawFile.name.lastIndexOf('.') + 1).toLowerCase()
+    : ''
+
+  if (props.fileType.length) {
+    const isValidType = props.fileType.some((type) => {
+      const normalizedType = String(type).toLowerCase()
+      return rawFile.type.toLowerCase().includes(normalizedType) || extension === normalizedType
+    })
+    if (!isValidType) {
+      proxy.$modal.msgError(`璇蜂笂浼� ${props.fileType.join('/')} 鏍煎紡鐨勬枃浠禶)
+      return false
+    }
+  }
+
+  const isWithinSize = rawFile.size / 1024 / 1024 <= props.fileSize
+  if (!isWithinSize) {
+    proxy.$modal.msgError(`鏂囦欢澶у皬涓嶈兘瓒呰繃 ${props.fileSize}MB`)
+    return false
+  }
+
+  return true
+}
+
+function scheduleUpload(uploadFiles) {
+  clearTimeout(uploadQueueTimer.value)
+  uploadQueueTimer.value = setTimeout(() => {
+    const readyFiles = uploadFiles.filter((file) => file.raw && !queuedUidSet.value.has(file.uid))
+    if (!readyFiles.length) return
+
+    const remainCount = props.limit - currentList.value.length
+    if (remainCount <= 0) {
+      proxy.$modal.msgError(`鏈�澶氫笂浼� ${props.limit} 涓枃浠禶)
+      uploadRef.value?.clearFiles()
+      return
+    }
+
+    const selectedFiles = readyFiles.slice(0, remainCount)
+    if (selectedFiles.length < readyFiles.length) {
+      proxy.$modal.msgWarning(`鏈�澶氫笂浼� ${props.limit} 涓枃浠讹紝瓒呭嚭閮ㄥ垎宸插拷鐣)
+    }
+
+    selectedFiles.forEach((file) => queuedUidSet.value.add(file.uid))
+    uploadSelectedFiles(selectedFiles)
+  }, 0)
+}
+
+async function uploadSelectedFiles(files) {
+  const validFiles = files.filter((file) => validateFile(file.raw))
+  const invalidFiles = files.filter((file) => !validFiles.includes(file))
+
+  invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+
+  if (!validFiles.length) {
+    uploadRef.value?.clearFiles()
+    return
+  }
+
+  const formData = new FormData()
+  validFiles.forEach((file) => {
+    formData.append(props.uploadFieldName, file.raw)
+  })
+
+  uploading.value = true
+  proxy.$modal.loading('鏂囦欢涓婁紶涓紝璇风◢鍊�...')
+
+  try {
+    const response = await uploadFile(formData)
+    const responseList = extractResponseArray(response).map((item, index) => normalizeResponseItem(item, index))
+
+    if (!responseList.length) {
+      proxy.$modal.msgError('涓婁紶鎺ュ彛鏈繑鍥炴暟缁勬暟鎹�')
+      return
+    }
+
+    currentList.value = [...currentList.value, ...responseList]
+    proxy.$modal.msgSuccess('涓婁紶鎴愬姛')
+  } catch (error) {
+    proxy.$modal.msgError(error?.message || '涓婁紶澶辫触')
+  } finally {
+    validFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+    invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+    uploadRef.value?.clearFiles()
+    uploading.value = false
+    proxy.$modal.closeLoading()
+  }
+}
+
+function handleChange(file, uploadFiles) {
+  if (props.disabled || uploading.value) return
+  scheduleUpload(uploadFiles)
+}
+
+function handleRemove(file) {
+  const targetUrl = file.url || getItemUrl(file.rawData)
+  const nextList = currentList.value.filter((item, index) => {
+    const itemUrl = getItemUrl(item)
+    const itemName = getItemName(item, index)
+    return !(itemUrl === targetUrl && itemName === file.name)
+  })
+  currentList.value = nextList
+}
+
+function handleExceed() {
+  proxy.$modal.msgError(`鏈�澶氫笂浼� ${props.limit} 涓枃浠禶)
+}
+
+function openFile(file) {
+  const fileUrl = file.url || getItemUrl(file.rawData)
+  if (!fileUrl) return
+  window.open(fileUrl, '_blank')
+}
+
+onBeforeUnmount(() => {
+  clearTimeout(uploadQueueTimer.value)
+})
+</script>
+
+<template>
+  <div class="attachment-upload-file">
+    <el-upload
+      ref="uploadRef"
+      drag
+      :auto-upload="false"
+      :multiple="true"
+      :show-file-list="true"
+      :file-list="displayFileList"
+      :disabled="disabled || uploading"
+      :limit="limit"
+      :on-change="handleChange"
+      :on-remove="handleRemove"
+      :on-exceed="handleExceed"
+      :on-preview="openFile"
+    >
+      <el-icon class="upload-drag-icon"><UploadFilled /></el-icon>
+      <div class="el-upload__text">
+        灏嗘枃浠舵嫋鍒版澶勶紝鎴� <em>{{ buttonText }}</em>
+      </div>
+      <div class="upload-tip">{{ uploadTip }}</div>
+    </el-upload>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.attachment-upload-file {
+  width: 100%;
+}
+
+.upload-drag-icon {
+  font-size: 40px;
+  color: var(--el-text-color-secondary);
+}
+
+.upload-tip {
+  margin-top: 8px;
+  color: var(--el-text-color-secondary);
+  font-size: 12px;
+}
+</style>
diff --git a/src/components/AttachmentUpload/image/index.vue b/src/components/AttachmentUpload/image/index.vue
new file mode 100644
index 0000000..8243f9c
--- /dev/null
+++ b/src/components/AttachmentUpload/image/index.vue
@@ -0,0 +1,335 @@
+<script setup>
+import {Plus} from '@element-plus/icons-vue'
+import {uploadFile} from '@/api/basicData/common'
+
+const props = defineProps({
+  fileList: {
+    type: Array,
+    default: () => [],
+  },
+  index: {
+    type: Number,
+    default: -1,
+  },
+  childrenKey: {
+    type: String,
+    default: 'images',
+  },
+  limit: {
+    type: Number,
+    default: 10,
+  },
+  fileSize: {
+    type: Number,
+    default: 10,
+  },
+  fileType: {
+    type: Array,
+    default: () => ['png', 'jpg', 'jpeg', 'webp'],
+  },
+  buttonText: {
+    type: String,
+    default: '涓婁紶鍥剧墖',
+  },
+  disabled: {
+    type: Boolean,
+    default: false,
+  },
+  uploadFieldName: {
+    type: String,
+    default: 'files',
+  },
+})
+
+const emit = defineEmits(['update:fileList', 'change'])
+const {proxy} = getCurrentInstance()
+
+const uploadRef = ref()
+const previewVisible = ref(false)
+const previewUrl = ref('')
+const uploadQueueTimer = ref(null)
+const uploading = ref(false)
+const queuedUidSet = ref(new Set())
+
+const currentList = computed({
+  get() {
+    if (props.index > -1) {
+      const row = props.fileList?.[props.index]
+      return Array.isArray(row?.[props.childrenKey]) ? row[props.childrenKey] : []
+    }
+    return Array.isArray(props.fileList) ? props.fileList : []
+  },
+  set(value) {
+    const nextList = Array.isArray(value) ? value : []
+    if (props.index > -1) {
+      const nextModelValue = Array.isArray(props.fileList) ? [...props.fileList] : []
+      const currentRow = nextModelValue[props.index] || {}
+      nextModelValue[props.index] = {
+        ...currentRow,
+        [props.childrenKey]: nextList,
+      }
+      emit('update:fileList', nextModelValue)
+      emit('change', nextList, nextModelValue)
+      return
+    }
+    emit('update:fileList', nextList)
+    emit('change', nextList, nextList)
+  },
+})
+
+const displayFileList = computed(() => {
+  return currentList.value.map((item, index) => ({
+    uid: getItemUid(item, index),
+    name: getItemName(item, index),
+    url: getItemUrl(item),
+    status: 'success',
+    rawData: item,
+  }))
+})
+
+const uploadTip = computed(() => {
+  return `鏀寔 ${props.fileType.join('/')}锛屽崟寮犱笉瓒呰繃 ${props.fileSize}MB锛屾渶澶氫笂浼� ${props.limit} 寮犲浘鐗嘸
+})
+
+function getItemUid(item, index) {
+  if (item?.id !== undefined && item?.id !== null) return `${item.id}`
+  return `${getItemName(item, index)}-${getItemUrl(item) || index}`
+}
+
+function getItemUrl(item) {
+  if (!item) return ''
+  if (typeof item === 'string') return item
+  return item.url || item.previewURL || ''
+}
+
+function getItemName(item, index = 0) {
+  if (!item) return `image-${index + 1}`
+  if (typeof item === 'string') return `image-${index + 1}`
+  return item.name || item.fileName || item.originalFilename || `image-${index + 1}`
+}
+
+function normalizeResponseItem(item, index) {
+  if (typeof item === 'string') {
+    return {
+      name: `image-${currentList.value.length + index + 1}`,
+      url: item,
+    }
+  }
+  return Object.assign({}, item, {
+    url: item.url || item.previewURL || item.previewUrl || '',
+    name: item.name || item.originalFilename || item.fileName || `image-${currentList.value.length + index + 1}`,
+  })
+}
+
+function extractResponseArray(response) {
+  if (Array.isArray(response)) return response
+  if (Array.isArray(response?.data)) return response.data
+  if (Array.isArray(response?.data?.data)) return response.data.data
+  if (Array.isArray(response?.payload)) return response.payload
+  if (Array.isArray(response?.payload?.data)) return response.payload.data
+  if (Array.isArray(response?.rows)) return response.rows
+  if (Array.isArray(response?.result)) return response.result
+  return []
+}
+
+function validateFile(rawFile) {
+  let isValidType = false
+  const extension = rawFile.name.includes('.')
+      ? rawFile.name.slice(rawFile.name.lastIndexOf('.') + 1).toLowerCase()
+      : ''
+
+  if (props.fileType.length) {
+    isValidType = props.fileType.some((type) => {
+      const normalizedType = String(type).toLowerCase()
+      return rawFile.type.toLowerCase().includes(normalizedType) || extension === normalizedType
+    })
+  } else {
+    isValidType = rawFile.type.includes('image')
+  }
+
+  if (!isValidType) {
+    proxy.$modal.msgError(`璇蜂笂浼� ${props.fileType.join('/')} 鏍煎紡鐨勫浘鐗嘸)
+    return false
+  }
+
+  const isWithinSize = rawFile.size / 1024 / 1024 <= props.fileSize
+  if (!isWithinSize) {
+    proxy.$modal.msgError(`鍥剧墖澶у皬涓嶈兘瓒呰繃 ${props.fileSize}MB`)
+    return false
+  }
+
+  return true
+}
+
+function scheduleUpload(uploadFiles) {
+  clearTimeout(uploadQueueTimer.value)
+  uploadQueueTimer.value = setTimeout(() => {
+    const readyFiles = uploadFiles.filter((file) => file.raw && !queuedUidSet.value.has(file.uid))
+    if (!readyFiles.length) return
+
+    const remainCount = props.limit - currentList.value.length
+    if (remainCount <= 0) {
+      proxy.$modal.msgError(`鏈�澶氫笂浼� ${props.limit} 寮犲浘鐗嘸)
+      uploadRef.value?.clearFiles()
+      return
+    }
+
+    const selectedFiles = readyFiles.slice(0, remainCount)
+    if (selectedFiles.length < readyFiles.length) {
+      proxy.$modal.msgWarning(`鏈�澶氫笂浼� ${props.limit} 寮犲浘鐗囷紝瓒呭嚭閮ㄥ垎宸插拷鐣)
+    }
+
+    selectedFiles.forEach((file) => queuedUidSet.value.add(file.uid))
+    uploadSelectedFiles(selectedFiles)
+  }, 0)
+}
+
+async function uploadSelectedFiles(files) {
+  const validFiles = files.filter((file) => validateFile(file.raw))
+  const invalidFiles = files.filter((file) => !validFiles.includes(file))
+
+  invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+
+  if (!validFiles.length) {
+    uploadRef.value?.clearFiles()
+    return
+  }
+
+  const formData = new FormData()
+  validFiles.forEach((file) => {
+    formData.append(props.uploadFieldName, file.raw)
+  })
+
+  uploading.value = true
+  proxy.$modal.loading('鍥剧墖涓婁紶涓紝璇风◢鍊�...')
+
+  try {
+    const response = await uploadFile(formData)
+    const responseList = extractResponseArray(response).map((item, index) => normalizeResponseItem(item, index))
+
+    if (!responseList.length) {
+      proxy.$modal.msgError('涓婁紶鎺ュ彛鏈繑鍥炴暟缁勬暟鎹�')
+      return
+    }
+    console.log('responseList', responseList)
+
+    currentList.value = [...currentList.value, ...responseList]
+    console.log('currentList.value', currentList.value)
+    proxy.$modal.msgSuccess('涓婁紶鎴愬姛')
+  } catch (error) {
+    proxy.$modal.msgError(error?.message || '涓婁紶澶辫触')
+  } finally {
+    validFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+    invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+    uploadRef.value?.clearFiles()
+    uploading.value = false
+    proxy.$modal.closeLoading()
+  }
+}
+
+function handleChange(file, uploadFiles) {
+  if (props.disabled || uploading.value) return
+  scheduleUpload(uploadFiles)
+}
+
+function handleRemove(file) {
+  const targetUrl = file.url || getItemUrl(file.rawData)
+  const nextList = currentList.value.filter((item, index) => {
+    const itemUrl = getItemUrl(item)
+    const itemName = getItemName(item, index)
+    return !(itemUrl === targetUrl && itemName === file.name)
+  })
+  currentList.value = nextList
+}
+
+function handlePreview(file) {
+  previewUrl.value = file.url || getItemUrl(file.rawData)
+  previewVisible.value = true
+}
+
+function handleExceed() {
+  proxy.$modal.msgError(`鏈�澶氫笂浼� ${props.limit} 寮犲浘鐗嘸)
+}
+
+onBeforeUnmount(() => {
+  clearTimeout(uploadQueueTimer.value)
+})
+</script>
+
+<template>
+  <div class="attachment-upload-image">
+    <el-upload
+        ref="uploadRef"
+        :auto-upload="false"
+        :multiple="true"
+        :show-file-list="true"
+        :file-list="displayFileList"
+        list-type="picture-card"
+        accept="image/*"
+        :disabled="disabled || uploading"
+        :limit="limit"
+        :on-change="handleChange"
+        :on-remove="handleRemove"
+        :on-preview="handlePreview"
+        :on-exceed="handleExceed"
+    >
+      <div class="upload-trigger">
+        <el-icon>
+          <Plus/>
+        </el-icon>
+        <span>{{ buttonText }}</span>
+      </div>
+    </el-upload>
+
+    <div class="upload-tip">
+      {{ uploadTip }}
+    </div>
+
+    <el-dialog v-model="previewVisible" title="鍥剧墖棰勮" width="720px" append-to-body>
+      <img class="preview-image" :src="previewUrl" alt="preview"/>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.attachment-upload-image {
+  width: 100%;
+}
+
+.upload-trigger {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  color: var(--el-text-color-secondary);
+  font-size: 12px;
+  line-height: 1.2;
+}
+
+.upload-tip {
+  margin-top: 8px;
+  color: var(--el-text-color-secondary);
+  font-size: 12px;
+}
+
+.preview-image {
+  display: block;
+  max-width: 100%;
+  margin: 0 auto;
+}
+
+:deep(.el-upload-list--picture-card) {
+  margin: 0;
+}
+
+:deep(.el-upload--picture-card) {
+  width: 132px;
+  height: 132px;
+}
+
+:deep(.el-upload-list--picture-card .el-upload-list__item) {
+  width: 132px;
+  height: 132px;
+}
+</style>
diff --git a/src/components/Dialog/FileList.vue b/src/components/Dialog/FileList.vue
new file mode 100644
index 0000000..b0e78cf
--- /dev/null
+++ b/src/components/Dialog/FileList.vue
@@ -0,0 +1,263 @@
+<template>
+  <el-dialog v-model="isShow"
+             :title="title"
+             :width="width"
+             @close="handleClose"
+             class="attachment-dialog">
+    <!-- 宸ュ叿鏍� -->
+    <div v-if="editable"
+         class="toolbar">
+      <el-button type="primary"
+                 size="small"
+                 @click="handleUpload">
+        涓婁紶闄勪欢
+      </el-button>
+    </div>
+    <!-- 涓婁紶缁勪欢寮圭獥 -->
+    <el-dialog v-model="uploadDialogVisible"
+               title="涓婁紶闄勪欢"
+               width="50%"
+               @close="closeUpload">
+      <AttachmentUpload v-model:file-list="newFileList" />
+      <template #footer>
+        <el-button @click="saveUpload">淇濆瓨</el-button>
+        <el-button @click="closeUpload">鍏抽棴</el-button>
+      </template>
+    </el-dialog>
+    <!-- 鏂囦欢鍒楄〃琛ㄦ牸 -->
+    <div class="table-container">
+      <el-table :data="tableData"
+                border
+                class="attachment-table"
+                :height="tableData.length > 0 ? 'auto' : '120px'">
+        <el-table-column label="闄勪欢鍚嶇О"
+                         prop="originalFilename"
+                         show-overflow-tooltip />
+        <el-table-column v-if="showActions"
+                         fixed="right"
+                         label="鎿嶄綔"
+                         :width="150"
+                         align="center">
+          <template #default="scope">
+            <el-button link
+                       type="primary"
+                       size="small"
+                       class="download-link"
+                       @click="previewFile(scope.row.previewURL)">
+              棰勮
+            </el-button>
+            <el-button link
+                       type="primary"
+                       size="small"
+                       class="download-link"
+                       @click="downloadFile(scope.row.downloadURL)">
+              涓嬭浇
+            </el-button>
+            <el-button v-if="editable"
+                       link
+                       type="danger"
+                       size="small"
+                       @click="handleDelete(scope.row)">
+              鍒犻櫎
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </el-dialog>
+  <filePreview ref="filePreviewRef" />
+</template>
+
+<script setup>
+import { ElMessage } from 'element-plus'
+  import { ref, computed, getCurrentInstance, onMounted, watch } from "vue";
+  import AttachmentUpload from "@/components/AttachmentUpload/file/index.vue";
+  import {
+    attachmentList,
+    deleteAttachment,
+    createAttachment,
+  } from "@/api/basicData/storageAttachment.js";
+  import filePreview from '@/components/filePreview/index.vue'
+  const filePreviewRef = ref()
+
+  const props = defineProps({
+    visible: {
+      type: Boolean,
+      required: true,
+    },
+    recordType: {
+      type: String,
+      default: "",
+      required: true,
+    },
+    recordId: {
+      type: Number,
+      default: 0,
+      required: true,
+    },
+    title: {
+      type: String,
+      default: "闄勪欢",
+    },
+    width: {
+      type: String,
+      default: "50%",
+    },
+    showActions: {
+      type: Boolean,
+      default: true,
+    },
+    editable: {
+      type: Boolean,
+      default: true,
+    },
+  });
+
+  const emit = defineEmits(["close", "download", "upload", "delete"]);
+
+  const { proxy } = getCurrentInstance();
+  const tableData = ref([]);
+  const uploadDialogVisible = ref(false);
+  const newFileList = ref([]);
+
+  const isShow = computed({
+    get() {
+      return props.visible;
+    },
+    set(val) {
+      emit("update:visible", val);
+    },
+  });
+
+  const handleClose = () => {
+    isShow.value = false;
+  };
+
+  // 棰勮鏂囦欢
+  const previewFile = (url) => {
+    if (url) {
+      filePreviewRef.value.open(url)
+    } else {
+      ElMessage.warning('鏂囦欢鍦板潃鏃犳晥锛屾棤娉曢瑙�')
+    }
+  }
+
+  const handleUpload = () => {
+    uploadDialogVisible.value = true;
+  };
+
+  const saveUpload = async () => {
+    // 妫�鏌ユ槸鍚︽湁鏂颁笂浼犵殑鏂囦欢
+    if (newFileList.value.length > 0) {
+      createAttachment({
+        application: "file",
+        recordType: props.recordType,
+        recordId: props.recordId,
+        storageBlobDTOs: [...newFileList.value, ...tableData.value],
+      }).then((res) => {
+        if (res && res.code === 200) {
+          proxy?.$modal?.msgSuccess("涓婁紶鎴愬姛");
+          newFileList.value = [];
+          // 鍒锋柊鍒楄〃
+          setList();
+        }
+      }).finally(() => {
+        uploadDialogVisible.value = false;
+      })
+    }
+  }
+
+  const closeUpload = () => {
+    newFileList.value = [];
+    uploadDialogVisible.value = false;
+  };
+
+  const handleDelete = async (row, index) => {
+    deleteAttachment([row.storageAttachmentId]).then((res) => {
+      if (res && res.code === 200) {
+        proxy?.$modal?.msgSuccess("鍒犻櫎鎴愬姛");
+        setList();
+      }
+    })
+  };
+
+  const setList = () => {
+    attachmentList({
+      recordType: props.recordType,
+      recordId: props.recordId,
+    }).then(res => {
+      tableData.value = (res && res.data) || [];
+    });
+  };
+
+  const downloadFile = url => {
+    window.open(url, "_blank");
+  };
+  onMounted(() => {
+    setList();
+  });
+</script>
+
+<style scoped>
+  .attachment-dialog {
+    border-radius: 12px;
+  }
+
+  .toolbar {
+    margin-bottom: 16px;
+    text-align: right;
+  }
+
+  .table-container {
+    max-height: 40vh;
+    overflow-y: auto;
+    min-height: 120px;
+    padding-bottom: 16px;
+    box-sizing: border-box;
+    will-change: scroll-position;
+    transform: translateZ(0);
+    -webkit-overflow-scrolling: touch;
+  }
+
+  :deep(.el-table) {
+    margin-bottom: 0;
+  }
+
+  :deep(.el-table__body-wrapper) {
+    overflow-y: auto;
+    will-change: transform;
+    transform: translateZ(0);
+  }
+
+  :deep(.el-table__body tr) {
+    transition: none;
+  }
+
+  :deep(.el-dialog__footer) {
+    padding-top: 12px;
+    border-top: 1px solid #e9ecef;
+  }
+
+  .attachment-table {
+    border-radius: 8px;
+  }
+
+  :deep(.el-dialog__header) {
+    background-color: #f8f9fa;
+    border-bottom: 1px solid #e9ecef;
+    padding: 16px 20px;
+  }
+
+  :deep(.el-dialog__title) {
+    font-size: 16px;
+    font-weight: 600;
+  }
+
+  :deep(.el-dialog__body) {
+    padding: 16px 20px;
+  }
+
+  :deep(.el-table__empty-text) {
+    color: #999;
+  }
+</style>
\ No newline at end of file
diff --git a/src/components/Dialog/FileListDialog.vue b/src/components/Dialog/FileListDialog.vue
index fc82411..6fea795 100644
--- a/src/components/Dialog/FileListDialog.vue
+++ b/src/components/Dialog/FileListDialog.vue
@@ -77,6 +77,7 @@
                 @pagination="paginationSearch"
                 @change="handleChange" />
   </el-dialog>
+<!-- // todo 闄勪欢棰勮鐩稿叧 -->
   <filePreview v-if="showPreview"
                ref="filePreviewRef" />
 </template>
diff --git a/src/components/Dialog/FormDialog.vue b/src/components/Dialog/FormDialog.vue
index 8b657de..b60bfb4 100644
--- a/src/components/Dialog/FormDialog.vue
+++ b/src/components/Dialog/FormDialog.vue
@@ -55,7 +55,7 @@
 })
 
 // 璇︽儏妯″紡涓嶅睍绀衡�滅‘璁も�濇寜閽紝鍏跺畠绫诲瀷姝e父鏄剧ず
-const showConfirm = computed(() => props.operationType !== 'detail')
+const showConfirm = computed(() => props.operationType !== 'detail' && props.operationType !== 'view')
 
 const computedTitle = computed(() => {
   if (typeof props.title === 'function') {
diff --git a/src/views/financialManagement/assets/fixedAssets.vue b/src/views/financialManagement/assets/fixedAssets.vue
new file mode 100644
index 0000000..24b4cc3
--- /dev/null
+++ b/src/views/financialManagement/assets/fixedAssets.vue
@@ -0,0 +1,482 @@
+<template>
+  <div class="app-container">
+    <el-form :model="filters" :inline="true">
+      <el-form-item label="璧勪骇缂栧彿:">
+        <el-input v-model="filters.assetCode" placeholder="璇疯緭鍏ヨ祫浜х紪鍙�" clearable style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="璧勪骇鍚嶇О:">
+        <el-input v-model="filters.assetName" placeholder="璇疯緭鍏ヨ祫浜у悕绉�" clearable style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="璧勪骇绫诲埆:">
+        <el-select v-model="filters.category" placeholder="璇烽�夋嫨绫诲埆" clearable style="width: 150px;">
+          <el-option label="鎴垮眿寤虹瓚" value="building" />
+          <el-option label="鏈哄櫒璁惧" value="machine" />
+          <el-option label="杩愯緭宸ュ叿" value="vehicle" />
+          <el-option label="鐢靛瓙璁惧" value="electronic" />
+          <el-option label="鍔炲叕瀹跺叿" value="furniture" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="鐘舵��:">
+        <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+          <el-option label="鍦ㄧ敤" value="in_use" />
+          <el-option label="闂茬疆" value="idle" />
+          <el-option label="鎶ュ簾" value="scrapped" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div>
+          <el-statistic title="璧勪骇鍘熷�煎悎璁�" :value="totalOriginalValue" precision="2" prefix="楼" />
+          <el-statistic title="绱鎶樻棫鍚堣" :value="totalDepreciation" precision="2" prefix="楼" style="margin-left: 30px;" />
+          <el-statistic title="鍑�鍊煎悎璁�" :value="totalNetValue" precision="2" prefix="楼" style="margin-left: 30px;" />
+        </div>
+        <div>
+          <el-button type="primary" @click="add" icon="Plus">鏂板璧勪骇</el-button>
+          <el-button type="warning" @click="handleDepreciation" icon="Money">鎶樻棫璁℃彁</el-button>
+          <!-- <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button> -->
+        </div>
+      </div>
+      <PIMTable
+        rowKey="id"
+        isSelection
+        :column="columns"
+        :tableData="dataList"
+        :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+        @selection-change="handleSelectionChange"
+        @pagination="changePage"
+      >
+        <template #originalValue="{ row }">
+          <span class="text-primary">楼{{ formatMoney(row.originalValue) }}</span>
+        </template>
+        <template #accumulatedDepreciation="{ row }">
+          <span class="text-warning">楼{{ formatMoney(row.accumulatedDepreciation) }}</span>
+        </template>
+        <template #netValue="{ row }">
+          <span class="text-success">楼{{ formatMoney(row.netValue) }}</span>
+        </template>
+        <template #category="{ row }">
+          <el-tag>{{ getCategoryLabel(row.category) }}</el-tag>
+        </template>
+        <template #status="{ row }">
+          <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
+        </template>
+        <template #operation="{ row }">
+          <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+          <el-button type="primary" link @click="edit(row)">缂栬緫</el-button>
+          <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+        </template>
+      </PIMTable>
+    </div>
+
+    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
+      <el-form :model="form" :rules="rules" :disabled="isView" ref="formRef" label-width="120px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="璧勪骇缂栧彿" prop="assetCode">
+              <el-input v-model="form.assetCode" placeholder="绯荤粺鑷姩鐢熸垚" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="璧勪骇鍚嶇О" prop="assetName">
+              <el-input v-model="form.assetName" placeholder="璇疯緭鍏ヨ祫浜у悕绉�" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="璧勪骇绫诲埆" prop="category">
+              <el-select v-model="form.category" placeholder="璇烽�夋嫨璧勪骇绫诲埆" style="width: 100%;">
+                <el-option label="鎴垮眿寤虹瓚" value="building" />
+                <el-option label="鏈哄櫒璁惧" value="machine" />
+                <el-option label="杩愯緭宸ュ叿" value="vehicle" />
+                <el-option label="鐢靛瓙璁惧" value="electronic" />
+                <el-option label="鍔炲叕瀹跺叿" value="furniture" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="瑙勬牸鍨嬪彿" prop="specification">
+              <el-input v-model="form.specification" placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="璐疆鏃ユ湡" prop="purchaseDate">
+              <el-date-picker v-model="form.purchaseDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="璧勪骇鍘熷��" prop="originalValue">
+              <el-input-number v-model="form.originalValue" :min="0" :precision="2" style="width: 100%;" @change="calculateNetValue" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="浣跨敤骞撮檺" prop="usefulLife">
+              <el-input-number v-model="form.usefulLife" :min="1" :max="50" style="width: 100%;" />
+              <span style="margin-left: 10px;">骞�</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="娈嬪�肩巼" prop="residualRate">
+              <el-input-number v-model="form.residualRate" :min="0" :max="10" :precision="2" style="width: 100%;" />
+              <span style="margin-left: 10px;">%</span>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="绱鎶樻棫">
+              <el-input v-model="form.accumulatedDepreciation" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="璧勪骇鍑�鍊�">
+              <el-input v-model="form.netValue" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="瀛樻斁鍦扮偣" prop="location">
+              <el-input v-model="form.location" placeholder="璇疯緭鍏ュ瓨鏀惧湴鐐�" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="浣跨敤閮ㄩ棬" prop="department">
+              <el-input v-model="form.department" placeholder="璇疯緭鍏ヤ娇鐢ㄩ儴闂�" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="淇濈浜�" prop="keeper">
+              <el-input v-model="form.keeper" placeholder="璇疯緭鍏ヤ繚绠′汉" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鐘舵��" prop="status">
+              <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%;">
+                <el-option label="鍦ㄧ敤" value="in_use" />
+                <el-option label="闂茬疆" value="idle" />
+                <el-option label="鎶ュ簾" value="scrapped" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="澶囨敞" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ娉�" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button v-if="!isView" type="primary" @click="submitForm">纭畾</el-button>
+        <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+      </template>
+    </FormDialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import {
+  listFixedAssetPage,
+  addFixedAsset,
+  updateFixedAsset,
+  deleteFixedAsset,
+  depreciateFixedAsset,
+} from "@/api/financialManagement/fixedAsset";
+
+defineOptions({
+  name: "鍥哄畾璧勪骇",
+});
+
+const filters = reactive({
+  assetCode: "",
+  assetName: "",
+  category: "",
+  status: "",
+});
+
+const pagination = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+const columns = [
+  { label: "璧勪骇缂栧彿", prop: "assetCode", width: "130" },
+  { label: "璧勪骇鍚嶇О", prop: "assetName", width: "150" },
+  { label: "璧勪骇绫诲埆", prop: "category", dataType: "slot", slot: "category" },
+  { label: "瑙勬牸鍨嬪彿", prop: "specification", width: "120" },
+  { label: "璧勪骇鍘熷��", prop: "originalValue", dataType: "slot", slot: "originalValue" },
+  { label: "绱鎶樻棫", prop: "accumulatedDepreciation", dataType: "slot", slot: "accumulatedDepreciation" },
+  { label: "璧勪骇鍑�鍊�", prop: "netValue", dataType: "slot", slot: "netValue" },
+  { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status" },
+  { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "180", fixed: "right" },
+];
+
+const dataList = ref([]);
+const multipleList = ref([]);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isEdit = ref(false);
+const isView = ref(false);
+const currentId = ref(null);
+const selectedIds = computed(() =>
+  multipleList.value
+    .map(item => item?.id)
+    .filter(id => id !== undefined && id !== null && id !== "")
+);
+
+const createDefaultForm = () => ({
+  assetCode: "",
+  assetName: "",
+  category: "",
+  specification: "",
+  purchaseDate: "",
+  originalValue: 0,
+  usefulLife: 5,
+  residualRate: 5,
+  accumulatedDepreciation: 0,
+  netValue: 0,
+  location: "",
+  department: "",
+  keeper: "",
+  status: "in_use",
+  remark: "",
+});
+
+const form = reactive({
+  ...createDefaultForm(),
+});
+
+const rules = {
+  assetName: [{ required: true, message: "璇疯緭鍏ヨ祫浜у悕绉�", trigger: "blur" }],
+  category: [{ required: true, message: "璇烽�夋嫨璧勪骇绫诲埆", trigger: "change" }],
+  purchaseDate: [{ required: true, message: "璇烽�夋嫨璐疆鏃ユ湡", trigger: "change" }],
+  originalValue: [{ required: true, message: "璇疯緭鍏ヨ祫浜у師鍊�", trigger: "blur" }],
+  usefulLife: [{ required: true, message: "璇疯緭鍏ヤ娇鐢ㄥ勾闄�", trigger: "blur" }],
+};
+
+const totalOriginalValue = computed(() => {
+  return dataList.value.reduce((sum, item) => sum + Number(item.originalValue), 0);
+});
+
+const totalDepreciation = computed(() => {
+  return dataList.value.reduce((sum, item) => sum + Number(item.accumulatedDepreciation), 0);
+});
+
+const totalNetValue = computed(() => {
+  return dataList.value.reduce((sum, item) => sum + Number(item.netValue), 0);
+});
+
+const formatMoney = (value) => {
+  if (value === undefined || value === null) return "0.00";
+  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getCategoryLabel = (category) => {
+  const map = {
+    building: "鎴垮眿寤虹瓚",
+    machine: "鏈哄櫒璁惧",
+    vehicle: "杩愯緭宸ュ叿",
+    electronic: "鐢靛瓙璁惧",
+    furniture: "鍔炲叕瀹跺叿",
+  };
+  return map[category] || category;
+};
+
+const getStatusLabel = (status) => {
+  const key = String(status || "").toLowerCase();
+  const map = { in_use: "鍦ㄧ敤", idle: "闂茬疆", repair: "缁翠慨涓�", scrapped: "鎶ュ簾" };
+  return map[key] || status;
+};
+
+const getStatusType = (status) => {
+  const key = String(status || "").toLowerCase();
+  const map = { in_use: "success", idle: "warning", repair: "warning", scrapped: "info" };
+  return map[key] || "";
+};
+
+const calculateNetValue = () => {
+  const originalValue = Number(form.originalValue || 0);
+  const accumulatedDepreciation = Number(form.accumulatedDepreciation || 0);
+  form.netValue = Number((originalValue - accumulatedDepreciation).toFixed(2));
+};
+
+// 鑱旇皟绾﹀畾锛氬垎椤靛弬鏁板浐瀹氫负 current/size锛岃繑鍥� data.records/data.total
+const getTableData = async () => {
+  try {
+    const { data } = await listFixedAssetPage({
+      current: pagination.currentPage,
+      size: pagination.pageSize,
+      assetCode: filters.assetCode,
+      assetName: filters.assetName,
+      category: filters.category,
+      status: filters.status,
+    });
+    dataList.value = data?.records || [];
+    multipleList.value = [];
+    pagination.total = Number(data?.total || 0);
+  } catch (error) {
+    // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+  }
+};
+
+const handleSelectionChange = (selectionList) => {
+  multipleList.value = selectionList;
+};
+
+const resetFilters = () => {
+  filters.assetCode = "";
+  filters.assetName = "";
+  filters.category = "";
+  filters.status = "";
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const changePage = ({ current, size }) => {
+  pagination.currentPage = current;
+  pagination.pageSize = size;
+  getTableData();
+};
+
+const buildAssetCode = () => `GD${Date.now().toString().slice(-10)}`;
+
+const add = () => {
+  isEdit.value = false;
+  isView.value = false;
+  currentId.value = null;
+  dialogTitle.value = "鏂板鍥哄畾璧勪骇";
+  Object.assign(form, createDefaultForm(), {
+    assetCode: buildAssetCode(),
+    purchaseDate: new Date().toISOString().split('T')[0],
+  });
+  dialogVisible.value = true;
+};
+
+const edit = (row) => {
+  isEdit.value = true;
+  isView.value = false;
+  currentId.value = row.id;
+  dialogTitle.value = "缂栬緫鍥哄畾璧勪骇";
+  Object.assign(form, createDefaultForm(), row);
+  dialogVisible.value = true;
+};
+
+const view = (row) => {
+  edit(row);
+  isView.value = true;
+};
+
+const handleDelete = (row) => {
+  ElMessageBox.confirm("纭鍒犻櫎璇ュ浐瀹氳祫浜у悧锛�", "鎻愮ず", {
+    confirmButtonText: "纭畾",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(async () => {
+    // 鑱旇皟绾﹀畾锛氬垹闄ゆ帴鍙d娇鐢� ids=1&ids=2
+    await deleteFixedAsset([row.id]);
+    if (dataList.value.length === 1 && pagination.currentPage > 1) {
+      pagination.currentPage -= 1;
+    }
+    ElMessage.success("鍒犻櫎鎴愬姛");
+    await getTableData();
+  });
+};
+
+const handleDepreciation = () => {
+  const ids = selectedIds.value;
+  const confirmText = ids.length
+    ? `纭瀵归�変腑鐨� ${ids.length} 鏉¤祫浜ц繘琛屾湰鏈堟姌鏃ц鎻愬悧锛焋
+    : "纭杩涜鏈湀鎶樻棫璁℃彁鍚楋紵";
+  ElMessageBox.confirm(confirmText, "鎻愮ず", {
+    confirmButtonText: "纭",
+    cancelButtonText: "鍙栨秷",
+    type: "info",
+  }).then(async () => {
+    await depreciateFixedAsset({ ids });
+    ElMessage.success("鎶樻棫璁℃彁瀹屾垚");
+    await getTableData();
+  });
+};
+
+const handleOut = () => {
+  ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+const submitForm = () => {
+  if (isView.value) {
+    dialogVisible.value = false;
+    return;
+  }
+  formRef.value.validate(async valid => {
+    if (valid) {
+      try {
+        calculateNetValue();
+        const payload = { ...form };
+        if (isEdit.value) {
+          payload.id = currentId.value;
+          await updateFixedAsset(payload);
+          ElMessage.success("缂栬緫鎴愬姛");
+        } else {
+          await addFixedAsset(payload);
+          ElMessage.success("鏂板鎴愬姛");
+        }
+        dialogVisible.value = false;
+        await getTableData();
+      } catch (error) {
+        // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+      }
+    }
+  });
+};
+
+onMounted(() => {
+  getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+
+  > div:first-child {
+    display: flex;
+    align-items: center;
+  }
+}
+
+.text-primary {
+  color: #409eff;
+  font-weight: bold;
+}
+
+.text-warning {
+  color: #e6a23c;
+  font-weight: bold;
+}
+
+.text-success {
+  color: #67c23a;
+  font-weight: bold;
+}
+</style>
diff --git a/src/views/financialManagement/assets/intangibleAssets.vue b/src/views/financialManagement/assets/intangibleAssets.vue
new file mode 100644
index 0000000..4642166
--- /dev/null
+++ b/src/views/financialManagement/assets/intangibleAssets.vue
@@ -0,0 +1,480 @@
+<template>
+  <div class="app-container">
+    <el-form :model="filters" :inline="true">
+      <el-form-item label="璧勪骇缂栧彿:">
+        <el-input v-model="filters.assetCode" placeholder="璇疯緭鍏ヨ祫浜х紪鍙�" clearable style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="璧勪骇鍚嶇О:">
+        <el-input v-model="filters.assetName" placeholder="璇疯緭鍏ヨ祫浜у悕绉�" clearable style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="璧勪骇绫诲埆:">
+        <el-select v-model="filters.category" placeholder="璇烽�夋嫨绫诲埆" clearable style="width: 150px;">
+          <el-option label="涓撳埄鏉�" value="patent" />
+          <el-option label="鍟嗘爣鏉�" value="trademark" />
+          <el-option label="钁椾綔鏉�" value="copyright" />
+          <el-option label="杞欢" value="software" />
+          <el-option label="鍦熷湴浣跨敤鏉�" value="land" />
+          <el-option label="鍏朵粬" value="other" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="鐘舵��:">
+        <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+          <el-option label="鍦ㄧ敤" value="in_use" />
+          <el-option label="闂茬疆" value="idle" />
+          <el-option label="宸叉憡閿�瀹屾瘯" value="amortized" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div>
+          <el-statistic title="璧勪骇鍘熷�煎悎璁�" :value="totalOriginalValue" precision="2" prefix="楼" />
+          <el-statistic title="绱鎽婇攢鍚堣" :value="totalAmortization" precision="2" prefix="楼" style="margin-left: 30px;" />
+          <el-statistic title="鍑�鍊煎悎璁�" :value="totalNetValue" precision="2" prefix="楼" style="margin-left: 30px;" />
+        </div>
+        <div>
+          <el-button type="primary" @click="add" icon="Plus">鏂板璧勪骇</el-button>
+          <el-button type="warning" @click="handleAmortization" icon="Money">鎽婇攢璁℃彁</el-button>
+          <!-- <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button> -->
+        </div>
+      </div>
+      <PIMTable
+        rowKey="id"
+        isSelection
+        :column="columns"
+        :tableData="dataList"
+        :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+        @selection-change="handleSelectionChange"
+        @pagination="changePage"
+      >
+        <template #originalValue="{ row }">
+          <span class="text-primary">楼{{ formatMoney(row.originalValue) }}</span>
+        </template>
+        <template #accumulatedAmortization="{ row }">
+          <span class="text-warning">楼{{ formatMoney(row.accumulatedAmortization) }}</span>
+        </template>
+        <template #netValue="{ row }">
+          <span class="text-success">楼{{ formatMoney(row.netValue) }}</span>
+        </template>
+        <template #category="{ row }">
+          <el-tag>{{ getCategoryLabel(row.category) }}</el-tag>
+        </template>
+        <template #status="{ row }">
+          <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
+        </template>
+        <template #operation="{ row }">
+          <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+          <el-button type="primary" link @click="edit(row)">缂栬緫</el-button>
+          <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+        </template>
+      </PIMTable>
+    </div>
+
+    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
+      <el-form :model="form" :rules="rules" :disabled="isView" ref="formRef" label-width="120px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="璧勪骇缂栧彿" prop="assetCode">
+              <el-input v-model="form.assetCode" placeholder="绯荤粺鑷姩鐢熸垚" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="璧勪骇鍚嶇О" prop="assetName">
+              <el-input v-model="form.assetName" placeholder="璇疯緭鍏ヨ祫浜у悕绉�" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="璧勪骇绫诲埆" prop="category">
+              <el-select v-model="form.category" placeholder="璇烽�夋嫨璧勪骇绫诲埆" style="width: 100%;">
+                <el-option label="涓撳埄鏉�" value="patent" />
+                <el-option label="鍟嗘爣鏉�" value="trademark" />
+                <el-option label="钁椾綔鏉�" value="copyright" />
+                <el-option label="杞欢" value="software" />
+                <el-option label="鍦熷湴浣跨敤鏉�" value="land" />
+                <el-option label="鍏朵粬" value="other" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="璇佷功缂栧彿" prop="certificateNo">
+              <el-input v-model="form.certificateNo" placeholder="璇疯緭鍏ヨ瘉涔︾紪鍙�" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鍙栧緱鏃ユ湡" prop="acquisitionDate">
+              <el-date-picker v-model="form.acquisitionDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="璧勪骇鍘熷��" prop="originalValue">
+              <el-input-number v-model="form.originalValue" :min="0" :precision="2" style="width: 100%;" @change="calculateNetValue" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鎽婇攢骞撮檺" prop="amortizationPeriod">
+              <el-input-number v-model="form.amortizationPeriod" :min="1" :max="50" style="width: 100%;" />
+              <span style="margin-left: 10px;">骞�</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="娈嬪�肩巼" prop="residualRate">
+              <el-input-number v-model="form.residualRate" :min="0" :max="10" :precision="2" style="width: 100%;" />
+              <span style="margin-left: 10px;">%</span>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="绱鎽婇攢">
+              <el-input v-model="form.accumulatedAmortization" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="璧勪骇鍑�鍊�">
+              <el-input v-model="form.netValue" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鏈夋晥鏈熻嚦" prop="validityDate">
+              <el-date-picker v-model="form.validityDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鐘舵��" prop="status">
+              <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%;">
+                <el-option label="鍦ㄧ敤" value="in_use" />
+                <el-option label="闂茬疆" value="idle" />
+                <el-option label="宸叉憡閿�瀹屾瘯" value="amortized" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="璧勪骇鎻忚堪" prop="description">
+          <el-input v-model="form.description" type="textarea" :rows="3" placeholder="璇疯緭鍏ヨ祫浜ф弿杩�" />
+        </el-form-item>
+        <el-form-item label="澶囨敞" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button v-if="!isView" type="primary" @click="submitForm">纭畾</el-button>
+        <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+      </template>
+    </FormDialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import {
+  listIntangibleAssetPage,
+  addIntangibleAsset,
+  updateIntangibleAsset,
+  deleteIntangibleAsset,
+  amortizeIntangibleAsset,
+} from "@/api/financialManagement/intangibleAsset";
+
+defineOptions({
+  name: "鏃犲舰璧勪骇",
+});
+
+const filters = reactive({
+  assetCode: "",
+  assetName: "",
+  category: "",
+  status: "",
+});
+
+const pagination = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+const columns = [
+  { label: "璧勪骇缂栧彿", prop: "assetCode", width: "130" },
+  { label: "璧勪骇鍚嶇О", prop: "assetName", width: "150" },
+  { label: "璧勪骇绫诲埆", prop: "category", dataType: "slot", slot: "category" },
+  { label: "璇佷功缂栧彿", prop: "certificateNo", width: "150" },
+  { label: "璧勪骇鍘熷��", prop: "originalValue", dataType: "slot", slot: "originalValue" },
+  { label: "绱鎽婇攢", prop: "accumulatedAmortization", dataType: "slot", slot: "accumulatedAmortization" },
+  { label: "璧勪骇鍑�鍊�", prop: "netValue", dataType: "slot", slot: "netValue" },
+  { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status" },
+  { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "180", fixed: "right" },
+];
+
+const dataList = ref([]);
+const multipleList = ref([]);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isEdit = ref(false);
+const isView = ref(false);
+const currentId = ref(null);
+const selectedIds = computed(() =>
+  multipleList.value
+    .map(item => item?.id)
+    .filter(id => id !== undefined && id !== null && id !== "")
+);
+
+const createDefaultForm = () => ({
+  assetCode: "",
+  assetName: "",
+  category: "",
+  certificateNo: "",
+  acquisitionDate: "",
+  originalValue: 0,
+  amortizationPeriod: 10,
+  residualRate: 0,
+  accumulatedAmortization: 0,
+  netValue: 0,
+  validityDate: "",
+  status: "in_use",
+  description: "",
+  remark: "",
+});
+
+const form = reactive({
+  ...createDefaultForm(),
+});
+
+const rules = {
+  assetName: [{ required: true, message: "璇疯緭鍏ヨ祫浜у悕绉�", trigger: "blur" }],
+  category: [{ required: true, message: "璇烽�夋嫨璧勪骇绫诲埆", trigger: "change" }],
+  acquisitionDate: [{ required: true, message: "璇烽�夋嫨鍙栧緱鏃ユ湡", trigger: "change" }],
+  originalValue: [{ required: true, message: "璇疯緭鍏ヨ祫浜у師鍊�", trigger: "blur" }],
+  amortizationPeriod: [{ required: true, message: "璇疯緭鍏ユ憡閿�骞撮檺", trigger: "blur" }],
+};
+
+const totalOriginalValue = computed(() => {
+  return dataList.value.reduce((sum, item) => sum + Number(item.originalValue), 0);
+});
+
+const totalAmortization = computed(() => {
+  return dataList.value.reduce((sum, item) => sum + Number(item.accumulatedAmortization), 0);
+});
+
+const totalNetValue = computed(() => {
+  return dataList.value.reduce((sum, item) => sum + Number(item.netValue), 0);
+});
+
+const formatMoney = (value) => {
+  if (value === undefined || value === null) return "0.00";
+  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getCategoryLabel = (category) => {
+  const map = {
+    patent: "涓撳埄鏉�",
+    trademark: "鍟嗘爣鏉�",
+    copyright: "钁椾綔鏉�",
+    software: "杞欢",
+    land: "鍦熷湴浣跨敤鏉�",
+    other: "鍏朵粬",
+  };
+  return map[category] || category;
+};
+
+const getStatusLabel = (status) => {
+  const key = String(status || "").toLowerCase();
+  const map = {
+    in_use: "鍦ㄧ敤",
+    idle: "闂茬疆",
+    expired: "宸插埌鏈�",
+    amortized: "宸叉憡閿�瀹屾瘯",
+  };
+  return map[key] || status;
+};
+
+const getStatusType = (status) => {
+  const key = String(status || "").toLowerCase();
+  const map = { in_use: "success", idle: "warning", expired: "warning", amortized: "info" };
+  return map[key] || "";
+};
+
+const calculateNetValue = () => {
+  const originalValue = Number(form.originalValue || 0);
+  const accumulatedAmortization = Number(form.accumulatedAmortization || 0);
+  form.netValue = Number((originalValue - accumulatedAmortization).toFixed(2));
+};
+
+// 鑱旇皟绾﹀畾锛氬垎椤靛弬鏁板浐瀹氫负 current/size锛岃繑鍥� data.records/data.total
+const getTableData = async () => {
+  try {
+    const { data } = await listIntangibleAssetPage({
+      current: pagination.currentPage,
+      size: pagination.pageSize,
+      assetCode: filters.assetCode,
+      assetName: filters.assetName,
+      category: filters.category,
+      status: filters.status,
+    });
+    dataList.value = data?.records || [];
+    multipleList.value = [];
+    pagination.total = Number(data?.total || 0);
+  } catch (error) {
+    // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+  }
+};
+
+const handleSelectionChange = (selectionList) => {
+  multipleList.value = selectionList;
+};
+
+const resetFilters = () => {
+  filters.assetCode = "";
+  filters.assetName = "";
+  filters.category = "";
+  filters.status = "";
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const changePage = ({ current, size }) => {
+  pagination.currentPage = current;
+  pagination.pageSize = size;
+  getTableData();
+};
+
+const buildAssetCode = () => `WX${Date.now().toString().slice(-10)}`;
+
+const add = () => {
+  isEdit.value = false;
+  isView.value = false;
+  currentId.value = null;
+  dialogTitle.value = "鏂板鏃犲舰璧勪骇";
+  Object.assign(form, createDefaultForm(), {
+    assetCode: buildAssetCode(),
+    acquisitionDate: new Date().toISOString().split('T')[0],
+  });
+  dialogVisible.value = true;
+};
+
+const edit = (row) => {
+  isEdit.value = true;
+  isView.value = false;
+  currentId.value = row.id;
+  dialogTitle.value = "缂栬緫鏃犲舰璧勪骇";
+  Object.assign(form, createDefaultForm(), row);
+  dialogVisible.value = true;
+};
+
+const view = (row) => {
+  edit(row);
+  isView.value = true;
+};
+
+const handleDelete = (row) => {
+  ElMessageBox.confirm("纭鍒犻櫎璇ユ棤褰㈣祫浜у悧锛�", "鎻愮ず", {
+    confirmButtonText: "纭畾",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(async () => {
+    // 鑱旇皟绾﹀畾锛氬垹闄ゆ帴鍙d娇鐢� ids=1&ids=2
+    await deleteIntangibleAsset([row.id]);
+    if (dataList.value.length === 1 && pagination.currentPage > 1) {
+      pagination.currentPage -= 1;
+    }
+    ElMessage.success("鍒犻櫎鎴愬姛");
+    await getTableData();
+  });
+};
+
+const handleAmortization = () => {
+  const ids = selectedIds.value;
+  const confirmText = ids.length
+    ? `纭瀵归�変腑鐨� ${ids.length} 鏉¤祫浜ц繘琛屾湰鏈堟憡閿�璁℃彁鍚楋紵`
+    : "纭杩涜鏈湀鎽婇攢璁℃彁鍚楋紵";
+  ElMessageBox.confirm(confirmText, "鎻愮ず", {
+    confirmButtonText: "纭",
+    cancelButtonText: "鍙栨秷",
+    type: "info",
+  }).then(async () => {
+    await amortizeIntangibleAsset({ ids });
+    ElMessage.success("鎽婇攢璁℃彁瀹屾垚");
+    await getTableData();
+  });
+};
+
+const handleOut = () => {
+  ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+const submitForm = () => {
+  if (isView.value) {
+    dialogVisible.value = false;
+    return;
+  }
+  formRef.value.validate(async valid => {
+    if (valid) {
+      try {
+        calculateNetValue();
+        const payload = { ...form };
+        if (isEdit.value) {
+          payload.id = currentId.value;
+          await updateIntangibleAsset(payload);
+          ElMessage.success("缂栬緫鎴愬姛");
+        } else {
+          await addIntangibleAsset(payload);
+          ElMessage.success("鏂板鎴愬姛");
+        }
+        dialogVisible.value = false;
+        await getTableData();
+      } catch (error) {
+        // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+      }
+    }
+  });
+};
+
+onMounted(() => {
+  getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+
+  > div:first-child {
+    display: flex;
+    align-items: center;
+  }
+}
+
+.text-primary {
+  color: #409eff;
+  font-weight: bold;
+}
+
+.text-warning {
+  color: #e6a23c;
+  font-weight: bold;
+}
+
+.text-success {
+  color: #67c23a;
+  font-weight: bold;
+}
+</style>
diff --git a/src/views/financialManagement/generalLedger/index.vue b/src/views/financialManagement/generalLedger/index.vue
new file mode 100644
index 0000000..a7b1d30
--- /dev/null
+++ b/src/views/financialManagement/generalLedger/index.vue
@@ -0,0 +1,498 @@
+<template>
+  <div class="app-container">
+    <el-form :model="filters"
+             :inline="true">
+      <el-form-item label="绉戠洰缂栫爜:">
+        <el-input v-model="filters.subjectCode"
+                  placeholder="璇疯緭鍏ョ鐩紪鐮�"
+                  clearable
+                  style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="绉戠洰鍚嶇О:">
+        <el-input v-model="filters.subjectName"
+                  placeholder="璇疯緭鍏ョ鐩悕绉�"
+                  clearable
+                  style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="绉戠洰绫诲瀷:">
+        <el-select v-model="filters.subjectType"
+                   placeholder="璇烽�夋嫨"
+                   clearable
+                   style="width: 200px;">
+          <el-option label="璧勪骇绫�"
+                     value="璧勪骇绫�" />
+          <el-option label="璐熷�虹被"
+                     value="璐熷�虹被" />
+          <el-option label="鏉冪泭绫�"
+                     value="鏉冪泭绫�" />
+          <el-option label="鎴愭湰绫�"
+                     value="鎴愭湰绫�" />
+          <el-option label="鎹熺泭绫�"
+                     value="鎹熺泭绫�" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary"
+                   @click="getTableData">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div></div>
+        <div>
+          <el-button type="primary"
+                     @click="add"
+                     icon="Plus">鏂板</el-button>
+          <!-- <el-button @click="handleOut"
+                     icon="Download">瀵煎嚭</el-button> -->
+        </div>
+      </div>
+      <el-table ref="tableRef"
+                v-loading="loading"
+                :data="dataList"
+                row-key="id"
+                :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+                height="calc(100vh - 280px)"
+                border
+                stripe
+                highlight-current-row
+                class="subject-table">
+        <el-table-column label="绉戠洰缂栫爜" prop="subjectCode" width="140">
+          <template #default="scope">
+            <span class="subject-code">{{ scope.row.subjectCode }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="绉戠洰鍚嶇О" prop="subjectName" min-width="180">
+          <template #default="scope">
+            <span class="subject-name" :class="{ 'is-parent': scope.row.children?.length > 0 }">
+              {{ scope.row.subjectName }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="绉戠洰绫诲瀷" prop="subjectType" width="100" align="center">
+          <template #default="scope">
+            <el-tag size="small" :type="getSubjectTypeType(scope.row.subjectType)">
+              {{ scope.row.subjectType }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="浣欓鏂瑰悜" prop="balanceDirection" width="100" align="center">
+          <template #default="scope">
+            <el-tag size="small" :type="scope.row.balanceDirection === '鍊熸柟' ? 'primary' : 'danger'">
+              {{ scope.row.balanceDirection }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="鐘舵��" prop="status" width="80" align="center">
+          <template #default="scope">
+            <el-tag size="small" :type="scope.row.status === 0 || scope.row.status === '0' ? 'success' : 'info'">
+              {{ scope.row.status === 0 || scope.row.status === '0' ? '鍚敤' : '绂佺敤' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="澶囨敞" prop="remark" show-overflow-tooltip min-width="150" />
+        <el-table-column label="鎿嶄綔" align="center" fixed="right" width="240">
+          <template #default="scope">
+            <el-button link type="primary" icon="Plus" @click="addChild(scope.row)">鏂板</el-button>
+            <el-button link type="primary" icon="Edit" @click="edit(scope.row)">缂栬緫</el-button>
+            <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+    <FormDialog :title="dialogTitle"
+                v-model="dialogVisible"
+                width="600px"
+                @confirm="submitForm"
+                @cancel="dialogVisible = false">
+      <el-form :model="form"
+               :rules="rules"
+               ref="formRef"
+               label-width="100px">
+        <el-form-item label="鐖剁骇绉戠洰">
+          <el-input :model-value="parentSubjectLabel"
+                    disabled />
+        </el-form-item>
+        <el-form-item label="绉戠洰缂栫爜"
+                      prop="subjectCode">
+          <el-input v-model="form.subjectCode"
+                    placeholder="璇疯緭鍏ョ鐩紪鐮�" />
+        </el-form-item>
+        <el-form-item label="绉戠洰鍚嶇О"
+                      prop="subjectName">
+          <el-input v-model="form.subjectName"
+                    placeholder="璇疯緭鍏ョ鐩悕绉�" />
+        </el-form-item>
+        <el-form-item label="绉戠洰绫诲瀷"
+                      prop="subjectType">
+          <el-select v-model="form.subjectType"
+                     placeholder="璇烽�夋嫨绉戠洰绫诲瀷"
+                     style="width: 100%;">
+            <el-option label="璧勪骇绫�"
+                       value="璧勪骇绫�" />
+            <el-option label="璐熷�虹被"
+                       value="璐熷�虹被" />
+            <el-option label="鏉冪泭绫�"
+                       value="鏉冪泭绫�" />
+            <el-option label="鎴愭湰绫�"
+                       value="鎴愭湰绫�" />
+            <el-option label="鎹熺泭绫�"
+                       value="鎹熺泭绫�" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="浣欓鏂瑰悜"
+                      prop="balanceDirection">
+          <el-radio-group v-model="form.balanceDirection">
+            <el-radio label="鍊熸柟">鍊熸柟</el-radio>
+            <el-radio label="璐锋柟">璐锋柟</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="鐘舵��"
+                      prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio :label="0">鍚敤</el-radio>
+            <el-radio :label="1">绂佺敤</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="澶囨敞"
+                      prop="remark">
+          <el-input v-model="form.remark"
+                    type="textarea"
+                    :rows="3"
+                    placeholder="璇疯緭鍏ュ娉�" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button type="primary"
+                   @click="submitForm">纭畾</el-button>
+        <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+      </template>
+    </FormDialog>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, onMounted, getCurrentInstance, nextTick } from "vue";
+  import { ElMessage, ElMessageBox } from "element-plus";
+  import FormDialog from "@/components/Dialog/FormDialog.vue";
+  import {
+    listAccountSubject,
+    addAccountSubject,
+    updateAccountSubject,
+    delAccountSubject,
+    exportAccountSubject,
+  } from "@/api/financialManagement/accountSubject";
+
+  defineOptions({
+    name: "鎬诲笎绉戠洰",
+  });
+
+  const { proxy } = getCurrentInstance();
+
+  const filters = reactive({
+    subjectCode: "",
+    subjectName: "",
+    subjectType: "",
+  });
+
+  const pagination = reactive({
+    currentPage: 1,
+    pageSize: 10,
+    total: 0,
+  });
+
+  const columns = [
+    { label: "绉戠洰缂栫爜", prop: "subjectCode", width: "120" },
+    { label: "绉戠洰鍚嶇О", prop: "subjectName", width: "150" },
+    { label: "绉戠洰绫诲瀷", prop: "subjectType" },
+    {
+      label: "浣欓鏂瑰悜",
+      prop: "balanceDirection",
+      dataType: "tag",
+      formatData: value => {
+        if (value === "鍊熸柟") {
+          return "鍊熸柟";
+        }
+        return "璐锋柟";
+      },
+      formatType: value => {
+        if (value === "鍊熸柟") {
+          return "primary";
+        }
+        return "danger";
+      },
+    },
+    {
+      label: "鐘舵��",
+      prop: "status",
+      dataType: "tag",
+      formatData: value => {
+        if (value === 0 || value === "0") {
+          return "鍚敤";
+        }
+        return "绂佺敤";
+      },
+      formatType: value => {
+        if (value === 0 || value === "0") {
+          return "success";
+        }
+        return "info";
+      },
+    },
+
+    { label: "澶囨敞", prop: "remark", showOverflowTooltip: true },
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      fixed: "right",
+      width: "220",
+      operation: [
+        {
+          name: "鏂板",
+          type: "primary",
+          clickFun: row => {
+            addChild(row);
+          },
+        },
+        {
+          name: "缂栬緫",
+          type: "primary",
+          clickFun: row => {
+            edit(row);
+          },
+        },
+        {
+          name: "鍒犻櫎",
+          type: "danger",
+          clickFun: row => {
+            handleDelete(row);
+          },
+        },
+      ],
+    },
+  ];
+
+  const dataList = ref([]);
+  const dialogVisible = ref(false);
+  const dialogTitle = ref("");
+  const parentSubjectLabel = ref("椤剁骇绉戠洰");
+  const formRef = ref(null);
+  const tableRef = ref(null);
+  const isEdit = ref(false);
+  const loading = ref(false);
+
+  const form = reactive({
+    id: undefined,
+    parentId: null,
+    subjectCode: "",
+    subjectName: "",
+    subjectType: "",
+    balanceDirection: "鍊熸柟",
+    status: 0,
+    remark: "",
+  });
+
+  const rules = {
+    subjectCode: [{ required: true, message: "璇疯緭鍏ョ鐩紪鐮�", trigger: "blur" }],
+    subjectName: [{ required: true, message: "璇疯緭鍏ョ鐩悕绉�", trigger: "blur" }],
+    subjectType: [
+      { required: true, message: "璇烽�夋嫨绉戠洰绫诲瀷", trigger: "change" },
+    ],
+  };
+
+  const getSubjectTypeType = type => {
+    const map = {
+      璧勪骇绫�: "success",
+      璐熷�虹被: "danger",
+      鏉冪泭绫�: "warning",
+      鎴愭湰绫�: "info",
+      鎹熺泭绫�: "primary",
+    };
+    return map[type] || "";
+  };
+
+  const getTableData = () => {
+    loading.value = true;
+    const query = {
+      current: pagination.currentPage,
+      size: pagination.pageSize,
+      ...filters,
+    };
+    listAccountSubject(query).then(response => {
+      dataList.value = response.data.records || [];
+      loading.value = false;
+    }).catch(() => {
+      loading.value = false;
+    });
+  };
+
+  const resetFilters = () => {
+    filters.subjectCode = "";
+    filters.subjectName = "";
+    filters.subjectType = "";
+    pagination.currentPage = 1;
+    getTableData();
+  };
+
+  const changePage = obj => {
+    pagination.currentPage = obj.page;
+    pagination.pageSize = obj.limit;
+    getTableData();
+  };
+
+  const buildParentSubjectLabel = parentRow => {
+    if (!parentRow) {
+      return "椤剁骇绉戠洰";
+    }
+    const code = parentRow.subjectCode || "";
+    const name = parentRow.subjectName || "";
+    return `${code} ${name}`.trim();
+  };
+
+  const resetForm = ({ parentId = null, parentRow = null } = {}) => {
+    Object.assign(form, {
+      id: undefined,
+      parentId,
+      subjectCode: "",
+      subjectName: "",
+      subjectType: "",
+      balanceDirection: "鍊熸柟",
+      status: 0,
+      remark: "",
+    });
+    parentSubjectLabel.value = buildParentSubjectLabel(parentRow);
+  };
+
+  const add = () => {
+    isEdit.value = false;
+    dialogTitle.value = "鏂板绉戠洰";
+    resetForm({ parentId: null, parentRow: null });
+    dialogVisible.value = true;
+  };
+
+  const addChild = row => {
+    isEdit.value = false;
+    dialogTitle.value = "鏂板瀛愮鐩�";
+    resetForm({ parentId: row.id, parentRow: row });
+    form.subjectType = row.subjectType || "";
+    form.balanceDirection = row.balanceDirection || "鍊熸柟";
+    dialogVisible.value = true;
+  };
+
+  const findSubjectById = (nodes, id) => {
+    for (const item of nodes || []) {
+      if (item.id === id) {
+        return item;
+      }
+      if (item.children && item.children.length > 0) {
+        const found = findSubjectById(item.children, id);
+        if (found) {
+          return found;
+        }
+      }
+    }
+    return null;
+  };
+
+  const edit = row => {
+    isEdit.value = true;
+    dialogTitle.value = "缂栬緫绉戠洰";
+    Object.assign(form, row);
+    form.parentId = row.parentId ?? null;
+    const parentRow =
+      row.parentId === null || row.parentId === undefined
+        ? null
+        : findSubjectById(dataList.value, row.parentId);
+    parentSubjectLabel.value = parentRow
+      ? buildParentSubjectLabel(parentRow)
+      : row.parentId
+      ? `涓婄骇ID: ${row.parentId}`
+      : buildParentSubjectLabel(null);
+    dialogVisible.value = true;
+  };
+
+  const submitForm = () => {
+    formRef.value.validate(valid => {
+      if (valid) {
+        if (isEdit.value) {
+          updateAccountSubject(form).then(() => {
+            ElMessage.success("缂栬緫鎴愬姛");
+            dialogVisible.value = false;
+            getTableData();
+          });
+        } else {
+          addAccountSubject(form).then(() => {
+            ElMessage.success("鏂板鎴愬姛");
+            dialogVisible.value = false;
+            getTableData();
+          });
+        }
+      }
+    });
+  };
+
+  const handleDelete = row => {
+    const ids = row.id;
+    ElMessageBox.confirm("纭鍒犻櫎璇ョ鐩悧锛�", "鎻愮ず", {
+      confirmButtonText: "纭畾",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(() => {
+        return delAccountSubject(ids);
+      })
+      .then(() => {
+        ElMessage.success("鍒犻櫎鎴愬姛");
+        getTableData();
+      });
+  };
+
+  const handleOut = () => {
+    proxy.download(
+      "accountSubject/export",
+      {
+        ...filters,
+      },
+      `account_subject_${new Date().getTime()}.xlsx`
+    );
+  };
+
+  onMounted(() => {
+    getTableData();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .actions {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 15px;
+  }
+
+  .subject-table {
+    border-radius: 8px;
+    overflow: hidden;
+
+    :deep(.el-table__row) {
+      transition: background-color 0.3s;
+    }
+
+    :deep(.el-table__row:hover) {
+      background-color: #f5f7fa;
+    }
+
+    .subject-code {
+      color: #606266;
+    }
+
+    .subject-name {
+      font-weight: 500;
+
+      &.is-parent {
+        color: #409eff;
+      }
+    }
+  }
+</style>
diff --git a/src/views/financialManagement/payable/input-invoice.vue b/src/views/financialManagement/payable/input-invoice.vue
new file mode 100644
index 0000000..86ebd09
--- /dev/null
+++ b/src/views/financialManagement/payable/input-invoice.vue
@@ -0,0 +1,945 @@
+<template>
+  <div class="app-container">
+    <el-form :model="filters" :inline="true">
+      <el-form-item label="鍙戠エ鍙风爜:">
+        <el-input v-model="filters.invoiceNumber" placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�" clearable style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="渚涘簲鍟�:">
+        <el-select v-model="filters.supplierId" placeholder="璇烽�夋嫨渚涘簲鍟�" clearable filterable style="width: 200px;">
+          <el-option
+            v-for="item in supplierList"
+            :key="item.id"
+            :label="item.supplierName"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="寮�绁ㄦ棩鏈�:">
+        <el-date-picker
+          v-model="filters.dateRange"
+          type="daterange"
+          value-format="YYYY-MM-DD"
+          format="YYYY-MM-DD"
+          range-separator="鑷�"
+          start-placeholder="寮�濮嬫棩鏈�"
+          end-placeholder="缁撴潫鏃ユ湡"
+          clearable
+          style="width: 240px;"
+        />
+      </el-form-item>
+      <el-form-item label="鐘舵��:">
+        <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+          <el-option label="姝e父" :value="0" />
+          <el-option label="浣滃簾" :value="1" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div></div>
+        <div>
+          <el-button type="primary" @click="add" icon="Plus">褰曞叆鍙戠エ</el-button>
+          <el-button @click="handleExport" icon="Download">瀵煎嚭</el-button>
+        </div>
+      </div>
+      <PIMTable
+        rowKey="id"
+        :column="columns"
+        :tableData="dataList"
+        :tableLoading="tableLoading"
+        :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+        @pagination="changePage"
+      >
+        <template #amount="{ row }">
+          <span class="text-primary">楼{{ formatMoney(row.amount) }}</span>
+        </template>
+        <template #taxAmount="{ row }">
+          <span class="text-danger">楼{{ formatMoney(row.taxAmount) }}</span>
+        </template>
+        <template #totalAmount="{ row }">
+          <span class="text-success">楼{{ formatMoney(row.totalAmount) }}</span>
+        </template>
+        <template #status="{ row }">
+          <el-tag :type="getStatusType(row.status)" effect="light" round>
+            {{ getStatusLabel(row.status) }}
+          </el-tag>
+        </template>
+        <template #operation="{ row }">
+          <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+          <el-button type="warning" link @click="handleCancel(row)" v-if="isNormalStatus(row.status)">浣滃簾</el-button>
+          <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+        </template>
+      </PIMTable>
+    </div>
+
+    <FormDialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="800px"
+      :operation-type="isView ? 'detail' : ''"
+      @confirm="submitForm"
+      @cancel="closeDialog"
+    >
+      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+        <el-row v-if="isView" :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鐘舵��">
+              <el-tag :type="getStatusType(form.status)" effect="light" round>
+                {{ getStatusLabel(form.status) }}
+              </el-tag>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鍙戠エ鍙风爜" prop="invoiceNo">
+              <el-input v-model="form.invoiceNo" placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�" :disabled="isView" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="渚涘簲鍟�" prop="supplierId">
+              <el-select
+                v-model="form.supplierId"
+                placeholder="璇烽�夋嫨渚涘簲鍟�"
+                style="width: 100%;"
+                filterable
+                :disabled="isView"
+                @change="handleSupplierChange"
+              >
+                <el-option
+                  v-for="item in supplierList"
+                  :key="item.id"
+                  :label="item.supplierName"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鍏宠仈鍏ュ簱鍗�" prop="stockInRecordIds">
+              <el-input
+                :model-value="inboundBatchDisplayText"
+                placeholder="璇峰厛閫夋嫨渚涘簲鍟�"
+                readonly
+                :disabled="!form.supplierId || isView"
+                class="inbound-batch-input"
+                @click="handleInboundInputClick"
+              >
+                <template v-if="!isView" #append>
+                  <el-button
+                    :disabled="!form.supplierId"
+                    :loading="inboundBatchLoading"
+                    @click.stop="openInboundSelectDialog"
+                  >
+                    閫夋嫨
+                  </el-button>
+                </template>
+              </el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="寮�绁ㄦ棩鏈�" prop="invoiceDate">
+              <el-date-picker
+                v-model="form.invoiceDate"
+                type="date"
+                placeholder="閫夋嫨鏃ユ湡"
+                value-format="YYYY-MM-DD"
+                style="width: 100%;"
+                :disabled="isView"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鍙戠エ绫诲瀷" prop="invoiceType">
+              <el-select
+                v-model="form.invoiceType"
+                placeholder="璇烽�夋嫨鍙戠エ绫诲瀷"
+                style="width: 100%;"
+                :disabled="isView"
+              >
+                <el-option label="澧炲�肩◣涓撶敤鍙戠エ" value="澧炲�肩◣涓撶敤鍙戠エ" />
+                <el-option label="澧炲�肩◣鏅�氬彂绁�" value="澧炲�肩◣鏅�氬彂绁�" />
+                <el-option label="鐢靛瓙鍙戠エ" value="鐢靛瓙鍙戠エ" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="绋庣巼" prop="taxRate">
+              <el-select
+                v-model="form.taxRate"
+                placeholder="璇烽�夋嫨绋庣巼"
+                style="width: 100%;"
+                :disabled="isView"
+                @change="handleTaxRateChange"
+              >
+                <el-option
+                  v-for="dict in tax_rate"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="Number(dict.value)"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="閲戦(涓嶅惈绋�)" prop="amount">
+              <el-input-number
+                v-model="form.amount"
+                :min="0"
+                :precision="2"
+                style="width: 100%;"
+                :disabled="isView"
+                placeholder="鏍规嵁鍏ュ簱鍗曞惈绋庨噾棰濊嚜鍔ㄦ崲绠楋紝鍙慨鏀�"
+                @change="calculateTaxFromExclusive"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="绋庨">
+              <el-input-number
+                v-model="form.taxAmount"
+                :min="0"
+                :precision="2"
+                :controls="false"
+                style="width: 100%;"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="浠风◣鍚堣">
+              <el-input-number
+                v-model="form.totalAmount"
+                :min="0"
+                :precision="2"
+                :controls="false"
+                style="width: 100%;"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="鍙戠エ鍐呭" prop="content">
+          <el-input v-model="form.content" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ彂绁ㄥ唴瀹�" :disabled="isView" />
+        </el-form-item>
+        <el-form-item label="澶囨敞" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" :disabled="isView" />
+        </el-form-item>
+      </el-form>
+      <template v-if="!isView" #footer>
+        <el-button type="primary" :loading="submitLoading" @click="submitForm">纭畾</el-button>
+        <el-button @click="closeDialog">鍙栨秷</el-button>
+      </template>
+    </FormDialog>
+
+    <el-dialog
+      v-model="inboundSelectVisible"
+      title="閫夋嫨鍏ュ簱鍗曞彿"
+      width="1100px"
+      append-to-body
+      destroy-on-close
+      :close-on-click-modal="false"
+      @closed="handleInboundDialogClosed"
+    >
+      <el-table
+        ref="inboundTableRef"
+        v-loading="inboundBatchLoading"
+        :data="inboundBatchList"
+        row-key="id"
+        border
+        stripe
+        max-height="480"
+        @selection-change="handleInboundDialogSelectionChange"
+      >
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column prop="inboundBatches" label="鍏ュ簱鍗曞彿" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="supplierName" label="渚涘簲鍟�" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="productName" label="浜у搧鍚嶇О" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="specificationModel" label="瑙勬牸鍨嬪彿" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="purchaseContractNumber" label="閲囪喘璁㈠崟鍙�" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="inboundDate" label="鍏ュ簱鏃ユ湡" width="110" align="center" />
+        <el-table-column prop="inboundAmount" label="鍏ュ簱閲戦(鍚◣)" width="120" align="right">
+          <template #default="{ row }">楼{{ formatMoney(getInboundRowTaxInclusiveAmount(row)) }}</template>
+        </el-table-column>
+      </el-table>
+      <template #footer>
+        <el-button type="primary" @click="confirmInboundSelection">纭畾</el-button>
+        <el-button @click="inboundSelectVisible = false">鍙栨秷</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
+import {
+  getInboundBatchesBySupplier,
+  addAccountPurchaseInvoice,
+  listPageAccountPurchaseInvoice,
+  cancelAccountPurchaseInvoice,
+  deleteAccountPurchaseInvoice,
+} from "@/api/financialManagement/accountPurchaseInvoice.js";
+
+defineOptions({
+  name: "杩涢」鍙戠エ",
+});
+
+const { proxy } = getCurrentInstance();
+const { tax_rate } = proxy.useDict("tax_rate");
+
+const filters = reactive({
+  invoiceNumber: "",
+  supplierId: "",
+  dateRange: [],
+  status: "",
+});
+
+const pagination = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+const columns = [
+  { label: "鍙戠エ鍙风爜", prop: "invoiceNo", width: "140" },
+  { label: "渚涘簲鍟�", prop: "supplierName", width: "180" },
+  { label: "寮�绁ㄦ棩鏈�", prop: "invoiceDate", width: "120" },
+  { label: "閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+  { label: "绋庨", prop: "taxAmount", dataType: "slot", slot: "taxAmount" },
+  { label: "浠风◣鍚堣", prop: "totalAmount", dataType: "slot", slot: "totalAmount" },
+  { label: "鍙戠エ绫诲瀷", prop: "invoiceType", width: "130" },
+  { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status", width: "90", align: "center" },
+  { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "200", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isView = ref(false);
+const submitLoading = ref(false);
+const supplierList = ref([]);
+
+const inboundBatchList = ref([]);
+const inboundBatchOptions = ref([]);
+const inboundBatchLoading = ref(false);
+const inboundSelectVisible = ref(false);
+const inboundTableRef = ref(null);
+const dialogInboundSelection = ref([]);
+
+const STATUS_LABEL_MAP = { 0: "姝e父", 1: "浣滃簾" };
+const STATUS_TYPE_MAP = { 0: "success", 1: "info" };
+
+const form = reactive({
+  invoiceNo: "",
+  supplierId: "",
+  invoiceDate: "",
+  invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+  taxRate: 13,
+  amount: 0,
+  taxAmount: 0,
+  totalAmount: 0,
+  content: "",
+  remark: "",
+  stockInRecordIds: [],
+  inboundBatches: "",
+  storageAttachmentId: undefined,
+  status: 0,
+});
+
+const rules = {
+  invoiceNo: [{ required: true, message: "璇疯緭鍏ュ彂绁ㄥ彿鐮�", trigger: "blur" }],
+  supplierId: [{ required: true, message: "璇烽�夋嫨渚涘簲鍟�", trigger: "change" }],
+  stockInRecordIds: [{ required: true, type: "array", min: 1, message: "璇烽�夋嫨鍏宠仈鍏ュ簱鍗�", trigger: "change" }],
+  invoiceDate: [{ required: true, message: "璇烽�夋嫨寮�绁ㄦ棩鏈�", trigger: "change" }],
+  invoiceType: [{ required: true, message: "璇烽�夋嫨鍙戠エ绫诲瀷", trigger: "change" }],
+  taxRate: [{ required: true, message: "璇烽�夋嫨绋庣巼", trigger: "change" }],
+  amount: [{ required: true, message: "璇疯緭鍏ラ噾棰�", trigger: "blur" }],
+};
+
+const formatMoney = (value) => {
+  if (value === undefined || value === null) return "0.00";
+  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const normalizeStatus = (status) => {
+  if (status === undefined || status === null || status === "") return 0;
+  const num = Number(status);
+  return Number.isNaN(num) ? 0 : num;
+};
+
+const isNormalStatus = (status) => normalizeStatus(status) === 0;
+
+const getStatusLabel = (status) => STATUS_LABEL_MAP[normalizeStatus(status)] ?? "姝e父";
+
+const getStatusType = (status) => STATUS_TYPE_MAP[normalizeStatus(status)] ?? "success";
+
+const parseStockInRecordIds = (value) => {
+  if (!value) return [];
+  if (Array.isArray(value)) return value;
+  return String(value)
+    .split(/[,锛宂/)
+    .map((s) => s.trim())
+    .filter(Boolean)
+    .map((s) => (/^\d+$/.test(s) ? Number(s) : s));
+};
+
+const formatInboundBatches = (value) => {
+  if (value === undefined || value === null || value === "") return "";
+  if (Array.isArray(value)) return value.filter(Boolean).join("銆�");
+  return String(value)
+    .split(/[,锛宂/)
+    .map((s) => s.trim())
+    .filter(Boolean)
+    .join("銆�");
+};
+
+const isSameInboundId = (a, b) => String(a) === String(b);
+
+const getInboundRowId = (row) => row?.id ?? row?.stockInRecordId;
+
+/** 鍏ュ簱鍗曢噾棰濅负鍚◣浠� */
+const getInboundRowTaxInclusiveAmount = (row) =>
+  Number(row?.inboundAmount ?? row?.taxInclusivePrice ?? row?.totalAmount ?? 0);
+
+const normalizeInboundBatchOptions = (data) => {
+  const list = Array.isArray(data) ? data : [];
+  return list.map((item, index) => {
+    if (typeof item === "string" || typeof item === "number") {
+      const text = String(item);
+      return { label: text, value: text, inboundAmount: 0 };
+    }
+    const label =
+      item.inboundBatches ?? item.batchNo ?? item.inboundNo ?? item.label ?? `鍏ュ簱鍗�${index + 1}`;
+    const value = item.id ?? item.stockInRecordId ?? label;
+    return {
+      label: String(label),
+      value,
+      inboundAmount: getInboundRowTaxInclusiveAmount(item),
+    };
+  });
+};
+
+/** 涓嶅惈绋庨噾棰濆彉鏇达細绋庨銆佷环绋庡悎璁℃鍚戣绠� */
+const calculateTaxFromExclusive = () => {
+  form.taxAmount = Number((form.amount * form.taxRate / 100).toFixed(2));
+  form.totalAmount = Number((form.amount + form.taxAmount).toFixed(2));
+};
+
+/** 浠风◣鍚堣鍙樻洿锛氭寜绋庣巼鍙嶇畻涓嶅惈绋庨噾棰濄�佺◣棰� */
+const calculateTaxFromInclusive = (inclusiveTotal) => {
+  const total = Number(inclusiveTotal ?? form.totalAmount ?? 0);
+  if (total <= 0) {
+    form.amount = 0;
+    form.taxAmount = 0;
+    form.totalAmount = 0;
+    return;
+  }
+  const rate = Number(form.taxRate) / 100;
+  form.totalAmount = Number(total.toFixed(2));
+  form.amount = Number((form.totalAmount / (1 + rate)).toFixed(2));
+  form.taxAmount = Number((form.totalAmount - form.amount).toFixed(2));
+};
+
+const handleTaxRateChange = () => {
+  if (form.totalAmount > 0) {
+    calculateTaxFromInclusive(form.totalAmount);
+  } else {
+    calculateTaxFromExclusive();
+  }
+};
+
+/** 鏍规嵁宸查�夊叆搴撳崟姹囨�诲惈绋庨噾棰濓紝鍙嶇畻涓嶅惈绋庨噾棰濅笌绋庨 */
+const syncInvoiceAmount = () => {
+  const selected = form.stockInRecordIds || [];
+  const sumFromOptions = inboundBatchOptions.value
+    .filter((opt) => selected.some((id) => isSameInboundId(id, opt.value)))
+    .reduce((acc, opt) => acc + (Number(opt.inboundAmount) || 0), 0);
+
+  let taxInclusiveSum = sumFromOptions;
+  if (taxInclusiveSum <= 0 && selected.length) {
+    taxInclusiveSum = inboundBatchList.value
+      .filter((row) => selected.some((id) => isSameInboundId(id, getInboundRowId(row))))
+      .reduce((acc, row) => acc + getInboundRowTaxInclusiveAmount(row), 0);
+  }
+
+  calculateTaxFromInclusive(taxInclusiveSum > 0 ? Number(taxInclusiveSum.toFixed(2)) : 0);
+};
+
+const inboundBatchDisplayText = computed(() => {
+  if (form.inboundBatches) return form.inboundBatches;
+  const ids = form.stockInRecordIds || [];
+  if (!ids.length) return "";
+  const labels = inboundBatchOptions.value
+    .filter((opt) => ids.some((id) => isSameInboundId(id, opt.value)))
+    .map((opt) => opt.label);
+  if (labels.length) return labels.join("銆�");
+  return ids.join("銆�");
+});
+
+const normalizeTableRow = (row) => ({
+  ...row,
+  invoiceNo: row.invoiceNumber ?? row.invoiceNo,
+  invoiceDate: row.issueDate ?? row.invoiceDate,
+  amount: row.taxExclusivelPrice ?? row.amount,
+  taxAmount: row.taxPrice ?? row.taxAmount,
+  totalAmount: row.taxInclusivePrice ?? row.totalAmount,
+  content: row.invoiceContent ?? row.content,
+  status: normalizeStatus(row.status),
+  stockInRecordIds: row.stockInRecordIds ?? "",
+  inboundBatches: formatInboundBatches(row.inboundBatches),
+});
+
+const toFormNumber = (val) => {
+  const n = Number(val);
+  return Number.isFinite(n) ? n : 0;
+};
+
+const resolveFormAmounts = (row) => {
+  let amount = toFormNumber(row.taxExclusivelPrice ?? row.amount);
+  let taxAmount = toFormNumber(row.taxPrice ?? row.taxAmount);
+  let totalAmount = toFormNumber(row.taxInclusivePrice ?? row.totalAmount);
+  const taxRate = toFormNumber(row.taxRate) || 13;
+
+  if (totalAmount > 0 && amount === 0 && taxAmount === 0) {
+    amount = Number((totalAmount / (1 + taxRate / 100)).toFixed(2));
+    taxAmount = Number((totalAmount - amount).toFixed(2));
+  } else if (totalAmount > 0 && amount > 0 && taxAmount === 0) {
+    taxAmount = Number((totalAmount - amount).toFixed(2));
+  } else if (amount > 0 && taxAmount === 0 && totalAmount === 0) {
+    taxAmount = Number((amount * taxRate / 100).toFixed(2));
+    totalAmount = Number((amount + taxAmount).toFixed(2));
+  } else if (amount > 0 && taxAmount > 0 && totalAmount === 0) {
+    totalAmount = Number((amount + taxAmount).toFixed(2));
+  }
+
+  return { amount, taxAmount, totalAmount };
+};
+
+const fillFormFromRow = (row) => {
+  const stockInRecordIds = parseStockInRecordIds(row.stockInRecordIds);
+  const { amount, taxAmount, totalAmount } = resolveFormAmounts(row);
+  Object.assign(form, {
+    invoiceNo: row.invoiceNo ?? row.invoiceNumber ?? "",
+    supplierId: row.supplierId,
+    invoiceDate: row.invoiceDate ?? row.issueDate ?? "",
+    invoiceType: row.invoiceType ?? "澧炲�肩◣涓撶敤鍙戠エ",
+    taxRate: row.taxRate ?? 13,
+    amount,
+    taxAmount,
+    totalAmount,
+    content: row.content ?? row.invoiceContent ?? "",
+    remark: row.remark ?? "",
+    stockInRecordIds,
+    inboundBatches: formatInboundBatches(row.inboundBatches),
+    storageAttachmentId: row.storageAttachmentId,
+    status: normalizeStatus(row.status),
+  });
+};
+
+const buildCancelPayload = (row) => ({
+  id: row.id,
+  invoiceNumber: row.invoiceNumber ?? row.invoiceNo,
+  taxRate: row.taxRate,
+  invoiceType: row.invoiceType,
+  issueDate: row.issueDate ?? row.invoiceDate,
+  taxExclusivelPrice: row.taxExclusivelPrice ?? row.amount,
+  taxPrice: row.taxPrice ?? row.taxAmount,
+  taxInclusivePrice: row.taxInclusivePrice ?? row.totalAmount,
+  remark: row.remark ?? "",
+  invoiceContent: row.invoiceContent ?? row.content,
+  supplierId: row.supplierId,
+  storageAttachmentId: row.storageAttachmentId,
+  stockInRecordIds: row.stockInRecordIds ?? "",
+  status: 1,
+});
+
+const buildSubmitPayload = () => ({
+  invoiceNumber: form.invoiceNo,
+  supplierId: form.supplierId,
+  issueDate: form.invoiceDate,
+  invoiceType: form.invoiceType,
+  taxRate: form.taxRate,
+  taxExclusivelPrice: form.amount,
+  taxPrice: form.taxAmount,
+  taxInclusivePrice: form.totalAmount,
+  invoiceContent: form.content,
+  remark: form.remark || "",
+  stockInRecordIds: (form.stockInRecordIds || []).join(","),
+  status: 0,
+  storageAttachmentId: form.storageAttachmentId,
+});
+
+const getSupplierList = () => {
+  getOptions().then((res) => {
+    if (res.code === 200) {
+      supplierList.value = res.data ?? [];
+    }
+  });
+};
+
+const appendFilterParams = (params) => {
+  if (filters.invoiceNumber) {
+    params.invoiceNumber = filters.invoiceNumber;
+  }
+  if (filters.supplierId) {
+    params.supplierId = filters.supplierId;
+  }
+  if (filters.dateRange?.length === 2) {
+    params.startDate = filters.dateRange[0];
+    params.endDate = filters.dateRange[1];
+  }
+  if (filters.status !== "" && filters.status != null) {
+    params.status = filters.status;
+  }
+  return params;
+};
+
+const buildListParams = () =>
+  appendFilterParams({
+    current: pagination.currentPage,
+    size: pagination.pageSize,
+  });
+
+const buildExportParams = () => appendFilterParams({});
+
+const handleExport = () => {
+  proxy.download(
+    "/accountPurchaseInvoice/exportAccountPurchaseInvoice",
+    buildExportParams(),
+    `杩涢」鍙戠エ_${Date.now()}.xlsx`
+  );
+};
+
+const getTableData = () => {
+  tableLoading.value = true;
+  listPageAccountPurchaseInvoice(buildListParams())
+    .then((res) => {
+      if (res.code === 200) {
+        const records = res.data?.records ?? [];
+        dataList.value = records.map(normalizeTableRow);
+        pagination.total = res.data?.total ?? 0;
+      } else {
+        dataList.value = [];
+        pagination.total = 0;
+        ElMessage.error(res.msg || "鏌ヨ澶辫触");
+      }
+    })
+    .catch(() => {
+      dataList.value = [];
+      pagination.total = 0;
+      ElMessage.error("鏌ヨ澶辫触");
+    })
+    .finally(() => {
+      tableLoading.value = false;
+    });
+};
+
+const onSearch = () => {
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const resetFilters = () => {
+  filters.invoiceNumber = "";
+  filters.supplierId = "";
+  filters.dateRange = [];
+  filters.status = "";
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+  pagination.currentPage = page;
+  pagination.pageSize = limit;
+  getTableData();
+};
+
+const closeDialog = () => {
+  dialogVisible.value = false;
+  isView.value = false;
+  inboundSelectVisible.value = false;
+};
+
+const resetForm = () => {
+  Object.assign(form, {
+    invoiceNo: "",
+    supplierId: "",
+    invoiceDate: new Date().toISOString().split("T")[0],
+    invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+    taxRate: 13,
+    amount: 0,
+    taxAmount: 0,
+    totalAmount: 0,
+    content: "",
+    remark: "",
+    stockInRecordIds: [],
+    inboundBatches: "",
+    storageAttachmentId: undefined,
+    status: 0,
+  });
+  inboundBatchList.value = [];
+  inboundBatchOptions.value = [];
+};
+
+const add = () => {
+  isView.value = false;
+  dialogTitle.value = "褰曞叆鍙戠エ";
+  resetForm();
+  dialogVisible.value = true;
+};
+
+const view = (row) => {
+  isView.value = true;
+  dialogTitle.value = "鏌ョ湅鍙戠エ";
+  fillFormFromRow(row);
+  if (row.supplierId) {
+    loadInboundBatches(row.supplierId, true, false);
+  }
+  dialogVisible.value = true;
+};
+
+const handleCancel = (row) => {
+  ElMessageBox.confirm(`纭浣滃簾鍙戠エ銆�${row.invoiceNo ?? row.invoiceNumber}銆嶅悧锛焋, "浣滃簾纭", {
+    confirmButtonText: "纭畾",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(() => {
+    cancelAccountPurchaseInvoice(buildCancelPayload(row))
+      .then((res) => {
+        if (res.code === 200) {
+          ElMessage.success("浣滃簾鎴愬姛");
+          getTableData();
+        } else {
+          ElMessage.error(res.msg || "浣滃簾澶辫触");
+        }
+      })
+      .catch(() => {
+        ElMessage.error("浣滃簾澶辫触");
+      });
+  });
+};
+
+const handleDelete = (row) => {
+  ElMessageBox.confirm(`纭鍒犻櫎鍙戠エ銆�${row.invoiceNo ?? row.invoiceNumber}銆嶅悧锛焋, "鍒犻櫎纭", {
+    confirmButtonText: "纭畾",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(() => {
+    deleteAccountPurchaseInvoice([row.id])
+      .then((res) => {
+        if (res.code === 200) {
+          ElMessage.success("鍒犻櫎鎴愬姛");
+          getTableData();
+        } else {
+          ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+        }
+      })
+      .catch(() => {
+        ElMessage.error("鍒犻櫎澶辫触");
+      });
+  });
+};
+
+const submitForm = () => {
+  formRef.value?.validate((valid) => {
+    if (!valid) return;
+    submitLoading.value = true;
+    addAccountPurchaseInvoice(buildSubmitPayload())
+      .then((res) => {
+        if (res.code === 200) {
+          ElMessage.success("褰曞叆鎴愬姛");
+          closeDialog();
+          pagination.currentPage = 1;
+          getTableData();
+        } else {
+          ElMessage.error(res.msg || "褰曞叆澶辫触");
+        }
+      })
+      .catch(() => {
+        ElMessage.error("褰曞叆澶辫触");
+      })
+      .finally(() => {
+        submitLoading.value = false;
+      });
+  });
+};
+
+const ensureInboundOptionsForSelected = () => {
+  const ids = form.stockInRecordIds || [];
+  ids.forEach((id) => {
+    const exists = inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, id));
+    if (exists) return;
+    const fromList = inboundBatchList.value.find((row) => isSameInboundId(getInboundRowId(row), id));
+    if (fromList) {
+      const [option] = normalizeInboundBatchOptions([fromList]);
+      if (option) inboundBatchOptions.value.push(option);
+      return;
+    }
+    inboundBatchOptions.value.push({
+      label: String(id),
+      value: id,
+      inboundAmount: 0,
+    });
+  });
+};
+
+const restoreInboundTableSelection = () => {
+  nextTick(() => {
+    const table = inboundTableRef.value;
+    if (!table) return;
+    table.clearSelection();
+    const selectedIds = new Set((form.stockInRecordIds || []).map((id) => String(id)));
+    inboundBatchList.value.forEach((row) => {
+      const rowId = getInboundRowId(row);
+      if (rowId !== undefined && rowId !== null && selectedIds.has(String(rowId))) {
+        table.toggleRowSelection(row, true);
+      }
+    });
+  });
+};
+
+const loadInboundBatches = (supplierId, keepSelected = false, syncAmount = true) => {
+  if (!supplierId) {
+    inboundBatchList.value = [];
+    inboundBatchOptions.value = [];
+    if (!keepSelected) {
+      form.stockInRecordIds = [];
+      form.inboundBatches = "";
+      form.amount = 0;
+      form.taxAmount = 0;
+      form.totalAmount = 0;
+    }
+    return Promise.resolve();
+  }
+  inboundBatchLoading.value = true;
+  return getInboundBatchesBySupplier({ supplierId })
+    .then((res) => {
+      if (res.code === 200) {
+        const list = res.data?.records ?? res.data ?? [];
+        inboundBatchList.value = Array.isArray(list) ? list : [];
+        inboundBatchOptions.value = normalizeInboundBatchOptions(list);
+      } else {
+        inboundBatchList.value = [];
+        inboundBatchOptions.value = [];
+      }
+    })
+    .catch(() => {
+      inboundBatchList.value = [];
+      inboundBatchOptions.value = [];
+    })
+    .finally(() => {
+      inboundBatchLoading.value = false;
+      if (keepSelected) {
+        ensureInboundOptionsForSelected();
+        restoreInboundTableSelection();
+        if (syncAmount && !isView.value) {
+          syncInvoiceAmount();
+        }
+      }
+    });
+};
+
+const handleSupplierChange = (supplierId) => {
+  form.stockInRecordIds = [];
+  form.inboundBatches = "";
+  form.amount = 0;
+  form.taxAmount = 0;
+  form.totalAmount = 0;
+  loadInboundBatches(supplierId);
+};
+
+const handleInboundInputClick = () => {
+  if (isView.value) return;
+  openInboundSelectDialog();
+};
+
+const openInboundSelectDialog = () => {
+  if (!form.supplierId || isView.value) return;
+  inboundSelectVisible.value = true;
+  loadInboundBatches(form.supplierId, true).then(() => {
+    restoreInboundTableSelection();
+  });
+};
+
+const handleInboundDialogSelectionChange = (selection) => {
+  dialogInboundSelection.value = selection;
+};
+
+const confirmInboundSelection = () => {
+  if (dialogInboundSelection.value.length === 0) {
+    ElMessage.warning("璇疯嚦灏戦�夋嫨涓�鏉″叆搴撳崟");
+    return;
+  }
+  form.stockInRecordIds = dialogInboundSelection.value
+    .map((row) => getInboundRowId(row))
+    .filter((id) => id !== undefined && id !== null);
+  form.inboundBatches = dialogInboundSelection.value
+    .map((row) => row.inboundBatches ?? row.batchNo ?? "")
+    .filter(Boolean)
+    .join("銆�");
+  dialogInboundSelection.value.forEach((row) => {
+    const [option] = normalizeInboundBatchOptions([row]);
+    if (option && !inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, option.value))) {
+      inboundBatchOptions.value.push(option);
+    }
+  });
+  inboundSelectVisible.value = false;
+  syncInvoiceAmount();
+  formRef.value?.validateField("stockInRecordIds");
+};
+
+const handleInboundDialogClosed = () => {
+  dialogInboundSelection.value = [];
+};
+
+onMounted(() => {
+  getSupplierList();
+  getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 15px;
+}
+
+.text-primary {
+  color: #409eff;
+  font-weight: bold;
+}
+
+.text-danger {
+  color: #f56c6c;
+  font-weight: bold;
+}
+
+.text-success {
+  color: #67c23a;
+  font-weight: bold;
+}
+
+.inbound-batch-input :deep(.el-input__wrapper) {
+  cursor: pointer;
+}
+</style>
diff --git a/src/views/financialManagement/payable/payment.vue b/src/views/financialManagement/payable/payment.vue
new file mode 100644
index 0000000..18e7941
--- /dev/null
+++ b/src/views/financialManagement/payable/payment.vue
@@ -0,0 +1,299 @@
+<template>
+  <div class="app-container">
+    <el-form :model="filters"
+             :inline="true">
+      <el-form-item label="浠樻鍗曞彿:">
+        <el-input v-model="filters.paymentNumber"
+                  placeholder="璇疯緭鍏ヤ粯娆惧崟鍙�"
+                  clearable
+                  style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="渚涘簲鍟�:">
+        <el-select v-model="filters.supplierId"
+                   placeholder="璇烽�夋嫨渚涘簲鍟�"
+                   clearable
+                   filterable
+                   style="width: 200px;">
+          <el-option v-for="item in supplierList"
+                     :key="item.id"
+                     :label="item.supplierName"
+                     :value="item.id" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="浠樻鏂瑰紡:">
+        <el-select v-model="filters.paymentMethod"
+                   placeholder="璇烽�夋嫨浠樻鏂瑰紡"
+                   clearable
+                   style="width: 150px;">
+          <el-option v-for="item in checkout_payment"
+                     :key="item.value"
+                     :label="item.label"
+                     :value="item.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="浠樻鏃ユ湡:">
+        <el-date-picker v-model="filters.dateRange"
+                        type="daterange"
+                        value-format="YYYY-MM-DD"
+                        format="YYYY-MM-DD"
+                        range-separator="鑷�"
+                        start-placeholder="寮�濮嬫棩鏈�"
+                        end-placeholder="缁撴潫鏃ユ湡"
+                        clearable
+                        style="width: 240px;" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary"
+                   @click="onSearch">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div>
+          <el-statistic title="鏈〉浠樻鍚堣"
+                        :value="totalPaymentAmount"
+                        :precision="2"
+                        prefix="楼" />
+        </div>
+        <div>
+          <el-button @click="handleExport"
+                     icon="Download">瀵煎嚭</el-button>
+        </div>
+      </div>
+      <PIMTable rowKey="id"
+                :column="columns"
+                :tableData="dataList"
+                :tableLoading="tableLoading"
+                :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+                @pagination="changePage">
+        <template #amount="{ row }">
+          <span class="text-danger">楼{{ formatMoney(row.amount) }}</span>
+        </template>
+        <template #paymentMethod="{ row }">
+          <el-tag>{{ getPaymentMethodLabel(row.paymentMethod) }}</el-tag>
+        </template>
+        <template #operation="{ row }">
+          <el-button :disabled="row.accountStatemen"
+                     type="danger"
+                     link
+                     @click="handleDelete(row)">鍒犻櫎</el-button>
+        </template>
+      </PIMTable>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, computed, onMounted, getCurrentInstance } from "vue";
+  import { ElMessage, ElMessageBox } from "element-plus";
+  import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
+  import {
+    listPageAccountPurchasePayment,
+    deleteAccountPurchasePayment,
+  } from "@/api/financialManagement/accountPurchasePayment.js";
+
+  defineOptions({
+    name: "浠樻鍗�",
+  });
+
+  const { proxy } = getCurrentInstance();
+  const { checkout_payment } = proxy.useDict("checkout_payment");
+
+  const filters = reactive({
+    paymentNumber: "",
+    supplierId: "",
+    paymentMethod: "",
+    dateRange: [],
+  });
+
+  const pagination = reactive({
+    currentPage: 1,
+    pageSize: 10,
+    total: 0,
+  });
+
+  const columns = [
+    { label: "浠樻鍗曞彿", prop: "paymentNumber", width: "150" },
+    { label: "鍏宠仈鐢宠鍗�", prop: "invoiceApplicationNo", width: "150" },
+    { label: "渚涘簲鍟�", prop: "supplierName", width: "180" },
+    { label: "浠樻鏃ユ湡", prop: "paymentDate", width: "120" },
+    { label: "浠樻閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+    {
+      label: "浠樻鏂瑰紡",
+      prop: "paymentMethod",
+      dataType: "slot",
+      slot: "paymentMethod",
+      width: "120",
+    },
+    { label: "澶囨敞", prop: "remark", showOverflowTooltip: true },
+    {
+      label: "鎿嶄綔",
+      prop: "operation",
+      dataType: "slot",
+      slot: "operation",
+      width: "80",
+      fixed: "right",
+    },
+  ];
+
+  const dataList = ref([]);
+  const tableLoading = ref(false);
+  const supplierList = ref([]);
+
+  const totalPaymentAmount = computed(() =>
+    dataList.value.reduce((sum, item) => sum + Number(item.amount ?? 0), 0)
+  );
+
+  const formatMoney = value => {
+    if (value === undefined || value === null) return "0.00";
+    return Number(value)
+      .toFixed(2)
+      .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+  };
+
+  const getPaymentMethodLabel = value => {
+    if (value === undefined || value === null || value === "") return "-";
+    const item = checkout_payment.value?.find(
+      m => String(m.value) === String(value)
+    );
+    return item?.label ?? value;
+  };
+
+  const normalizeTableRow = row => ({
+    ...row,
+    paymentNumber: row.paymentNumber ?? row.paymentCode,
+    invoiceApplicationNo: row.invoiceApplicationNo ?? row.applyCode ?? "",
+    amount: row.paymentAmount ?? row.amount,
+    bankAccountNum: row.bankAccountNum ?? row.bankAccount ?? "",
+    bankAccountName: row.bankAccountName ?? row.bankName ?? "",
+  });
+
+  const getSupplierList = () => {
+    getOptions().then(res => {
+      if (res.code === 200) {
+        supplierList.value = res.data ?? [];
+      }
+    });
+  };
+
+  const appendFilterParams = params => {
+    if (filters.paymentNumber) {
+      params.paymentNumber = filters.paymentNumber;
+    }
+    if (filters.supplierId) {
+      params.supplierId = filters.supplierId;
+    }
+    if (filters.paymentMethod) {
+      params.paymentMethod = filters.paymentMethod;
+    }
+    if (filters.dateRange?.length === 2) {
+      params.startDate = filters.dateRange[0];
+      params.endDate = filters.dateRange[1];
+    }
+    return params;
+  };
+
+  const buildListParams = () =>
+    appendFilterParams({
+      current: pagination.currentPage,
+      size: pagination.pageSize,
+    });
+
+  const buildExportParams = () => appendFilterParams({});
+
+  const handleExport = () => {
+    proxy.download(
+      "/accountPurchasePayment/exportAccountPurchasePayment",
+      buildExportParams(),
+      `浠樻鍗昣${Date.now()}.xlsx`
+    );
+  };
+
+  const getTableData = () => {
+    tableLoading.value = true;
+    listPageAccountPurchasePayment(buildListParams())
+      .then(res => {
+        if (res.code === 200) {
+          dataList.value = (res.data?.records ?? []).map(normalizeTableRow);
+          pagination.total = res.data?.total ?? 0;
+        } else {
+          dataList.value = [];
+          pagination.total = 0;
+          ElMessage.error(res.msg || "鏌ヨ澶辫触");
+        }
+      })
+      .catch(() => {
+        dataList.value = [];
+        pagination.total = 0;
+        ElMessage.error("鏌ヨ澶辫触");
+      })
+      .finally(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  const onSearch = () => {
+    pagination.currentPage = 1;
+    getTableData();
+  };
+
+  const resetFilters = () => {
+    filters.paymentNumber = "";
+    filters.supplierId = "";
+    filters.paymentMethod = "";
+    filters.dateRange = [];
+    pagination.currentPage = 1;
+    getTableData();
+  };
+
+  const changePage = ({ page, limit }) => {
+    pagination.currentPage = page;
+    pagination.pageSize = limit;
+    getTableData();
+  };
+
+  const handleDelete = row => {
+    ElMessageBox.confirm(`纭鍒犻櫎浠樻鍗曘��${row.paymentNumber}銆嶅悧锛焋, "鎻愮ず", {
+      confirmButtonText: "纭畾",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    }).then(() => {
+      deleteAccountPurchasePayment([row.id])
+        .then(res => {
+          if (res.code === 200) {
+            ElMessage.success("鍒犻櫎鎴愬姛");
+            getTableData();
+          } else {
+            ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+          }
+        })
+        .catch(() => {
+          ElMessage.error("鍒犻櫎澶辫触");
+        });
+    });
+  };
+
+  onMounted(() => {
+    getSupplierList();
+    getTableData();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .actions {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 15px;
+  }
+
+  .text-danger {
+    color: #f56c6c;
+    font-weight: bold;
+  }
+</style>
diff --git a/src/views/financialManagement/payable/paymentApply.vue b/src/views/financialManagement/payable/paymentApply.vue
new file mode 100644
index 0000000..3937e96
--- /dev/null
+++ b/src/views/financialManagement/payable/paymentApply.vue
@@ -0,0 +1,1016 @@
+<template>
+  <div class="app-container">
+    <el-form :model="filters" :inline="true">
+      <el-form-item label="鐢宠鍗曞彿:">
+        <el-input v-model="filters.invoiceApplicationNo" placeholder="璇疯緭鍏ョ敵璇峰崟鍙�" clearable style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="渚涘簲鍟�:">
+        <el-select v-model="filters.supplierId" placeholder="璇烽�夋嫨渚涘簲鍟�" clearable filterable style="width: 200px;">
+          <el-option
+            v-for="item in supplierList"
+            :key="item.id"
+            :label="item.supplierName"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="瀹℃牳鐘舵��:">
+        <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+          <el-option label="寰呭鏍�" :value="0" />
+          <el-option label="瀹℃牳閫氳繃" :value="1" />
+          <el-option label="瀹℃牳涓嶉�氳繃" :value="2" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="鐢宠鏃ユ湡:">
+        <el-date-picker
+          v-model="filters.dateRange"
+          type="daterange"
+          value-format="YYYY-MM-DD"
+          format="YYYY-MM-DD"
+          range-separator="鑷�"
+          start-placeholder="寮�濮嬫棩鏈�"
+          end-placeholder="缁撴潫鏃ユ湡"
+          clearable
+          style="width: 240px;"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div></div>
+        <div>
+          <el-button type="primary" @click="add" icon="Plus">鏂板鐢宠</el-button>
+          <el-button @click="handleExport" icon="Download">瀵煎嚭</el-button>
+        </div>
+      </div>
+      <PIMTable
+        rowKey="id"
+        :column="columns"
+        :tableData="dataList"
+        :tableLoading="tableLoading"
+        :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+        @pagination="changePage"
+      >
+        <template #amount="{ row }">
+          <span class="text-danger">楼{{ formatMoney(row.amount) }}</span>
+        </template>
+        <template #paymentMethod="{ row }">
+          <el-tag>{{ getPaymentMethodLabel(row.paymentMethod) }}</el-tag>
+        </template>
+        <template #status="{ row }">
+          <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
+        </template>
+        <template #operation="{ row }">
+          <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+          <el-button type="primary" link @click="edit(row)" v-if="isPendingStatus(row.status)">缂栬緫</el-button>
+          <el-button type="success" link @click="handleAudit(row)" v-if="isPendingStatus(row.status)">瀹℃牳</el-button>
+          <el-button type="warning" link @click="openPaymentDialog(row)" v-if="isApprovedStatus(row.status)">浠樻</el-button>
+          <el-button type="danger" link @click="handleDelete(row)" v-if="isPendingStatus(row.status)">鍒犻櫎</el-button>
+        </template>
+      </PIMTable>
+    </div>
+
+    <FormDialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="800px"
+      :operation-type="isView ? 'detail' : ''"
+      @confirm="submitForm"
+      @cancel="closeDialog"
+    >
+      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+        <el-row v-if="isView" :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="瀹℃牳鐘舵��">
+              <el-tag :type="getStatusType(form.status)">{{ getStatusLabel(form.status) }}</el-tag>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鐢宠鍗曞彿" prop="invoiceApplicationNo">
+              <el-input v-model="form.invoiceApplicationNo" placeholder="绯荤粺鑷姩鐢熸垚" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="渚涘簲鍟�" prop="supplierId">
+              <el-select
+                v-model="form.supplierId"
+                placeholder="璇烽�夋嫨渚涘簲鍟�"
+                style="width: 100%;"
+                filterable
+                :disabled="isEdit || isView"
+                @change="handleSupplierChange"
+              >
+                <el-option
+                  v-for="item in supplierList"
+                  :key="item.id"
+                  :label="item.supplierName"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鍏宠仈鍏ュ簱鍗�" prop="stockInRecordIds">
+              <el-input
+                :model-value="inboundBatchDisplayText"
+                placeholder="璇峰厛閫夋嫨渚涘簲鍟�"
+                readonly
+                :disabled="!form.supplierId || isEdit || isView"
+                class="inbound-batch-input"
+                @click="handleInboundInputClick"
+              >
+                <template v-if="!isEdit && !isView" #append>
+                  <el-button
+                    :disabled="!form.supplierId"
+                    :loading="inboundBatchLoading"
+                    @click.stop="openInboundSelectDialog"
+                  >
+                    閫夋嫨
+                  </el-button>
+                </template>
+              </el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鐢宠鏃ユ湡" prop="applyDate">
+              <el-date-picker
+                v-model="form.applyDate"
+                type="date"
+                placeholder="閫夋嫨鏃ユ湡"
+                value-format="YYYY-MM-DD"
+                style="width: 100%;"
+                :disabled="isView"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="浠樻閲戦" prop="paymentAmount">
+              <el-input-number
+                v-model="form.paymentAmount"
+                :min="0"
+                :precision="2"
+                style="width: 100%;"
+                :disabled="isView"
+                placeholder="鏍规嵁鍏ュ簱鍗曡嚜鍔ㄦ眹鎬伙紝鍙慨鏀�"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="浠樻鏂瑰紡" prop="paymentMethod">
+              <el-select
+                v-model="form.paymentMethod"
+                placeholder="璇烽�夋嫨浠樻鏂瑰紡"
+                style="width: 100%;"
+                :disabled="isView"
+              >
+                <el-option
+                  v-for="item in checkout_payment"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="浠樻浜嬬敱" prop="paymentContent">
+          <el-input
+            v-model="form.paymentContent"
+            type="textarea"
+            :rows="3"
+            placeholder="璇疯緭鍏ヤ粯娆句簨鐢�"
+            :disabled="isView"
+          />
+        </el-form-item>
+        <el-form-item label="澶囨敞" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" :disabled="isView" />
+        </el-form-item>
+      </el-form>
+      <template v-if="!isView" #footer>
+        <el-button type="primary" :loading="submitLoading" @click="submitForm">纭畾</el-button>
+        <el-button @click="closeDialog">鍙栨秷</el-button>
+      </template>
+    </FormDialog>
+
+    <FormDialog
+      title="浠樻"
+      v-model="paymentDialogVisible"
+      width="800px"
+      @confirm="submitPayment"
+      @cancel="paymentDialogVisible = false"
+    >
+      <el-form :model="paymentForm" :rules="paymentRules" ref="paymentFormRef" label-width="120px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="浠樻鍗曞彿" prop="paymentNumber">
+              <el-input v-model="paymentForm.paymentNumber" placeholder="绯荤粺鑷姩鐢熸垚" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鍏宠仈鐢宠鍗�" prop="invoiceApplicationNo">
+              <el-input v-model="paymentForm.invoiceApplicationNo" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="渚涘簲鍟�">
+              <el-input v-model="paymentForm.supplierName" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="浠樻鏃ユ湡" prop="paymentDate">
+              <el-date-picker
+                v-model="paymentForm.paymentDate"
+                type="date"
+                placeholder="閫夋嫨鏃ユ湡"
+                value-format="YYYY-MM-DD"
+                style="width: 100%;"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="浠樻閲戦" prop="paymentAmount">
+              <el-input-number
+                v-model="paymentForm.paymentAmount"
+                :min="0"
+                :precision="2"
+                style="width: 100%;"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="浠樻鏂瑰紡" prop="paymentMethod">
+              <el-select v-model="paymentForm.paymentMethod" placeholder="璇烽�夋嫨浠樻鏂瑰紡" style="width: 100%;">
+                <el-option
+                  v-for="item in checkout_payment"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row v-if="isBankTransferPayment(paymentForm.paymentMethod)" :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="閾惰璐﹀彿" prop="bankAccount">
+              <el-input v-model="paymentForm.bankAccount" placeholder="閾惰璐﹀彿" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="寮�鎴疯" prop="bankName">
+              <el-input v-model="paymentForm.bankName" placeholder="寮�鎴疯" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="澶囨敞" prop="remark">
+          <el-input v-model="paymentForm.remark" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ娉�" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button type="primary" :loading="paymentSubmitLoading" @click="submitPayment">纭畾</el-button>
+        <el-button @click="paymentDialogVisible = false">鍙栨秷</el-button>
+      </template>
+    </FormDialog>
+
+    <el-dialog
+      v-model="inboundSelectVisible"
+      title="閫夋嫨鍏ュ簱鍗曞彿"
+      width="1100px"
+      append-to-body
+      destroy-on-close
+      :close-on-click-modal="false"
+      @closed="handleInboundDialogClosed"
+    >
+      <el-table
+        ref="inboundTableRef"
+        v-loading="inboundBatchLoading"
+        :data="inboundBatchList"
+        row-key="id"
+        border
+        stripe
+        max-height="480"
+        @selection-change="handleInboundDialogSelectionChange"
+      >
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column prop="inboundBatches" label="鍏ュ簱鍗曞彿" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="supplierName" label="渚涘簲鍟�" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="productName" label="浜у搧鍚嶇О" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="specificationModel" label="瑙勬牸鍨嬪彿" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="purchaseContractNumber" label="閲囪喘璁㈠崟鍙�" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="inboundDate" label="鍏ュ簱鏃ユ湡" width="110" align="center" />
+        <el-table-column prop="inboundAmount" label="鍏ュ簱閲戦(鍚◣)" width="120" align="right">
+          <template #default="{ row }">楼{{ formatMoney(getInboundRowTaxInclusiveAmount(row)) }}</template>
+        </el-table-column>
+      </el-table>
+      <template #footer>
+        <el-button type="primary" @click="confirmInboundSelection">纭畾</el-button>
+        <el-button @click="inboundSelectVisible = false">鍙栨秷</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
+import {
+  getInboundBatchesBySupplier,
+  addAccountPaymentApplication,
+  listPageAccountPaymentApplication,
+  updateAccountPaymentApplication,
+  auditAccountPaymentApplication,
+  deleteAccountPaymentApplication,
+} from "@/api/financialManagement/accountPaymentApplication.js";
+import { addAccountPurchasePayment } from "@/api/financialManagement/accountPurchasePayment.js";
+
+defineOptions({
+  name: "浠樻鐢宠",
+});
+
+const { proxy } = getCurrentInstance();
+const { checkout_payment } = proxy.useDict("checkout_payment");
+
+const filters = reactive({
+  invoiceApplicationNo: "",
+  supplierId: "",
+  status: "",
+  dateRange: [],
+});
+
+const pagination = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+const columns = [
+  { label: "鐢宠鍗曞彿", prop: "applyCode", width: "150" },
+  { label: "渚涘簲鍟�", prop: "supplierName", width: "180" },
+  { label: "浠樻閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+  { label: "浠樻鏂瑰紡", prop: "paymentMethod", dataType: "slot", slot: "paymentMethod", width: "120" },
+  { label: "鐢宠鏃ユ湡", prop: "applyDate", width: "120" },
+  { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status", width: "100" },
+  { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "260", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isEdit = ref(false);
+const isView = ref(false);
+const submitLoading = ref(false);
+const currentId = ref(null);
+const supplierList = ref([]);
+
+const inboundBatchList = ref([]);
+const inboundBatchOptions = ref([]);
+const inboundBatchLoading = ref(false);
+const inboundSelectVisible = ref(false);
+const inboundTableRef = ref(null);
+const dialogInboundSelection = ref([]);
+
+const paymentDialogVisible = ref(false);
+const paymentFormRef = ref(null);
+const paymentSubmitLoading = ref(false);
+
+const paymentForm = reactive({
+  paymentNumber: "",
+  invoiceApplicationNo: "",
+  supplierName: "",
+  supplierId: "",
+  accountPaymentApplicationId: null,
+  paymentDate: "",
+  paymentAmount: 0,
+  paymentMethod: "",
+  bankAccount: "",
+  bankName: "",
+  remark: "",
+});
+
+const paymentRules = {
+  paymentDate: [{ required: true, message: "璇烽�夋嫨浠樻鏃ユ湡", trigger: "change" }],
+  paymentAmount: [{ required: true, message: "璇疯緭鍏ヤ粯娆鹃噾棰�", trigger: "blur" }],
+  paymentMethod: [{ required: true, message: "璇烽�夋嫨浠樻鏂瑰紡", trigger: "change" }],
+};
+
+const STATUS_LABEL_MAP = { 0: "寰呭鏍�", 1: "瀹℃牳閫氳繃", 2: "瀹℃牳涓嶉�氳繃" };
+const STATUS_TYPE_MAP = { 0: "warning", 1: "success", 2: "danger" };
+
+const form = reactive({
+  invoiceApplicationNo: "",
+  supplierId: "",
+  paymentAmount: 0,
+  paymentMethod: "",
+  applyDate: "",
+  paymentContent: "",
+  remark: "",
+  stockInRecordIds: [],
+  inboundBatches: "",
+  status: 0,
+});
+
+const rules = {
+  supplierId: [{ required: true, message: "璇烽�夋嫨渚涘簲鍟�", trigger: "change" }],
+  stockInRecordIds: [{ required: true, type: "array", min: 1, message: "璇烽�夋嫨鍏宠仈鍏ュ簱鍗�", trigger: "change" }],
+  paymentAmount: [{ required: true, message: "璇疯緭鍏ヤ粯娆鹃噾棰�", trigger: "blur" }],
+  paymentMethod: [{ required: true, message: "璇烽�夋嫨浠樻鏂瑰紡", trigger: "change" }],
+  applyDate: [{ required: true, message: "璇烽�夋嫨鐢宠鏃ユ湡", trigger: "change" }],
+};
+
+const formatMoney = (value) => {
+  if (value === undefined || value === null) return "0.00";
+  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const normalizeStatus = (status) => {
+  if (status === undefined || status === null || status === "") return 0;
+  const num = Number(status);
+  return Number.isNaN(num) ? 0 : num;
+};
+
+const isPendingStatus = (status) => normalizeStatus(status) === 0;
+
+const isApprovedStatus = (status) => normalizeStatus(status) === 1;
+
+const isBankTransferPayment = (method) => {
+  if (method === undefined || method === null || method === "") return false;
+  const item = checkout_payment.value?.find((m) => String(m.value) === String(method));
+  if (item?.label?.includes("閾惰")) return true;
+  return String(method) === "bank_transfer" || String(method).toLowerCase().includes("bank");
+};
+
+const getStatusLabel = (status) => STATUS_LABEL_MAP[normalizeStatus(status)] ?? "寰呭鏍�";
+
+const getStatusType = (status) => STATUS_TYPE_MAP[normalizeStatus(status)] ?? "warning";
+
+const getPaymentMethodLabel = (value) => {
+  if (value === undefined || value === null || value === "") return "-";
+  const item = checkout_payment.value?.find((m) => String(m.value) === String(value));
+  return item?.label ?? value;
+};
+
+const getDefaultPaymentMethod = () => checkout_payment.value?.[0]?.value ?? "";
+
+const parseStockInRecordIds = (value) => {
+  if (!value) return [];
+  if (Array.isArray(value)) return value;
+  return String(value)
+    .split(/[,锛宂/)
+    .map((s) => s.trim())
+    .filter(Boolean)
+    .map((s) => (/^\d+$/.test(s) ? Number(s) : s));
+};
+
+const formatInboundBatches = (value) => {
+  if (value === undefined || value === null || value === "") return "";
+  if (Array.isArray(value)) return value.filter(Boolean).join("銆�");
+  return String(value)
+    .split(/[,锛宂/)
+    .map((s) => s.trim())
+    .filter(Boolean)
+    .join("銆�");
+};
+
+const isSameInboundId = (a, b) => String(a) === String(b);
+
+const getInboundRowId = (row) => row?.id ?? row?.stockInRecordId;
+
+const getInboundRowTaxInclusiveAmount = (row) =>
+  Number(row?.inboundAmount ?? row?.taxInclusivePrice ?? row?.totalAmount ?? row?.amount ?? 0);
+
+const normalizeInboundBatchOptions = (data) => {
+  const list = Array.isArray(data) ? data : [];
+  return list.map((item, index) => {
+    const label =
+      item.inboundBatches ?? item.batchNo ?? item.inboundNo ?? `鍏ュ簱鍗�${index + 1}`;
+    const value = item.id ?? item.stockInRecordId ?? label;
+    return {
+      label: String(label),
+      value,
+      inboundAmount: getInboundRowTaxInclusiveAmount(item),
+    };
+  });
+};
+
+const syncPaymentAmount = () => {
+  const selected = form.stockInRecordIds || [];
+  let sum = inboundBatchOptions.value
+    .filter((opt) => selected.some((id) => isSameInboundId(id, opt.value)))
+    .reduce((acc, opt) => acc + (Number(opt.inboundAmount) || 0), 0);
+
+  if (sum <= 0 && selected.length) {
+    sum = inboundBatchList.value
+      .filter((row) => selected.some((id) => isSameInboundId(id, getInboundRowId(row))))
+      .reduce((acc, row) => acc + getInboundRowTaxInclusiveAmount(row), 0);
+  }
+
+  form.paymentAmount = sum > 0 ? Number(sum.toFixed(2)) : 0;
+};
+
+const inboundBatchDisplayText = computed(() => {
+  if (form.inboundBatches) return form.inboundBatches;
+  const ids = form.stockInRecordIds || [];
+  if (!ids.length) return "";
+  const labels = inboundBatchOptions.value
+    .filter((opt) => ids.some((id) => isSameInboundId(id, opt.value)))
+    .map((opt) => opt.label);
+  if (labels.length) return labels.join("銆�");
+  return ids.join("銆�");
+});
+
+const normalizeTableRow = (row) => ({
+  ...row,
+  applyCode: row.invoiceApplicationNo ?? row.applyCode,
+  amount: row.paymentAmount ?? row.amount,
+  reason: row.paymentContent ?? row.reason,
+  status: normalizeStatus(row.status),
+  stockInRecordIds: row.stockInRecordIds ?? "",
+  inboundBatches: formatInboundBatches(row.inboundBatches),
+});
+
+const fillFormFromRow = (row) => {
+  const stockInRecordIds = parseStockInRecordIds(row.stockInRecordIds);
+  Object.assign(form, {
+    invoiceApplicationNo: row.invoiceApplicationNo ?? row.applyCode ?? "",
+    supplierId: row.supplierId,
+    paymentAmount: Number(row.paymentAmount ?? row.amount ?? 0),
+    paymentMethod: row.paymentMethod ?? getDefaultPaymentMethod(),
+    applyDate: row.applyDate ?? "",
+    paymentContent: row.paymentContent ?? row.reason ?? "",
+    remark: row.remark ?? "",
+    stockInRecordIds,
+    inboundBatches: formatInboundBatches(row.inboundBatches),
+    status: normalizeStatus(row.status),
+  });
+};
+
+const buildPayloadFromRow = (row, statusOverride) => ({
+  id: row.id,
+  supplierId: row.supplierId,
+  stockInRecordIds:
+    typeof row.stockInRecordIds === "string"
+      ? row.stockInRecordIds
+      : (row.stockInRecordIds || []).join(","),
+  invoiceApplicationNo: row.invoiceApplicationNo ?? row.applyCode ?? "",
+  paymentMethod: row.paymentMethod,
+  paymentContent: row.paymentContent ?? row.reason ?? "",
+  applyDate: row.applyDate,
+  remark: row.remark ?? "",
+  status: statusOverride !== undefined ? statusOverride : normalizeStatus(row.status),
+  paymentAmount: Number(row.paymentAmount ?? row.amount ?? 0),
+});
+
+const buildSubmitPayload = (forUpdate = false) => {
+  const payload = {
+    supplierId: form.supplierId,
+    stockInRecordIds: (form.stockInRecordIds || []).join(","),
+    invoiceApplicationNo: form.invoiceApplicationNo || "",
+    paymentMethod: form.paymentMethod,
+    paymentContent: form.paymentContent || "",
+    applyDate: form.applyDate,
+    remark: form.remark || "",
+    status: 0,
+    paymentAmount: form.paymentAmount,
+  };
+  if (forUpdate) {
+    payload.id = currentId.value;
+  }
+  return payload;
+};
+
+const getSupplierList = () => {
+  getOptions().then((res) => {
+    if (res.code === 200) {
+      supplierList.value = res.data ?? [];
+    }
+  });
+};
+
+const appendFilterParams = (params) => {
+  if (filters.invoiceApplicationNo) {
+    params.invoiceApplicationNo = filters.invoiceApplicationNo;
+  }
+  if (filters.supplierId) {
+    params.supplierId = filters.supplierId;
+  }
+  if (filters.status !== "" && filters.status != null) {
+    params.status = filters.status;
+  }
+  if (filters.dateRange?.length === 2) {
+    params.startDate = filters.dateRange[0];
+    params.endDate = filters.dateRange[1];
+  }
+  return params;
+};
+
+const buildListParams = () =>
+  appendFilterParams({
+    current: pagination.currentPage,
+    size: pagination.pageSize,
+  });
+
+const buildExportParams = () => appendFilterParams({});
+
+const handleExport = () => {
+  proxy.download(
+    "/accountPaymentApplication/exportAccountPaymentApplication",
+    buildExportParams(),
+    `浠樻鐢宠_${Date.now()}.xlsx`
+  );
+};
+
+const getTableData = () => {
+  tableLoading.value = true;
+  listPageAccountPaymentApplication(buildListParams())
+    .then((res) => {
+      if (res.code === 200) {
+        dataList.value = (res.data?.records ?? []).map(normalizeTableRow);
+        pagination.total = res.data?.total ?? 0;
+      } else {
+        dataList.value = [];
+        pagination.total = 0;
+        ElMessage.error(res.msg || "鏌ヨ澶辫触");
+      }
+    })
+    .catch(() => {
+      dataList.value = [];
+      pagination.total = 0;
+      ElMessage.error("鏌ヨ澶辫触");
+    })
+    .finally(() => {
+      tableLoading.value = false;
+    });
+};
+
+const onSearch = () => {
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const resetFilters = () => {
+  filters.invoiceApplicationNo = "";
+  filters.supplierId = "";
+  filters.status = "";
+  filters.dateRange = [];
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+  pagination.currentPage = page;
+  pagination.pageSize = limit;
+  getTableData();
+};
+
+const closeDialog = () => {
+  dialogVisible.value = false;
+  isView.value = false;
+  isEdit.value = false;
+  inboundSelectVisible.value = false;
+};
+
+const resetForm = () => {
+  Object.assign(form, {
+    invoiceApplicationNo: "",
+    supplierId: "",
+    paymentAmount: 0,
+    paymentMethod: getDefaultPaymentMethod(),
+    applyDate: new Date().toISOString().split("T")[0],
+    paymentContent: "",
+    remark: "",
+    stockInRecordIds: [],
+    inboundBatches: "",
+    status: 0,
+  });
+  inboundBatchList.value = [];
+  inboundBatchOptions.value = [];
+};
+
+const add = () => {
+  isEdit.value = false;
+  isView.value = false;
+  dialogTitle.value = "鏂板浠樻鐢宠";
+  resetForm();
+  dialogVisible.value = true;
+};
+
+const edit = (row) => {
+  isEdit.value = true;
+  isView.value = false;
+  currentId.value = row.id;
+  dialogTitle.value = "缂栬緫浠樻鐢宠";
+  fillFormFromRow(row);
+  dialogVisible.value = true;
+};
+
+const view = (row) => {
+  isView.value = true;
+  isEdit.value = false;
+  dialogTitle.value = "鏌ョ湅浠樻鐢宠";
+  fillFormFromRow(row);
+  if (row.supplierId) {
+    loadInboundBatches(row.supplierId, true, false);
+  }
+  dialogVisible.value = true;
+};
+
+const submitAudit = (row, status) => {
+  auditAccountPaymentApplication(buildPayloadFromRow(row, status))
+    .then((res) => {
+      if (res.code === 200) {
+        ElMessage.success(status === 1 ? "瀹℃牳閫氳繃" : "瀹℃牳涓嶉�氳繃");
+        getTableData();
+      } else {
+        ElMessage.error(res.msg || "瀹℃牳澶辫触");
+      }
+    })
+    .catch(() => {
+      ElMessage.error("瀹℃牳澶辫触");
+    });
+};
+
+const handleAudit = (row) => {
+  ElMessageBox.confirm("璇烽�夋嫨瀹℃牳缁撴灉", "浠樻鐢宠瀹℃牳", {
+    confirmButtonText: "瀹℃牳閫氳繃",
+    cancelButtonText: "瀹℃牳涓嶉�氳繃",
+    distinguishCancelAndClose: true,
+    type: "warning",
+  })
+    .then(() => {
+      submitAudit(row, 1);
+    })
+    .catch((action) => {
+      if (action === "cancel") {
+        submitAudit(row, 2);
+      }
+    });
+};
+
+const openPaymentDialog = (row) => {
+  Object.assign(paymentForm, {
+    paymentNumber: "",
+    invoiceApplicationNo: row.invoiceApplicationNo ?? row.applyCode ?? "",
+    supplierName: row.supplierName ?? "",
+    supplierId: row.supplierId,
+    accountPaymentApplicationId: row.id,
+    paymentDate: new Date().toISOString().split("T")[0],
+    paymentAmount: Number(row.paymentAmount ?? row.amount ?? 0),
+    paymentMethod: row.paymentMethod ?? getDefaultPaymentMethod(),
+    bankAccount: row.bankAccountNum ?? row.bankAccount ?? "",
+    bankName: row.bankAccountName ?? row.bankName ?? "",
+    remark: "",
+  });
+  paymentDialogVisible.value = true;
+  nextTick(() => {
+    paymentFormRef.value?.clearValidate();
+  });
+};
+
+const submitPayment = () => {
+  paymentFormRef.value?.validate((valid) => {
+    if (!valid) return;
+    paymentSubmitLoading.value = true;
+    addAccountPurchasePayment({
+      accountPaymentApplicationId: paymentForm.accountPaymentApplicationId,
+      supplierId: paymentForm.supplierId,
+      paymentDate: paymentForm.paymentDate,
+      paymentMethod: paymentForm.paymentMethod,
+      paymentAmount: paymentForm.paymentAmount,
+      paymentNumber: paymentForm.paymentNumber || "",
+      remark: paymentForm.remark || "",
+    })
+      .then((res) => {
+        if (res.code === 200) {
+          ElMessage.success("浠樻鎴愬姛");
+          paymentDialogVisible.value = false;
+          getTableData();
+        } else {
+          ElMessage.error(res.msg || "浠樻澶辫触");
+        }
+      })
+      .catch(() => {
+        ElMessage.error("浠樻澶辫触");
+      })
+      .finally(() => {
+        paymentSubmitLoading.value = false;
+      });
+  });
+};
+
+const handleDelete = (row) => {
+  ElMessageBox.confirm(`纭鍒犻櫎鐢宠鍗曘��${row.applyCode ?? row.invoiceApplicationNo}銆嶅悧锛焋, "鎻愮ず", {
+    confirmButtonText: "纭畾",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(() => {
+    deleteAccountPaymentApplication([row.id])
+      .then((res) => {
+        if (res.code === 200) {
+          ElMessage.success("鍒犻櫎鎴愬姛");
+          getTableData();
+        } else {
+          ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+        }
+      })
+      .catch(() => {
+        ElMessage.error("鍒犻櫎澶辫触");
+      });
+  });
+};
+
+const submitForm = () => {
+  formRef.value?.validate((valid) => {
+    if (!valid) return;
+    submitLoading.value = true;
+    const request = isEdit.value
+      ? updateAccountPaymentApplication(buildSubmitPayload(true))
+      : addAccountPaymentApplication(buildSubmitPayload(false));
+
+    request
+      .then((res) => {
+        if (res.code === 200) {
+          ElMessage.success(isEdit.value ? "缂栬緫鎴愬姛" : "鏂板鎴愬姛");
+          closeDialog();
+          pagination.currentPage = 1;
+          getTableData();
+        } else {
+          ElMessage.error(res.msg || "淇濆瓨澶辫触");
+        }
+      })
+      .catch(() => {
+        ElMessage.error("淇濆瓨澶辫触");
+      })
+      .finally(() => {
+        submitLoading.value = false;
+      });
+  });
+};
+
+const ensureInboundOptionsForSelected = () => {
+  const ids = form.stockInRecordIds || [];
+  ids.forEach((id) => {
+    const exists = inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, id));
+    if (exists) return;
+    const fromList = inboundBatchList.value.find((row) => isSameInboundId(getInboundRowId(row), id));
+    if (fromList) {
+      const [option] = normalizeInboundBatchOptions([fromList]);
+      if (option) inboundBatchOptions.value.push(option);
+      return;
+    }
+    inboundBatchOptions.value.push({
+      label: String(id),
+      value: id,
+      inboundAmount: 0,
+    });
+  });
+};
+
+const restoreInboundTableSelection = () => {
+  nextTick(() => {
+    const table = inboundTableRef.value;
+    if (!table) return;
+    table.clearSelection();
+    const selectedIds = new Set((form.stockInRecordIds || []).map((id) => String(id)));
+    inboundBatchList.value.forEach((row) => {
+      const rowId = getInboundRowId(row);
+      if (rowId !== undefined && rowId !== null && selectedIds.has(String(rowId))) {
+        table.toggleRowSelection(row, true);
+      }
+    });
+  });
+};
+
+const loadInboundBatches = (supplierId, keepSelected = false, syncAmount = true) => {
+  if (!supplierId) {
+    inboundBatchList.value = [];
+    inboundBatchOptions.value = [];
+    if (!keepSelected) {
+      form.stockInRecordIds = [];
+      form.inboundBatches = "";
+      form.paymentAmount = 0;
+    }
+    return Promise.resolve();
+  }
+  inboundBatchLoading.value = true;
+  return getInboundBatchesBySupplier({ supplierId })
+    .then((res) => {
+      if (res.code === 200) {
+        const list = res.data?.records ?? res.data ?? [];
+        inboundBatchList.value = Array.isArray(list) ? list : [];
+        inboundBatchOptions.value = normalizeInboundBatchOptions(list);
+      } else {
+        inboundBatchList.value = [];
+        inboundBatchOptions.value = [];
+      }
+    })
+    .catch(() => {
+      inboundBatchList.value = [];
+      inboundBatchOptions.value = [];
+    })
+    .finally(() => {
+      inboundBatchLoading.value = false;
+      if (keepSelected) {
+        ensureInboundOptionsForSelected();
+        restoreInboundTableSelection();
+        if (syncAmount && !isView.value) {
+          syncPaymentAmount();
+        }
+      }
+    });
+};
+
+const handleSupplierChange = (supplierId) => {
+  form.stockInRecordIds = [];
+  form.inboundBatches = "";
+  form.paymentAmount = 0;
+  loadInboundBatches(supplierId);
+};
+
+const handleInboundInputClick = () => {
+  if (isEdit.value || isView.value) return;
+  openInboundSelectDialog();
+};
+
+const openInboundSelectDialog = () => {
+  if (!form.supplierId || isEdit.value || isView.value) return;
+  inboundSelectVisible.value = true;
+  loadInboundBatches(form.supplierId, true, false).then(() => {
+    restoreInboundTableSelection();
+  });
+};
+
+const handleInboundDialogSelectionChange = (selection) => {
+  dialogInboundSelection.value = selection;
+};
+
+const confirmInboundSelection = () => {
+  if (dialogInboundSelection.value.length === 0) {
+    ElMessage.warning("璇疯嚦灏戦�夋嫨涓�鏉″叆搴撳崟");
+    return;
+  }
+  form.stockInRecordIds = dialogInboundSelection.value
+    .map((row) => getInboundRowId(row))
+    .filter((id) => id !== undefined && id !== null);
+  form.inboundBatches = dialogInboundSelection.value
+    .map((row) => row.inboundBatches ?? row.batchNo ?? "")
+    .filter(Boolean)
+    .join("銆�");
+  dialogInboundSelection.value.forEach((row) => {
+    const [option] = normalizeInboundBatchOptions([row]);
+    if (option && !inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, option.value))) {
+      inboundBatchOptions.value.push(option);
+    }
+  });
+  inboundSelectVisible.value = false;
+  syncPaymentAmount();
+  formRef.value?.validateField("stockInRecordIds");
+};
+
+const handleInboundDialogClosed = () => {
+  dialogInboundSelection.value = [];
+};
+
+onMounted(() => {
+  getSupplierList();
+  getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 15px;
+}
+
+.text-danger {
+  color: #f56c6c;
+  font-weight: bold;
+}
+
+.inbound-batch-input :deep(.el-input__wrapper) {
+  cursor: pointer;
+}
+</style>
diff --git a/src/views/financialManagement/payable/purchaseIn.vue b/src/views/financialManagement/payable/purchaseIn.vue
new file mode 100644
index 0000000..532bcb4
--- /dev/null
+++ b/src/views/financialManagement/payable/purchaseIn.vue
@@ -0,0 +1,212 @@
+<template>
+  <!-- 閲囪喘鍏ュ簱 -->
+  <div class="app-container">
+    <el-form :model="filters"
+             :inline="true">
+      <el-form-item label="鍏ュ簱鍗曞彿:">
+        <el-input v-model="filters.inboundBatches"
+                  placeholder="璇疯緭鍏ュ叆搴撳崟鍙�"
+                  clearable
+                  style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="渚涘簲鍟�:">
+        <el-select v-model="filters.supplierId"
+                   placeholder="璇烽�夋嫨渚涘簲鍟�"
+                   clearable
+                   filterable
+                   style="width: 200px;">
+          <el-option v-for="item in supplierList"
+                     :key="item.id"
+                     :label="item.supplierName"
+                     :value="item.id" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="鍏ュ簱鏃ユ湡:">
+        <el-date-picker v-model="filters.dateRange"
+                        value-format="YYYY-MM-DD"
+                        format="YYYY-MM-DD"
+                        type="daterange"
+                        range-separator="鑷�"
+                        start-placeholder="寮�濮嬫棩鏈�"
+                        end-placeholder="缁撴潫鏃ユ湡"
+                        clearable />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary"
+                   @click="onSearch">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div></div>
+        <div>
+          <el-button @click="handleOut"
+                     icon="Download">瀵煎嚭</el-button>
+        </div>
+      </div>
+      <PIMTable rowKey="id"
+                :column="columns"
+                :tableData="dataList"
+                :tableLoading="tableLoading"
+                :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+                @pagination="changePage">
+        <template #inboundDate="{ row }">
+          {{ row.inboundDate ?? row.InboundDate ?? "" }}
+        </template>
+      </PIMTable>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+  import { ElMessage } from "element-plus";
+  import { listPageAccountPurchase } from "@/api/financialManagement/accountPurchase";
+  import { listSupplier } from "@/api/basicData/supplierManageFile.js";
+
+  defineOptions({
+    name: "閲囪喘鍏ュ簱",
+  });
+
+  const { proxy } = getCurrentInstance();
+
+  const filters = reactive({
+    inboundBatches: "",
+    supplierId: "",
+    dateRange: [],
+  });
+
+  const pagination = reactive({
+    currentPage: 1,
+    pageSize: 10,
+    total: 0,
+  });
+
+  const columns = [
+    { label: "鍏ュ簱鍗曞彿", prop: "inboundBatches", minWidth: "150" },
+    { label: "渚涘簲鍟�", prop: "supplierName", minWidth: "180" },
+    {
+      label: "鍏ュ簱鏃ユ湡",
+      prop: "inboundDate",
+      minWidth: "170",
+      dataType: "slot",
+      slot: "inboundDate",
+    },
+    { label: "浜у搧鍚嶇О", prop: "productName", minWidth: "140" },
+    { label: "瑙勬牸鍨嬪彿", prop: "specificationModel", minWidth: "140" },
+    {
+      label: "閲戦",
+      prop: "inboundAmount",
+      minWidth: "120",
+      align: "right",
+      formatData: val =>
+        val === null || val === undefined || val === ""
+          ? ""
+          : Number(val).toLocaleString("zh-CN", {
+              minimumFractionDigits: 2,
+              maximumFractionDigits: 2,
+            }),
+    },
+    { label: "閲囪喘璁㈠崟鍙�", prop: "purchaseContractNumber", minWidth: "150" },
+  ];
+
+  const dataList = ref([]);
+  const tableLoading = ref(false);
+  const supplierList = ref([]);
+
+  const buildFilterParams = () => {
+    const params = {};
+    if (filters.inboundBatches) {
+      params.inboundBatches = filters.inboundBatches;
+    }
+    if (filters.supplierId) {
+      params.supplierId = filters.supplierId;
+    }
+    if (filters.dateRange?.length === 2) {
+      params.startDate = filters.dateRange[0];
+      params.endDate = filters.dateRange[1];
+    }
+    return params;
+  };
+
+  const getSupplierList = () => {
+    listSupplier({ current: -1, size: -1, isWhite: 0 }).then(res => {
+      if (res.code === 200) {
+        supplierList.value = res.data?.records ?? [];
+      }
+    });
+  };
+
+  const onSearch = () => {
+    pagination.currentPage = 1;
+    getTableData();
+  };
+
+  const getTableData = () => {
+    tableLoading.value = true;
+    listPageAccountPurchase({
+      ...buildFilterParams(),
+      current: pagination.currentPage,
+      size: pagination.pageSize,
+    })
+      .then(res => {
+        const ok = res.code === 200 || res.code === 0;
+        if (ok && res.data) {
+          pagination.total = res.data.total ?? 0;
+          dataList.value = res.data.records ?? [];
+        } else {
+          ElMessage.error(res.msg || "鏌ヨ澶辫触");
+          dataList.value = [];
+          pagination.total = 0;
+        }
+      })
+      .catch(() => {
+        dataList.value = [];
+        pagination.total = 0;
+        ElMessage.error("鏌ヨ澶辫触");
+      })
+      .finally(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  const resetFilters = () => {
+    filters.inboundBatches = "";
+    filters.supplierId = "";
+    filters.dateRange = [];
+    pagination.currentPage = 1;
+    getTableData();
+  };
+
+  const changePage = ({ page, limit }) => {
+    pagination.currentPage = page;
+    pagination.pageSize = limit;
+    getTableData();
+  };
+
+  const handleOut = () => {
+    proxy.download(
+      "/accountPurchase/exportAccountPurchaseInbound",
+      buildFilterParams(),
+      `閲囪喘鍏ュ簱_${Date.now()}.xlsx`
+    );
+  };
+
+  onMounted(() => {
+    getSupplierList();
+    getTableData();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .actions {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 15px;
+  }
+</style>
diff --git a/src/views/financialManagement/payable/purchaseReturn.vue b/src/views/financialManagement/payable/purchaseReturn.vue
new file mode 100644
index 0000000..eeec383
--- /dev/null
+++ b/src/views/financialManagement/payable/purchaseReturn.vue
@@ -0,0 +1,198 @@
+<template>
+  <!-- 閲囪喘閫�璐� -->
+  <div class="app-container">
+    <el-form :model="filters" :inline="true">
+      <el-form-item label="閫�璐у崟鍙�:">
+        <el-input v-model="filters.returnNo" placeholder="璇疯緭鍏ラ��璐у崟鍙�" clearable style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="渚涘簲鍟�:">
+        <el-select v-model="filters.supplierId" placeholder="璇烽�夋嫨渚涘簲鍟�" clearable filterable style="width: 200px;">
+          <el-option
+            v-for="item in supplierList"
+            :key="item.id"
+            :label="item.supplierName"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="閫�璐ф棩鏈�:">
+        <el-date-picker
+          v-model="filters.dateRange"
+          value-format="YYYY-MM-DD"
+          format="YYYY-MM-DD"
+          type="daterange"
+          range-separator="鑷�"
+          start-placeholder="寮�濮嬫棩鏈�"
+          end-placeholder="缁撴潫鏃ユ湡"
+          clearable
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div></div>
+        <div>
+          <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button>
+        </div>
+      </div>
+      <PIMTable
+        rowKey="id"
+        :column="columns"
+        :tableData="dataList"
+        :tableLoading="tableLoading"
+        :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+        @pagination="changePage"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+import { ElMessage } from "element-plus";
+import { listPageAccountPurchaseReturn } from "@/api/financialManagement/accountPurchase";
+import { listSupplier } from "@/api/basicData/supplierManageFile.js";
+
+defineOptions({
+  name: "閲囪喘閫�璐�",
+});
+
+const { proxy } = getCurrentInstance();
+
+const filters = reactive({
+  returnNo: "",
+  supplierId: "",
+  dateRange: [],
+});
+
+const pagination = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+const columns = [
+  { label: "閫�璐у崟鍙�", prop: "returnNo", minWidth: "150" },
+  { label: "渚涘簲鍟�", prop: "supplierName", minWidth: "180" },
+  { label: "鍏宠仈鍏ュ簱鍗曞彿", prop: "inboundBatches", minWidth: "150" },
+  { label: "閫�璐ф棩鏈�", prop: "preparedAt", minWidth: "170" },
+  {
+    label: "閫�娆炬�婚",
+    prop: "totalAmount",
+    minWidth: "150",
+    align: "right",
+    formatData: (val) =>
+      val === null || val === undefined || val === ""
+        ? ""
+        : Number(val).toLocaleString("zh-CN", {
+            minimumFractionDigits: 2,
+            maximumFractionDigits: 2,
+          }),
+  },
+  { label: "閫�璐ф柟寮�", prop: "returnType", minWidth: "150" },
+  { label: "閲囪喘璁㈠崟鍙�", prop: "purchaseContractNumber", minWidth: "150" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const supplierList = ref([]);
+
+const buildFilterParams = () => {
+  const params = {};
+  if (filters.returnNo) {
+    params.returnNo = filters.returnNo;
+  }
+  if (filters.supplierId) {
+    params.supplierId = filters.supplierId;
+  }
+  if (filters.dateRange?.length === 2) {
+    params.startDate = filters.dateRange[0];
+    params.endDate = filters.dateRange[1];
+  }
+  return params;
+};
+
+const getSupplierList = () => {
+  listSupplier({ current: -1, size: -1, isWhite: 0 }).then((res) => {
+    if (res.code === 200) {
+      supplierList.value = res.data?.records ?? [];
+    }
+  });
+};
+
+const onSearch = () => {
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const getTableData = () => {
+  tableLoading.value = true;
+  listPageAccountPurchaseReturn({
+    ...buildFilterParams(),
+    current: pagination.currentPage,
+    size: pagination.pageSize,
+  })
+    .then((res) => {
+      const ok = res.code === 200 || res.code === 0;
+      if (ok && res.data) {
+        pagination.total = res.data.total ?? 0;
+        dataList.value = res.data.records ?? [];
+      } else {
+        ElMessage.error(res.msg || "鏌ヨ澶辫触");
+        dataList.value = [];
+        pagination.total = 0;
+      }
+    })
+    .catch(() => {
+      dataList.value = [];
+      pagination.total = 0;
+      ElMessage.error("鏌ヨ澶辫触");
+    })
+    .finally(() => {
+      tableLoading.value = false;
+    });
+};
+
+const resetFilters = () => {
+  filters.returnNo = "";
+  filters.supplierId = "";
+  filters.dateRange = [];
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+  pagination.currentPage = page;
+  pagination.pageSize = limit;
+  getTableData();
+};
+
+const handleOut = () => {
+  proxy.download(
+    "/accountPurchase/exportAccountPurchaseReturn",
+    buildFilterParams(),
+    `閲囪喘閫�璐${Date.now()}.xlsx`
+  );
+};
+
+onMounted(() => {
+  getSupplierList();
+  getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 15px;
+}
+</style>
diff --git a/src/views/financialManagement/payable/reconciliation.vue b/src/views/financialManagement/payable/reconciliation.vue
new file mode 100644
index 0000000..e749e56
--- /dev/null
+++ b/src/views/financialManagement/payable/reconciliation.vue
@@ -0,0 +1,766 @@
+<template>
+  <div class="app-container">
+    <el-form :model="filters" :inline="true">
+      <el-form-item label="渚涘簲鍟�:">
+        <el-select v-model="filters.supplierId" placeholder="璇烽�夋嫨渚涘簲鍟�" clearable filterable style="width: 200px;">
+          <el-option
+            v-for="item in supplierList"
+            :key="item.id"
+            :label="item.supplierName"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="瀵硅处鏈熼棿:">
+        <el-date-picker v-model="filters.startMonth" type="month" placeholder="寮�濮嬫湀浠�" value-format="YYYY-MM" style="width: 140px;" />
+        <span style="margin: 0 10px;">鑷�</span>
+        <el-date-picker v-model="filters.endMonth" type="month" placeholder="缁撴潫鏈堜唤" value-format="YYYY-MM" style="width: 140px;" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div>
+          <el-button type="primary" @click="generateStatement" icon="Document">鐢熸垚瀵硅处鍗�</el-button>
+        </div>
+        <div>
+          <el-button @click="handleOut" icon="Download">瀵煎嚭瀵硅处鍗�</el-button>
+        </div>
+      </div>
+      <PIMTable
+        rowKey="id"
+        :column="columns"
+        :tableData="dataList"
+        :tableLoading="tableLoading"
+        :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+        @pagination="changePage"
+      >
+        <template #openingBalance="{ row }">
+          <span :class="row.openingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.openingBalance) }}</span>
+        </template>
+        <template #currentPlan="{ row }">
+          <span class="text-danger">楼{{ formatMoney(row.currentPlan) }}</span>
+        </template>
+        <template #currentActually="{ row }">
+          <span class="text-success">楼{{ formatMoney(row.currentActually) }}</span>
+        </template>
+        <template #closingBalance="{ row }">
+          <span :class="row.closingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.closingBalance) }}</span>
+        </template>
+        <template #operation="{ row }">
+          <el-button type="primary" link @click="viewDetail(row)">鏌ョ湅鏄庣粏</el-button>
+          <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+        </template>
+      </PIMTable>
+    </div>
+
+    <FormDialog title="瀵硅处鏄庣粏" v-model="detailDialogVisible" width="900px" @confirm="printDetail" @cancel="detailDialogVisible = false" operationType="detail">
+      <div class="statement-header">
+        <h3>{{ currentSupplier }} 搴斾粯瀵硅处鍗�</h3>
+        <p>瀵硅处鏈熼棿: {{ currentPeriod }}</p>
+      </div>
+      <el-table :data="detailData" border style="width: 100%" v-loading="detailLoading">
+        <el-table-column prop="date" label="鏃ユ湡" width="120" />
+        <el-table-column prop="type" label="绫诲瀷" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.type === '鍏ュ簱' ? 'success' : row.type === '閫�璐�' ? 'danger' : 'primary'">{{ row.type }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="code" label="鍗曟嵁缂栧彿" width="150" />
+        <el-table-column prop="debit" label="鍊熸柟(浠樻)" width="120">
+          <template #default="{ row }">
+            <span v-if="row.debit > 0" class="text-success">楼{{ formatMoney(row.debit) }}</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="credit" label="璐锋柟(搴斾粯)" width="120">
+          <template #default="{ row }">
+            <span v-if="row.credit > 0" class="text-danger">楼{{ formatMoney(row.credit) }}</span>
+            <span v-else-if="row.credit < 0" class="text-success">楼{{ formatMoney(Math.abs(row.credit)) }}</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="balance" label="浣欓" width="120">
+          <template #default="{ row }">
+            <span :class="row.balance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.balance) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="remark" label="澶囨敞" show-overflow-tooltip />
+      </el-table>
+      <template #footer>
+        <el-button type="primary" @click="printDetail">鎵撳嵃</el-button>
+        <el-button @click="detailDialogVisible = false">鍏抽棴</el-button>
+      </template>
+    </FormDialog>
+
+    <FormDialog title="鐢熸垚瀵硅处鍗�" v-model="generateDialogVisible" width="1000px" @confirm="confirmGenerate" @cancel="generateDialogVisible = false">
+      <el-form :model="generateForm" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="閫夋嫨渚涘簲鍟�" prop="supplierId">
+              <el-select
+                v-model="generateForm.supplierId"
+                placeholder="璇烽�夋嫨渚涘簲鍟�"
+                style="width: 100%;"
+                filterable
+                @change="onSupplierChange"
+              >
+                <el-option
+                  v-for="item in supplierList"
+                  :key="item.id"
+                  :label="item.supplierName"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="瀵硅处鏈堜唤" prop="statementMonth">
+              <el-date-picker
+                v-model="generateForm.statementMonth"
+                type="month"
+                placeholder="閫夋嫨鏈堜唤"
+                value-format="YYYY-MM"
+                style="width: 100%;"
+                @change="onStatementMonthChange"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <div v-if="statementDetailLoaded" class="purchase-section">
+        <div v-if="purchaseData.length > 0" class="section-title">鏈湀閲囪喘鏁版嵁</div>
+        <el-table
+          v-if="purchaseData.length > 0"
+          ref="purchaseTableRef"
+          :data="purchaseData"
+          border
+          row-key="id"
+          style="width: 100%; margin-bottom: 15px;"
+          v-loading="purchaseLoading"
+          @selection-change="handlePurchaseSelectionChange"
+        >
+          <el-table-column type="selection" width="55" align="center" />
+          <el-table-column prop="occurrenceDate" label="鏃ユ湡" width="120" />
+          <el-table-column prop="receiptNumber" label="鍗曟嵁缂栧彿" width="150" />
+          <el-table-column prop="type" label="绫诲瀷" width="100">
+            <template #default="{ row }">
+              <el-tag :type="getDetailTypeTagType(row.type)">{{ row.typeLabel }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="amount" label="閲戦" width="120">
+            <template #default="{ row }">
+              <span :class="getDetailAmountClass(row.type)">楼{{ formatMoney(row.amount) }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="remark" label="澶囨敞" />
+        </el-table>
+        <el-empty v-else description="璇ヤ緵搴斿晢鏈湀鏆傛棤鏄庣粏鏁版嵁" :image-size="80" />
+
+        <div class="summary-row">
+          <span>鏈熷垵浣欓: <strong class="text-primary">楼{{ formatMoney(generateForm.openingBalance) }}</strong></span>
+          <span>鏈湡搴斾粯: <strong class="text-danger">楼{{ formatMoney(generateForm.currentPlan) }}</strong></span>
+          <span>鏈湡浠樻: <strong class="text-success">楼{{ formatMoney(generateForm.currentActually) }}</strong></span>
+          <span>鏈熸湯浣欓: <strong :class="displayClosingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(displayClosingBalance) }}</strong></span>
+        </div>
+      </div>
+
+      <div v-else-if="generateForm.supplierId && generateForm.statementMonth && !purchaseLoading" class="empty-tip">
+        <el-empty description="璇ヤ緵搴斿晢鏈湀鏆傛棤閲囪喘鏁版嵁" />
+      </div>
+
+      <template #footer>
+        <el-button type="primary" @click="confirmGenerate" :disabled="!canGenerate" :loading="submitLoading">纭鐢熸垚</el-button>
+        <el-button @click="generateDialogVisible = false">鍙栨秷</el-button>
+      </template>
+    </FormDialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, nextTick, getCurrentInstance } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
+import {
+  getAccountStatementDetailsByMonth,
+  addAccountStatement,
+  listPageAccountStatement,
+  deleteAccountStatement,
+} from "@/api/financialManagement/accountStatement.js";
+
+const ACCOUNT_TYPE_PAYABLE = 2;
+
+const { proxy } = getCurrentInstance();
+
+defineOptions({
+  name: "搴斾粯瀵硅处",
+});
+
+const filters = reactive({
+  supplierId: "",
+  startMonth: "",
+  endMonth: "",
+});
+
+const pagination = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+const columns = [
+  { label: "瀵硅处鍗曞彿", prop: "statementNumber", width: "150" },
+  { label: "渚涘簲鍟�", prop: "supplierName", width: "180" },
+  { label: "瀵硅处鏈熼棿", prop: "statementMonth", width: "150" },
+  { label: "鏈熷垵浣欓", prop: "openingBalance", dataType: "slot", slot: "openingBalance" },
+  { label: "鏈湡搴斾粯", prop: "currentPlan", dataType: "slot", slot: "currentPlan" },
+  { label: "鏈湡浠樻", prop: "currentActually", dataType: "slot", slot: "currentActually" },
+  { label: "鏈熸湯浣欓", prop: "closingBalance", dataType: "slot", slot: "closingBalance" },
+  { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "200", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const submitLoading = ref(false);
+const detailDialogVisible = ref(false);
+const currentSupplier = ref("");
+const currentPeriod = ref("");
+const detailData = ref([]);
+const detailLoading = ref(false);
+
+const generateDialogVisible = ref(false);
+const purchaseLoading = ref(false);
+const statementDetailLoaded = ref(false);
+const purchaseData = ref([]);
+const selectedPurchases = ref([]);
+const purchaseTableRef = ref(null);
+const supplierList = ref([]);
+
+/** 鏄庣粏 type锛�1鍑哄簱 2鍏ュ簱 3鏀舵 4浠樻 5閫�璐� */
+const STATEMENT_DETAIL_TYPE_MAP = {
+  1: "鍑哄簱",
+  2: "鍏ュ簱",
+  3: "鏀舵",
+  4: "浠樻",
+  5: "閫�璐�",
+};
+
+const calculateEndBalance = (openingBalance, currentPlan, currentActually) => {
+  return openingBalance + currentPlan - currentActually;
+};
+
+const getDetailTypeLabel = (type) => STATEMENT_DETAIL_TYPE_MAP[Number(type)] ?? "";
+
+const getDetailTypeTagType = (type) => {
+  const t = Number(type);
+  if (t === 2) return "success";
+  if (t === 4) return "primary";
+  if (t === 5) return "danger";
+  return "info";
+};
+
+const getDetailAmountClass = (type) => {
+  const t = Number(type);
+  if (t === 2) return "text-danger";
+  if (t === 4) return "text-success";
+  return "text-danger";
+};
+
+const generateForm = reactive({
+  supplierId: "",
+  supplierName: "",
+  statementMonth: "",
+  openingBalance: 0,
+  currentPlan: 0,
+  currentActually: 0,
+  closingBalance: 0,
+});
+
+const displayClosingBalance = computed(() =>
+  calculateEndBalance(
+    generateForm.openingBalance,
+    generateForm.currentPlan,
+    generateForm.currentActually
+  )
+);
+
+const canGenerate = computed(
+  () => generateForm.supplierId && generateForm.statementMonth && selectedPurchases.value.length > 0
+);
+
+const applyStatementSummary = (data) => {
+  generateForm.openingBalance = Number(data.openingBalance ?? 0);
+  generateForm.currentPlan = Number(data.currentPlan ?? 0);
+  generateForm.currentActually = Number(data.currentActually ?? 0);
+  generateForm.closingBalance = Number(
+    data.closingBalance ??
+      calculateEndBalance(
+        generateForm.openingBalance,
+        generateForm.currentPlan,
+        generateForm.currentActually
+      )
+  );
+};
+
+const getSupplierList = () => {
+  getOptions().then((res) => {
+    if (res.code === 200) {
+      supplierList.value = res.data ?? [];
+    }
+  });
+};
+
+const normalizePurchaseRows = (list) => {
+  const rows = Array.isArray(list) ? list : [];
+  return rows.map((item, index) => {
+    const type = Number(item.type);
+    return {
+      id: item.id ?? `detail-${index}`,
+      accountStatementId: item.accountStatementId,
+      occurrenceDate: item.occurrenceDate ?? "",
+      receiptNumber: item.receiptNumber ?? "",
+      type,
+      typeLabel: getDetailTypeLabel(type),
+      amount: Math.abs(Number(item.amount ?? 0)),
+      remark: item.remark ?? "",
+    };
+  });
+};
+
+const selectAllPurchaseRows = (keepApiSummary = false) => {
+  nextTick(() => {
+    const table = purchaseTableRef.value;
+    if (!table) return;
+    table.clearSelection();
+    purchaseData.value.forEach((row) => table.toggleRowSelection(row, true));
+    selectedPurchases.value = [...purchaseData.value];
+    if (!keepApiSummary) {
+      calculateSummary();
+    }
+  });
+};
+
+const isNumericId = (id) => id !== undefined && id !== null && id !== "" && /^\d+$/.test(String(id));
+
+const buildFilterParams = (params = {}) => {
+  const result = { ...params, accountType: ACCOUNT_TYPE_PAYABLE };
+  if (filters.supplierId) {
+    result.customerId = filters.supplierId;
+  }
+  if (filters.startMonth && filters.endMonth && filters.startMonth === filters.endMonth) {
+    result.statementMonth = filters.startMonth;
+  } else if (filters.startMonth) {
+    result.startMonth = filters.startMonth;
+  }
+  if (filters.endMonth && filters.startMonth !== filters.endMonth) {
+    result.endMonth = filters.endMonth;
+  }
+  return result;
+};
+
+const buildListParams = () =>
+  buildFilterParams({
+    current: pagination.currentPage,
+    size: pagination.pageSize,
+  });
+
+const buildExportParams = () => buildFilterParams({});
+
+const buildDetailSubmitItem = (row) => {
+  const item = {
+    occurrenceDate: row.occurrenceDate,
+    receiptNumber: row.receiptNumber,
+    type: row.type,
+    amount: row.amount,
+    remark: row.remark ?? "",
+  };
+  if (isNumericId(row.id)) {
+    item.id = Number(row.id);
+  }
+  if (row.accountStatementId) {
+    item.accountStatementId = row.accountStatementId;
+  }
+  return item;
+};
+
+const buildAddPayload = () => ({
+  customerId: generateForm.supplierId,
+  customerName: generateForm.supplierName,
+  statementMonth: generateForm.statementMonth,
+  accountType: ACCOUNT_TYPE_PAYABLE,
+  statementNumber: "",
+  openingBalance: generateForm.openingBalance,
+  currentPlan: generateForm.currentPlan,
+  currentActually: generateForm.currentActually,
+  closingBalance: generateForm.closingBalance,
+  accountStatementDetails: selectedPurchases.value.map(buildDetailSubmitItem),
+});
+
+const formatMoney = (value) => {
+  if (value === undefined || value === null) return "0.00";
+  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getTableData = () => {
+  tableLoading.value = true;
+  listPageAccountStatement(buildListParams())
+    .then((res) => {
+      const ok = res.code === 200 || res.code === 0;
+      if (ok && res.data) {
+        pagination.total = res.data.total ?? 0;
+        dataList.value = (res.data.records ?? []).map((row) => ({
+          ...row,
+          supplierName: row.supplierName ?? row.customerName,
+        }));
+      } else {
+        ElMessage.error(res.msg || "鏌ヨ澶辫触");
+        dataList.value = [];
+        pagination.total = 0;
+      }
+    })
+    .catch(() => {
+      dataList.value = [];
+      pagination.total = 0;
+      ElMessage.error("鏌ヨ澶辫触");
+    })
+    .finally(() => {
+      tableLoading.value = false;
+    });
+};
+
+const onSearch = () => {
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const resetFilters = () => {
+  filters.supplierId = "";
+  filters.startMonth = "";
+  filters.endMonth = "";
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+  pagination.currentPage = page;
+  pagination.pageSize = limit;
+  getTableData();
+};
+
+const generateStatement = () => {
+  generateForm.supplierId = "";
+  generateForm.supplierName = "";
+  generateForm.statementMonth = "";
+  generateForm.openingBalance = 0;
+  generateForm.currentPlan = 0;
+  generateForm.currentActually = 0;
+  generateForm.closingBalance = 0;
+  statementDetailLoaded.value = false;
+  purchaseData.value = [];
+  selectedPurchases.value = [];
+  generateDialogVisible.value = true;
+};
+
+const onSupplierChange = (supplierId) => {
+  const supplier = supplierList.value.find((item) => item.id === supplierId);
+  generateForm.supplierName = supplier?.supplierName ?? "";
+  loadPurchaseData();
+};
+
+const onStatementMonthChange = () => {
+  loadPurchaseData();
+};
+
+const loadPurchaseData = () => {
+  if (!generateForm.supplierId || !generateForm.statementMonth) {
+    purchaseData.value = [];
+    selectedPurchases.value = [];
+    statementDetailLoaded.value = false;
+    generateForm.openingBalance = 0;
+    generateForm.currentPlan = 0;
+    generateForm.currentActually = 0;
+    generateForm.closingBalance = 0;
+    return;
+  }
+
+  purchaseLoading.value = true;
+  selectedPurchases.value = [];
+  statementDetailLoaded.value = false;
+
+  getAccountStatementDetailsByMonth({
+    accountType: ACCOUNT_TYPE_PAYABLE,
+    customerId: generateForm.supplierId,
+    statementMonth: generateForm.statementMonth,
+  })
+    .then((res) => {
+      if (res.code === 200) {
+        const data = res.data ?? {};
+        const details = data.accountStatementDetails;
+        const list = Array.isArray(details) ? details : [];
+        purchaseData.value = normalizePurchaseRows(list);
+        applyStatementSummary(data);
+        statementDetailLoaded.value = true;
+
+        if (purchaseData.value.length > 0) {
+          selectAllPurchaseRows(true);
+        }
+      } else {
+        purchaseData.value = [];
+        statementDetailLoaded.value = false;
+        ElMessage.error(res.msg || "鏌ヨ瀵硅处鏄庣粏澶辫触");
+      }
+    })
+    .catch(() => {
+      purchaseData.value = [];
+      statementDetailLoaded.value = false;
+      ElMessage.error("鏌ヨ瀵硅处鏄庣粏澶辫触");
+    })
+    .finally(() => {
+      purchaseLoading.value = false;
+    });
+};
+
+const calculateSummary = () => {
+  let payable = 0;
+  let payment = 0;
+
+  selectedPurchases.value.forEach((item) => {
+    if (item.type === 2) {
+      payable += item.amount;
+    } else if (item.type === 5) {
+      payable -= item.amount;
+    } else if (item.type === 4) {
+      payment += item.amount;
+    }
+  });
+
+  generateForm.currentPlan = payable;
+  generateForm.currentActually = payment;
+  generateForm.closingBalance = calculateEndBalance(
+    generateForm.openingBalance,
+    generateForm.currentPlan,
+    generateForm.currentActually
+  );
+};
+
+const handlePurchaseSelectionChange = (selection) => {
+  selectedPurchases.value = selection;
+  calculateSummary();
+};
+
+const confirmGenerate = () => {
+  if (!canGenerate.value) return;
+  submitLoading.value = true;
+  addAccountStatement(buildAddPayload())
+    .then((res) => {
+      if (res.code === 200) {
+        generateDialogVisible.value = false;
+        ElMessage.success("瀵硅处鍗曠敓鎴愭垚鍔�");
+        pagination.currentPage = 1;
+        getTableData();
+      } else {
+        ElMessage.error(res.msg || "鐢熸垚澶辫触");
+      }
+    })
+    .catch(() => {
+      ElMessage.error("鐢熸垚澶辫触");
+    })
+    .finally(() => {
+      submitLoading.value = false;
+    });
+};
+
+const handleDelete = (row) => {
+  ElMessageBox.confirm(`纭鍒犻櫎瀵硅处鍗曘��${row.statementNumber || row.id}銆嶅悧锛焋, "鎻愮ず", {
+    confirmButtonText: "纭畾",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(() => {
+    deleteAccountStatement([row.id])
+      .then((res) => {
+        if (res.code === 200) {
+          ElMessage.success("鍒犻櫎鎴愬姛");
+          getTableData();
+        } else {
+          ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+        }
+      })
+      .catch(() => {
+        ElMessage.error("鍒犻櫎澶辫触");
+      });
+  });
+};
+
+const buildDetailTableFromApi = (data, statementMonth) => {
+  const details = Array.isArray(data.accountStatementDetails) ? data.accountStatementDetails : [];
+  let runningBalance = Number(data.openingBalance ?? 0);
+  const rows = [
+    {
+      date: statementMonth ?? "",
+      type: "鏈熷垵",
+      code: "-",
+      debit: 0,
+      credit: 0,
+      balance: runningBalance,
+      remark: "鏈熷垵浣欓",
+    },
+  ];
+
+  details.forEach((item) => {
+    const amount = Math.abs(Number(item.amount ?? 0));
+    const type = Number(item.type);
+    let debit = 0;
+    let credit = 0;
+
+    if (type === 2) {
+      credit = amount;
+      runningBalance += amount;
+    } else if (type === 4) {
+      debit = amount;
+      runningBalance -= amount;
+    } else if (type === 5) {
+      credit = -amount;
+      runningBalance -= amount;
+    }
+
+    rows.push({
+      date: item.occurrenceDate ?? "",
+      type: getDetailTypeLabel(type),
+      code: item.receiptNumber ?? "",
+      debit,
+      credit,
+      balance: runningBalance,
+      remark: item.remark ?? "",
+    });
+  });
+
+  return rows;
+};
+
+const viewDetail = (row) => {
+  const partnerId = row.customerId ?? row.supplierId;
+  if (!partnerId || !row.statementMonth) {
+    ElMessage.warning("缂哄皯渚涘簲鍟嗘垨瀵硅处鏈堜唤锛屾棤娉曟煡璇㈡槑缁�");
+    return;
+  }
+
+  currentSupplier.value = row.supplierName ?? row.customerName ?? "";
+  currentPeriod.value = row.statementMonth ?? "";
+  detailData.value = [];
+  detailDialogVisible.value = true;
+  detailLoading.value = true;
+
+  getAccountStatementDetailsByMonth({
+    accountType: ACCOUNT_TYPE_PAYABLE,
+    customerId: partnerId,
+    statementMonth: row.statementMonth,
+  })
+    .then((res) => {
+      if (res.code === 200) {
+        detailData.value = buildDetailTableFromApi(res.data ?? {}, row.statementMonth);
+      } else {
+        ElMessage.error(res.msg || "鏌ヨ鏄庣粏澶辫触");
+        detailDialogVisible.value = false;
+      }
+    })
+    .catch(() => {
+      ElMessage.error("鏌ヨ鏄庣粏澶辫触");
+      detailDialogVisible.value = false;
+    })
+    .finally(() => {
+      detailLoading.value = false;
+    });
+};
+
+const printDetail = () => {
+  ElMessage.info("鎵撳嵃鏄庣粏");
+};
+
+const handleOut = () => {
+  proxy.download(
+    "/accountStatement/exportAccountStatement",
+    buildExportParams(),
+    `搴斾粯瀵硅处鍗昣${Date.now()}.xlsx`
+  );
+};
+
+onMounted(() => {
+  getSupplierList();
+  getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 15px;
+}
+
+.text-success {
+  color: #67c23a;
+}
+
+.text-danger {
+  color: #f56c6c;
+}
+
+.text-primary {
+  color: #409eff;
+}
+
+.statement-header {
+  text-align: center;
+  margin-bottom: 20px;
+  h3 {
+    margin: 0 0 10px 0;
+  }
+  p {
+    color: #909399;
+    margin: 0;
+  }
+}
+
+.purchase-section {
+  margin-top: 20px;
+
+  .section-title {
+    font-size: 16px;
+    font-weight: bold;
+    margin-bottom: 15px;
+    padding-left: 10px;
+    border-left: 4px solid #409eff;
+  }
+}
+
+.summary-row {
+  display: flex;
+  justify-content: space-around;
+  padding: 15px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  margin-top: 15px;
+
+  span {
+    font-size: 14px;
+
+    strong {
+      font-size: 16px;
+      margin-left: 5px;
+    }
+  }
+}
+
+.empty-tip {
+  margin-top: 30px;
+}
+</style>
diff --git a/src/views/financialManagement/receivable/invoiceApply.vue b/src/views/financialManagement/receivable/invoiceApply.vue
new file mode 100644
index 0000000..31b6345
--- /dev/null
+++ b/src/views/financialManagement/receivable/invoiceApply.vue
@@ -0,0 +1,902 @@
+<template>
+  <div class="app-container">
+    <el-form :model="filters" :inline="true">
+      <el-form-item label="鐢宠鍗曞彿:">
+        <el-input v-model="filters.applyCode" placeholder="璇疯緭鍏ョ敵璇峰崟鍙�" clearable style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="瀹㈡埛:">
+        <el-select v-model="filters.customerId" placeholder="璇烽�夋嫨瀹㈡埛" clearable style="width: 200px;">
+          <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="瀹℃牳鐘舵��:">
+        <el-select v-model="filters.status" placeholder="璇烽�夋嫨瀹℃牳鐘舵��" clearable style="width: 150px;">
+          <el-option label="寰呭鏍�" :value="0" />
+          <el-option label="瀹℃牳閫氳繃" :value="1" />
+          <el-option label="瀹℃牳涓嶉�氳繃" :value="2" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="鐢宠鏃ユ湡:">
+        <el-date-picker
+          v-model="filters.dateRange"
+          type="daterange"
+          value-format="YYYY-MM-DD"
+          format="YYYY-MM-DD"
+          range-separator="鑷�"
+          start-placeholder="寮�濮嬫棩鏈�"
+          end-placeholder="缁撴潫鏃ユ湡"
+          clearable
+          style="width: 240px;"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div></div>
+        <div>
+          <el-button type="primary" @click="add" icon="Plus">鏂板鐢宠</el-button>
+          <el-button type="success" @click="handleExport" icon="Download">瀵煎嚭寮�绁ㄧ敵璇�</el-button>
+        </div>
+      </div>
+      <PIMTable
+        rowKey="id"
+        isSelection
+        v-loading="tableLoading"
+        :column="columns"
+        :tableData="dataList"
+        :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+        @selection-change="handleSelectionChange"
+        @pagination="changePage"
+      >
+        <template #amount="{ row }">
+          <span class="text-primary">楼{{ formatMoney(row.amount) }}</span>
+        </template>
+        <template #taxRate="{ row }">
+          <span>{{ row.taxRate }}%</span>
+        </template>
+        <template #status="{ row }">
+          <el-tag :type="getStatusType(row.status)" effect="light" round>
+            {{ getStatusLabel(row.status) }}
+          </el-tag>
+        </template>
+        <template #operation="{ row }">
+          <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+          <el-button type="primary" link @click="edit(row)" v-if="isPendingStatus(row.status)">缂栬緫</el-button>
+          <el-button type="danger" link @click="handleDelete(row)" v-if="isPendingStatus(row.status)">鍒犻櫎</el-button>
+          <el-button type="success" link @click="handleAudit(row)" v-if="isPendingStatus(row.status)">瀹℃牳</el-button>
+          <el-button type="primary" link @click="openFileDialog(row)" v-if="isApprovedStatus(row.status)">闄勪欢</el-button>
+        </template>
+      </PIMTable>
+    </div>
+
+    <FormDialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="800px"
+      :operation-type="isView ? 'detail' : ''"
+      @confirm="submitForm"
+      @cancel="closeDialog"
+    >
+      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+        <el-row v-if="isView" :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="瀹℃牳鐘舵��">
+              <el-tag :type="getStatusType(form.status)" effect="light" round>
+                {{ getStatusLabel(form.status) }}
+              </el-tag>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="鐢宠鍗曞彿" prop="applyCode">
+              <el-input v-model="form.applyCode" placeholder="绯荤粺鑷姩鐢熸垚" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="瀹㈡埛" prop="customerId">
+              <el-select
+                v-model="form.customerId"
+                placeholder="璇烽�夋嫨瀹㈡埛"
+                style="width: 100%;"
+                :disabled="isEdit || isView"
+                filterable
+                @change="handleCustomerChange"
+              >
+                <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鍑哄簱鍗曞彿" prop="outboundBatchNos">
+              <el-input
+                :model-value="outboundBatchDisplayText"
+                placeholder="璇峰厛閫夋嫨瀹㈡埛"
+                readonly
+                :disabled="!form.customerId || isEdit || isView"
+                class="outbound-batch-input"
+                @click="handleOutboundInputClick"
+              >
+                <template v-if="!isEdit && !isView" #append>
+                  <el-button
+                    :disabled="!form.customerId"
+                    :loading="outboundBatchLoading"
+                    @click.stop="openOutboundSelectDialog"
+                  >
+                    閫夋嫨
+                  </el-button>
+                </template>
+              </el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="寮�绁ㄩ噾棰�" prop="amount">
+              <el-input-number
+                v-model="form.amount"
+                :min="0"
+                :precision="2"
+                :disabled="isView"
+                style="width: 100%;"
+                placeholder="鏍规嵁鎵�閫夊嚭搴撳崟鑷姩姹囨�伙紝鍙慨鏀�"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="绋庣巼" prop="taxRate">
+              <el-select v-model="form.taxRate" placeholder="璇烽�夋嫨绋庣巼" style="width: 100%;" :disabled="isView">
+                <el-option
+                  v-for="dict in tax_rate"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="Number(dict.value)"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鍙戠エ绫诲瀷" prop="invoiceType">
+              <el-select v-model="form.invoiceType" placeholder="璇烽�夋嫨鍙戠エ绫诲瀷" style="width: 100%;" :disabled="isView">
+                <el-option label="澧炲�肩◣涓撶敤鍙戠エ" value="澧炲�肩◣涓撶敤鍙戠エ" />
+                <el-option label="澧炲�肩◣鏅�氬彂绁�" value="澧炲�肩◣鏅�氬彂绁�" />
+                <el-option label="鐢靛瓙鍙戠エ" value="鐢靛瓙鍙戠エ" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鐢宠鏃ユ湡" prop="applyDate">
+              <el-date-picker
+                v-model="form.applyDate"
+                type="date"
+                placeholder="閫夋嫨鏃ユ湡"
+                value-format="YYYY-MM-DD"
+                style="width: 100%;"
+                :disabled="isView"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="鍙戠エ鍐呭" prop="content">
+          <el-input v-model="form.content" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ彂绁ㄥ唴瀹�" :disabled="isView" />
+        </el-form-item>
+        <el-form-item label="澶囨敞" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" :disabled="isView" />
+        </el-form-item>
+      </el-form>
+      <template v-if="!isView" #footer>
+        <el-button type="primary" :loading="submitLoading" @click="submitForm">纭畾</el-button>
+        <el-button @click="closeDialog">鍙栨秷</el-button>
+      </template>
+    </FormDialog>
+
+    <el-dialog
+      v-model="outboundSelectVisible"
+      title="閫夋嫨鍑哄簱鍗�"
+      width="1200px"
+      append-to-body
+      destroy-on-close
+      :close-on-click-modal="false"
+      @closed="handleOutboundDialogClosed"
+    >
+      <el-table
+        ref="outboundTableRef"
+        v-loading="outboundBatchLoading"
+        :data="outboundBatchList"
+        row-key="id"
+        border
+        stripe
+        max-height="480"
+        @selection-change="handleOutboundDialogSelectionChange"
+      >
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column prop="outboundBatches" label="鍑哄簱鍗曞彿" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="customerName" label="瀹㈡埛鍚嶇О" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="productName" label="浜у搧鍚嶇О" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="specificationModel" label="瑙勬牸鍨嬪彿" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="salesContractNo" label="閿�鍞悎鍚屽彿" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="shippingNo" label="鍙戣揣鍗曞彿" min-width="130" show-overflow-tooltip />
+        <el-table-column prop="shippingDate" label="鍙戣揣鏃ユ湡" width="110" align="center" />
+        <el-table-column prop="outboundAmount" label="鍑哄簱閲戦" width="110" align="right">
+          <template #default="{ row }">楼{{ formatMoney(row.outboundAmount) }}</template>
+        </el-table-column>
+        <el-table-column prop="taxRate" label="绋庣巼" width="80" align="center">
+          <template #default="{ row }">{{ row.taxRate }}%</template>
+        </el-table-column>
+      </el-table>
+      <template #footer>
+        <el-button type="primary" @click="confirmOutboundSelection">纭畾</el-button>
+        <el-button @click="outboundSelectVisible = false">鍙栨秷</el-button>
+      </template>
+    </el-dialog>
+
+    <FileList
+      v-if="fileDialogVisible"
+      v-model:visible="fileDialogVisible"
+      record-type="account_invoice_application"
+      :record-id="currentRecordId"
+    />
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance, defineAsyncComponent } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { listCustomer } from "@/api/basicData/customer.js";
+import {
+  getOutboundBatchesByCustomer,
+  addAccountInvoiceApplication,
+  listPageAccountInvoiceApplication,
+  auditAccountInvoiceApplication,
+  updateAccountInvoiceApplication,
+  deleteAccountInvoiceApplication,
+} from "@/api/financialManagement/invoiceApply.js";
+
+const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
+
+defineOptions({
+  name: "寮�绁ㄧ敵璇�",
+});
+
+const { proxy } = getCurrentInstance();
+const { tax_rate } = proxy.useDict("tax_rate");
+
+const filters = reactive({
+  applyCode: "",
+  customerId: "",
+  status: "",
+  dateRange: [],
+});
+
+const pagination = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+const columns = [
+  { label: "鐢宠鍗曞彿", prop: "applyCode", width: "150" },
+  { label: "瀹㈡埛鍚嶇О", prop: "customerName", width: "180" },
+  { label: "寮�绁ㄩ噾棰�", prop: "amount", dataType: "slot", slot: "amount" },
+  { label: "绋庣巼", prop: "taxRate", dataType: "slot", slot: "taxRate" },
+  { label: "鍙戠エ绫诲瀷", prop: "invoiceType", width: "130" },
+  { label: "鐢宠鏃ユ湡", prop: "applyDate", width: "120" },
+  { label: "瀹℃牳鐘舵��", prop: "status", dataType: "slot", slot: "status", width: "110", align: "center" },
+  { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "300", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const selectedRows = ref([]);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isEdit = ref(false);
+const isView = ref(false);
+const currentId = ref(null);
+
+const closeDialog = () => {
+  dialogVisible.value = false;
+  outboundSelectVisible.value = false;
+  isView.value = false;
+  isEdit.value = false;
+};
+
+const customerList = ref([]);
+const outboundBatchList = ref([]);
+const outboundBatchOptions = ref([]);
+const outboundBatchLoading = ref(false);
+const outboundSelectVisible = ref(false);
+const outboundTableRef = ref(null);
+const dialogOutboundSelection = ref([]);
+
+const getCustomerList = () => {
+  listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
+    if (res.code === 200) {
+      customerList.value = res.data?.records || [];
+    }
+  });
+};
+
+const normalizeOutboundBatchOptions = (data) => {
+  const list = Array.isArray(data) ? data : [];
+  return list.map((item, index) => {
+    if (typeof item === "string" || typeof item === "number") {
+      const text = String(item);
+      return { label: text, value: text, outboundAmount: 0 };
+    }
+    const label =
+      item.outboundBatches ??
+      item.batchNo ??
+      item.shippingNo ??
+      item.outboundNo ??
+      item.label ??
+      `鍑哄簱鍗�${index + 1}`;
+    const value = item.id ?? item.stockOutRecordId ?? item.stockOutRecordIds ?? label;
+    const outboundAmount = Number(item.outboundAmount) || 0;
+    const taxRate =
+      item.taxRate !== undefined && item.taxRate !== null && item.taxRate !== ""
+        ? Number(item.taxRate)
+        : undefined;
+    return { label: String(label), value, outboundAmount, taxRate };
+  });
+};
+
+const isSameOutboundId = (a, b) => String(a) === String(b);
+
+const getSelectedOutboundOptions = () => {
+  const selected = form.outboundBatchNos || [];
+  return outboundBatchOptions.value.filter((opt) =>
+    selected.some((id) => isSameOutboundId(id, opt.value))
+  );
+};
+
+/** 鏍¢獙鎵�閫夊嚭搴撳崟绋庣巼鏄惁涓�鑷达紝涓�鑷村垯鍥炲~ form.taxRate */
+const checkTaxRateConsistency = (showMessage = true) => {
+  const selected = getSelectedOutboundOptions();
+  if (selected.length === 0) return true;
+
+  const withTaxRate = selected.filter(
+    (opt) => opt.taxRate !== undefined && opt.taxRate !== null && !Number.isNaN(opt.taxRate)
+  );
+  if (withTaxRate.length === 0) return true;
+
+  const uniqueRates = [...new Set(withTaxRate.map((opt) => Number(opt.taxRate)))];
+  if (uniqueRates.length > 1) {
+    if (showMessage) {
+      const detail = withTaxRate.map((opt) => `${opt.label}(${opt.taxRate}%)`).join("銆�");
+      ElMessage.error(`鎵�閫夊嚭搴撳崟绋庣巼涓嶄竴鑷达紝鏃犳硶寮�绁細${detail}`);
+    }
+    return false;
+  }
+
+  form.taxRate = uniqueRates[0];
+  return true;
+};
+
+/** 鏍规嵁鎵�閫夊嚭搴撳崟姹囨�� outboundAmount 浣滀负寮�绁ㄩ噾棰� */
+const syncInvoiceAmount = () => {
+  const selected = form.outboundBatchNos || [];
+  const sum = outboundBatchOptions.value
+    .filter((opt) => selected.some((id) => isSameOutboundId(id, opt.value)))
+    .reduce((acc, opt) => acc + (Number(opt.outboundAmount) || 0), 0);
+  form.amount = sum > 0 ? Number(sum.toFixed(2)) : 0;
+};
+
+const getOutboundRowId = (row) => row?.id ?? row?.stockOutRecordId;
+
+const outboundBatchDisplayText = computed(() => {
+  if (isEdit.value || isView.value) {
+    return form.outboundBatches || "";
+  }
+  if (form.outboundBatches) return form.outboundBatches;
+  const ids = form.outboundBatchNos || [];
+  if (!ids.length) return "";
+  return outboundBatchOptions.value
+    .filter((opt) => ids.some((id) => isSameOutboundId(id, opt.value)))
+    .map((opt) => opt.label)
+    .join("銆�");
+});
+
+const handleOutboundInputClick = () => {
+  if (isEdit.value || isView.value) return;
+  openOutboundSelectDialog();
+};
+
+const restoreOutboundTableSelection = () => {
+  nextTick(() => {
+    const table = outboundTableRef.value;
+    if (!table) return;
+    table.clearSelection();
+    const selectedIds = new Set((form.outboundBatchNos || []).map((id) => String(id)));
+    outboundBatchList.value.forEach((row) => {
+      const rowId = getOutboundRowId(row);
+      if (rowId !== undefined && rowId !== null && selectedIds.has(String(rowId))) {
+        table.toggleRowSelection(row, true);
+      }
+    });
+  });
+};
+
+const openOutboundSelectDialog = () => {
+  if (!form.customerId || isEdit.value || isView.value) return;
+  outboundSelectVisible.value = true;
+  loadOutboundBatches(form.customerId, true).then(() => {
+    restoreOutboundTableSelection();
+  });
+};
+
+const handleOutboundDialogSelectionChange = (selection) => {
+  dialogOutboundSelection.value = selection;
+};
+
+const confirmOutboundSelection = () => {
+  if (dialogOutboundSelection.value.length === 0) {
+    ElMessage.warning("璇疯嚦灏戦�夋嫨涓�鏉″嚭搴撳崟");
+    return;
+  }
+  const prevIds = [...(form.outboundBatchNos || [])];
+  const prevBatches = form.outboundBatches;
+  form.outboundBatchNos = dialogOutboundSelection.value
+    .map((row) => getOutboundRowId(row))
+    .filter((id) => id !== undefined && id !== null);
+  form.outboundBatches = dialogOutboundSelection.value
+    .map((row) => row.outboundBatches ?? row.batchNo ?? row.shippingNo ?? "")
+    .filter(Boolean)
+    .join("銆�");
+  if (!checkTaxRateConsistency()) {
+    form.outboundBatchNos = prevIds;
+    form.outboundBatches = prevBatches;
+    return;
+  }
+  outboundSelectVisible.value = false;
+  syncInvoiceAmount();
+  formRef.value?.validateField("outboundBatchNos");
+};
+
+const handleOutboundDialogClosed = () => {
+  dialogOutboundSelection.value = [];
+};
+
+const loadOutboundBatches = (customerId, keepSelected = false) => {
+  if (!customerId) {
+    outboundBatchList.value = [];
+    outboundBatchOptions.value = [];
+    if (!keepSelected) {
+      form.outboundBatchNos = [];
+      form.amount = 0;
+    }
+    return Promise.resolve();
+  }
+  outboundBatchLoading.value = true;
+  return getOutboundBatchesByCustomer({ customerId })
+    .then((res) => {
+      if (res.code === 200) {
+        const list = res.data?.records ?? res.data ?? [];
+        outboundBatchList.value = Array.isArray(list) ? list : [];
+        outboundBatchOptions.value = normalizeOutboundBatchOptions(list);
+      } else {
+        outboundBatchList.value = [];
+        outboundBatchOptions.value = [];
+      }
+    })
+    .catch(() => {
+      outboundBatchList.value = [];
+      outboundBatchOptions.value = [];
+    })
+    .finally(() => {
+      outboundBatchLoading.value = false;
+      if (keepSelected) {
+        syncInvoiceAmount();
+        checkTaxRateConsistency(false);
+      }
+    });
+};
+
+const handleCustomerChange = (customerId) => {
+  form.outboundBatchNos = [];
+  form.outboundBatches = "";
+  form.amount = 0;
+  loadOutboundBatches(customerId);
+};
+
+const form = reactive({
+  applyCode: "",
+  customerId: "",
+  outboundBatchNos: [],
+  outboundBatches: "",
+  amount: 0,
+  taxRate: 13,
+  invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+  applyDate: "",
+  content: "",
+  remark: "",
+});
+
+const rules = {
+  customerId: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
+  outboundBatchNos: [{ required: true, type: "array", min: 1, message: "璇烽�夋嫨鍑哄簱鍗曞彿", trigger: "change" }],
+  amount: [{ required: true, message: "璇疯緭鍏ュ紑绁ㄩ噾棰�", trigger: "blur" }],
+  taxRate: [{ required: true, message: "璇烽�夋嫨绋庣巼", trigger: "change" }],
+  invoiceType: [{ required: true, message: "璇烽�夋嫨鍙戠エ绫诲瀷", trigger: "change" }],
+  applyDate: [{ required: true, message: "璇烽�夋嫨鐢宠鏃ユ湡", trigger: "change" }],
+};
+
+/** 瀹℃牳鐘舵�侊細0寰呭鏍� 1瀹℃牳閫氳繃 2瀹℃牳涓嶉�氳繃 */
+const STATUS_LABEL_MAP = {
+  0: "寰呭鏍�",
+  1: "瀹℃牳閫氳繃",
+  2: "瀹℃牳涓嶉�氳繃",
+};
+
+const STATUS_TYPE_MAP = {
+  0: "warning",
+  1: "success",
+  2: "danger",
+};
+
+const normalizeStatus = (status) => {
+  if (status === undefined || status === null || status === "") return status;
+  const num = Number(status);
+  return Number.isNaN(num) ? status : num;
+};
+
+const isPendingStatus = (status) => normalizeStatus(status) === 0;
+const isApprovedStatus = (status) => normalizeStatus(status) === 1;
+
+const fileDialogVisible = ref(false);
+const currentRecordId = ref(0);
+
+const openFileDialog = (row) => {
+  currentRecordId.value = row.id;
+  fileDialogVisible.value = true;
+};
+
+const formatOutboundBatches = (value) => {
+  if (value === undefined || value === null || value === "") return "";
+  if (Array.isArray(value)) return value.filter(Boolean).join("銆�");
+  return String(value)
+    .split(/[,锛宂/)
+    .map((s) => s.trim())
+    .filter(Boolean)
+    .join("銆�");
+};
+
+const normalizeTableRow = (row) => ({
+  ...row,
+  applyCode: row.invoiceApplicationNo ?? row.applyCode,
+  amount: row.invoiceAmount ?? row.amount,
+  content: row.invoiceContent ?? row.content,
+  status: normalizeStatus(row.status ?? row.auditStatus),
+  stockOutRecordIds: row.stockOutRecordIds ?? row.stockOutRecordId ?? "",
+  outboundBatches: formatOutboundBatches(row.outboundBatches),
+});
+
+const appendFilterParams = (params) => {
+  if (filters.applyCode) {
+    params.invoiceApplicationNo = filters.applyCode;
+  }
+  if (filters.customerId) {
+    params.customerId = filters.customerId;
+  }
+  if (filters.status !== "" && filters.status != null) {
+    params.status = filters.status;
+  }
+  if (filters.dateRange?.length === 2) {
+    params.startDate = filters.dateRange[0];
+    params.endDate = filters.dateRange[1];
+  }
+  return params;
+};
+
+const buildListParams = () => {
+  return appendFilterParams({
+    current: pagination.currentPage,
+    size: pagination.pageSize,
+  });
+};
+
+const buildExportParams = () => {
+  const params = appendFilterParams({});
+  if (selectedRows.value.length > 0) {
+    params.ids = selectedRows.value.map((row) => row.id).join(",");
+  }
+  return params;
+};
+
+const handleExport = () => {
+  const params = buildExportParams();
+  const filename =
+    selectedRows.value.length > 0
+      ? `寮�绁ㄧ敵璇穇宸查��${selectedRows.value.length}鏉${Date.now()}.xlsx`
+      : `寮�绁ㄧ敵璇穇${Date.now()}.xlsx`;
+  proxy.download("/accountInvoiceApplication/exportAccountInvoiceApplication", params, filename);
+};
+
+const formatMoney = (value) => {
+  if (value === undefined || value === null) return "0.00";
+  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getStatusLabel = (status) => {
+  const num = normalizeStatus(status);
+  if (num === 0 || num === 1 || num === 2) {
+    return STATUS_LABEL_MAP[num];
+  }
+  return "-";
+};
+
+const getStatusType = (status) => {
+  const num = normalizeStatus(status);
+  if (num === 0 || num === 1 || num === 2) {
+    return STATUS_TYPE_MAP[num];
+  }
+  return "info";
+};
+
+const onSearch = () => {
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const getTableData = () => {
+  tableLoading.value = true;
+  listPageAccountInvoiceApplication(buildListParams())
+    .then((res) => {
+      const ok = res.code === 200 || res.code === 0;
+      if (ok && res.data) {
+        pagination.total = res.data.total ?? 0;
+        dataList.value = (res.data.records ?? []).map(normalizeTableRow);
+      } else {
+        ElMessage.error(res.msg || "鏌ヨ澶辫触");
+        dataList.value = [];
+        pagination.total = 0;
+      }
+    })
+    .catch(() => {
+      dataList.value = [];
+      pagination.total = 0;
+    })
+    .finally(() => {
+      tableLoading.value = false;
+    });
+};
+
+const resetFilters = () => {
+  filters.applyCode = "";
+  filters.customerId = "";
+  filters.status = "";
+  filters.dateRange = [];
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const changePage = ({ current, size }) => {
+  pagination.currentPage = current;
+  pagination.pageSize = size;
+  getTableData();
+};
+
+const handleSelectionChange = (selection) => {
+  selectedRows.value = selection;
+};
+
+const fillFormFromRow = (row) => {
+  const outboundBatchNos = Array.isArray(row.outboundBatchNos)
+    ? row.outboundBatchNos
+    : parseStockOutRecordIds(row.stockOutRecordIds ?? row.stockOutRecordId);
+  Object.assign(form, {
+    ...row,
+    applyCode: row.applyCode ?? row.invoiceApplicationNo ?? "",
+    amount: Number(row.amount ?? row.invoiceAmount ?? 0),
+    content: row.content ?? row.invoiceContent,
+    status: normalizeStatus(row.status ?? row.auditStatus),
+    outboundBatchNos,
+    outboundBatches: formatOutboundBatches(row.outboundBatches),
+  });
+};
+
+const add = () => {
+  isEdit.value = false;
+  isView.value = false;
+  dialogTitle.value = "鏂板寮�绁ㄧ敵璇�";
+  Object.assign(form, {
+    applyCode: "KP" + Date.now().toString().slice(-8),
+    customerId: "",
+    outboundBatchNos: [],
+    outboundBatches: "",
+    amount: 0,
+    taxRate: 13,
+    invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+    applyDate: new Date().toISOString().split("T")[0],
+    content: "",
+    remark: "",
+  });
+  outboundBatchList.value = [];
+  outboundBatchOptions.value = [];
+  dialogVisible.value = true;
+};
+
+const parseStockOutRecordIds = (value) => {
+  if (!value) return [];
+  if (Array.isArray(value)) return value;
+  return String(value)
+    .split(/[,锛宂/)
+    .map((s) => s.trim())
+    .filter(Boolean)
+    .map((s) => (/^\d+$/.test(s) ? Number(s) : s));
+};
+
+const buildSubmitPayload = (forUpdate = false) => {
+  const payload = {
+    customerId: form.customerId,
+    stockOutRecordIds: (form.outboundBatchNos || []).join(","),
+    invoiceApplicationNo: form.applyCode || "",
+    invoiceType: form.invoiceType,
+    applyDate: form.applyDate,
+    invoiceContent: form.content,
+    remark: form.remark || "",
+    invoiceAmount: form.amount,
+    taxRate: form.taxRate,
+    status: 0,
+  };
+  if (forUpdate) {
+    payload.id = currentId.value;
+  }
+  return payload;
+};
+
+const edit = (row) => {
+  isEdit.value = true;
+  isView.value = false;
+  currentId.value = row.id;
+  dialogTitle.value = "缂栬緫寮�绁ㄧ敵璇�";
+  fillFormFromRow(row);
+  dialogVisible.value = true;
+  loadOutboundBatches(form.customerId, true);
+};
+
+const view = (row) => {
+  isView.value = true;
+  isEdit.value = false;
+  dialogTitle.value = "鏌ョ湅寮�绁ㄧ敵璇�";
+  fillFormFromRow(row);
+  dialogVisible.value = true;
+};
+
+const submitAudit = (row, status) => {
+  auditAccountInvoiceApplication({ id: row.id, status })
+    .then((res) => {
+      if (res.code === 200) {
+        ElMessage.success(status === 1 ? "瀹℃牳閫氳繃" : "瀹℃牳涓嶉�氳繃");
+        getTableData();
+      } else {
+        ElMessage.error(res.msg || "瀹℃壒澶辫触");
+      }
+    })
+    .catch(() => {
+      ElMessage.error("瀹℃壒澶辫触");
+    });
+};
+
+const handleDelete = (row) => {
+  ElMessageBox.confirm(`纭鍒犻櫎鐢宠鍗曘��${row.applyCode ?? row.invoiceApplicationNo}銆嶅悧锛焋, "鎻愮ず", {
+    confirmButtonText: "纭畾",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(() => {
+    deleteAccountInvoiceApplication([row.id])
+      .then((res) => {
+        if (res.code === 200) {
+          ElMessage.success("鍒犻櫎鎴愬姛");
+          getTableData();
+        } else {
+          ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+        }
+      })
+      .catch(() => {
+        ElMessage.error("鍒犻櫎澶辫触");
+      });
+  });
+};
+
+const handleAudit = (row) => {
+  ElMessageBox.confirm("璇烽�夋嫨瀹℃壒缁撴灉", "寮�绁ㄧ敵璇峰鏍�", {
+    confirmButtonText: "瀹℃牳閫氳繃",
+    cancelButtonText: "瀹℃牳涓嶉�氳繃",
+    distinguishCancelAndClose: true,
+    type: "warning",
+  })
+    .then(() => {
+      submitAudit(row, 1);
+    })
+    .catch((action) => {
+      if (action === "cancel") {
+        submitAudit(row, 2);
+      }
+    });
+};
+
+const handleInvoice = (row) => {
+  ElMessageBox.confirm("纭宸插紑鍏峰彂绁紵", "鎻愮ず", {
+    confirmButtonText: "纭",
+    cancelButtonText: "鍙栨秷",
+    type: "info",
+  }).then(() => {
+    ElMessage.success("寮�绁ㄥ畬鎴�");
+    getTableData();
+  });
+};
+
+const handleBatchApply = () => {
+  ElMessage.success(`鎵归噺鐢宠 ${selectedRows.value.length} 鏉¤褰昤);
+};
+
+const submitLoading = ref(false);
+
+const submitForm = () => {
+  formRef.value.validate((valid) => {
+    if (!valid) return;
+    if (!checkTaxRateConsistency()) return;
+
+    submitLoading.value = true;
+    const request = isEdit.value
+      ? updateAccountInvoiceApplication(buildSubmitPayload(true))
+      : addAccountInvoiceApplication(buildSubmitPayload());
+
+    request
+      .then((res) => {
+        if (res.code === 200) {
+          ElMessage.success(isEdit.value ? "淇敼鎴愬姛" : "鏂板鎴愬姛");
+          closeDialog();
+          getTableData();
+        } else {
+          ElMessage.error(res.msg || (isEdit.value ? "淇敼澶辫触" : "鏂板澶辫触"));
+        }
+      })
+      .catch(() => {
+        ElMessage.error(isEdit.value ? "淇敼澶辫触" : "鏂板澶辫触");
+      })
+      .finally(() => {
+        submitLoading.value = false;
+      });
+  });
+};
+
+onMounted(() => {
+  getCustomerList();
+  getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 15px;
+}
+
+.text-primary {
+  color: #409eff;
+  font-weight: bold;
+}
+
+.outbound-batch-input:not(.is-disabled) {
+  :deep(.el-input__wrapper) {
+    cursor: pointer;
+  }
+}
+</style>
diff --git a/src/views/financialManagement/receivable/outputInvoice.vue b/src/views/financialManagement/receivable/outputInvoice.vue
new file mode 100644
index 0000000..d746aea
--- /dev/null
+++ b/src/views/financialManagement/receivable/outputInvoice.vue
@@ -0,0 +1,608 @@
+<template>
+  <div class="app-container">
+    <el-form :model="filters" :inline="true">
+      <el-form-item label="鍙戠エ鍙风爜:">
+        <el-input v-model="filters.invoiceNumber" placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�" clearable style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="瀹㈡埛:">
+        <el-select v-model="filters.customerId" placeholder="璇烽�夋嫨瀹㈡埛" clearable style="width: 200px;">
+          <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="寮�绁ㄦ棩鏈�:">
+        <el-date-picker
+          v-model="filters.dateRange"
+          type="daterange"
+          value-format="YYYY-MM-DD"
+          format="YYYY-MM-DD"
+          range-separator="鑷�"
+          start-placeholder="寮�濮嬫棩鏈�"
+          end-placeholder="缁撴潫鏃ユ湡"
+          clearable
+          style="width: 240px;"
+        />
+      </el-form-item>
+      <el-form-item label="鐘舵��:">
+        <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+          <el-option label="姝e父" :value="0" />
+          <el-option label="浣滃簾" :value="1" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div></div>
+        <div>
+          <!-- <el-button type="primary" @click="add" icon="Plus">褰曞叆鍙戠エ</el-button> -->
+          <el-button type="success" @click="handleExport" icon="Download">瀵煎嚭</el-button>
+        </div>
+      </div>
+      <PIMTable
+        rowKey="id"
+        v-loading="tableLoading"
+        :column="columns"
+        :tableData="dataList"
+        :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+        @pagination="changePage"
+      >
+        <template #amount="{ row }">
+          <span class="text-primary">楼{{ formatMoney(row.amount) }}</span>
+        </template>
+        <template #taxAmount="{ row }">
+          <span class="text-danger">楼{{ formatMoney(row.taxAmount) }}</span>
+        </template>
+        <template #totalAmount="{ row }">
+          <span class="text-success">楼{{ formatMoney(row.totalAmount) }}</span>
+        </template>
+        <template #status="{ row }">
+          <el-tag :type="getStatusType(row.status)" effect="light" round>
+            {{ getStatusLabel(row.status) }}
+          </el-tag>
+        </template>
+        <template #operation="{ row }">
+          <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+          <el-button
+            type="primary"
+            link
+            @click="openFileDialog(row)"
+            v-if="row.accountInvoiceApplicationId"
+          >
+            闄勪欢
+          </el-button>
+          <el-button type="warning" link @click="handleCancel(row)" v-if="isNormalStatus(row.status)">浣滃簾</el-button>
+          <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+        </template>
+      </PIMTable>
+    </div>
+
+    <FormDialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="800px"
+      :operation-type="isView ? 'detail' : ''"
+      @confirm="submitForm"
+      @cancel="closeDialog"
+    >
+      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+        <el-row v-if="isView" :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鐘舵��">
+              <el-tag :type="getStatusType(form.status)" effect="light" round>
+                {{ getStatusLabel(form.status) }}
+              </el-tag>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鍙戠エ鍙风爜" prop="invoiceNo">
+              <el-input v-model="form.invoiceNo" placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�" :disabled="isView" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="瀹㈡埛" prop="customerId">
+              <el-select v-model="form.customerId" placeholder="璇烽�夋嫨瀹㈡埛" style="width: 100%;" :disabled="isView">
+                <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="寮�绁ㄦ棩鏈�" prop="invoiceDate">
+              <el-date-picker
+                v-model="form.invoiceDate"
+                type="date"
+                placeholder="閫夋嫨鏃ユ湡"
+                value-format="YYYY-MM-DD"
+                style="width: 100%;"
+                :disabled="isView"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鍙戠エ绫诲瀷" prop="invoiceType">
+              <el-select
+                v-model="form.invoiceType"
+                placeholder="璇烽�夋嫨鍙戠エ绫诲瀷"
+                style="width: 100%;"
+                :disabled="isView"
+                @change="handleInvoiceTypeChange"
+              >
+                <el-option label="澧炲�肩◣涓撶敤鍙戠エ" value="澧炲�肩◣涓撶敤鍙戠エ" />
+                <el-option label="澧炲�肩◣鏅�氬彂绁�" value="澧炲�肩◣鏅�氬彂绁�" />
+                <el-option label="鐢靛瓙鍙戠エ" value="鐢靛瓙鍙戠エ" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="绋庣巼" prop="taxRate">
+              <el-select
+                v-model="form.taxRate"
+                placeholder="璇烽�夋嫨绋庣巼"
+                style="width: 100%;"
+                :disabled="isView"
+                @change="calculateTax"
+              >
+                <el-option
+                  v-for="dict in tax_rate"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="Number(dict.value)"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="閲戦(涓嶅惈绋�)" prop="amount">
+              <el-input-number
+                v-model="form.amount"
+                :min="0"
+                :precision="2"
+                style="width: 100%;"
+                :disabled="isView"
+                @change="calculateTax"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="绋庨">
+              <el-input v-model="form.taxAmount" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="浠风◣鍚堣">
+              <el-input v-model="form.totalAmount" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="鍙戠エ鍐呭" prop="content">
+          <el-input v-model="form.content" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ彂绁ㄥ唴瀹�" :disabled="isView" />
+        </el-form-item>
+        <el-form-item label="澶囨敞" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" :disabled="isView" />
+        </el-form-item>
+      </el-form>
+      <template v-if="!isView" #footer>
+        <el-button type="primary" :loading="submitLoading" @click="submitForm">纭畾</el-button>
+        <el-button @click="closeDialog">鍙栨秷</el-button>
+      </template>
+    </FormDialog>
+
+    <FileList
+      v-if="fileDialogVisible"
+      v-model:visible="fileDialogVisible"
+      record-type="account_invoice_application"
+      :record-id="currentRecordId"
+      :editable="false"
+    />
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance, defineAsyncComponent } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { listCustomer } from "@/api/basicData/customer.js";
+import {
+  addAccountSalesInvoice,
+  listPageAccountSalesInvoice,
+  cancelAccountSalesInvoice,
+  deleteAccountSalesInvoice,
+} from "@/api/financialManagement/accountSalesInvoice.js";
+
+const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
+
+defineOptions({
+  name: "閿�椤瑰彂绁�",
+});
+
+const { proxy } = getCurrentInstance();
+const { tax_rate } = proxy.useDict("tax_rate");
+
+const filters = reactive({
+  invoiceNumber: "",
+  customerId: "",
+  dateRange: [],
+  status: "",
+});
+
+const pagination = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+const columns = [
+  { label: "鍙戠エ鍙风爜", prop: "invoiceNo", width: "140" },
+  { label: "瀹㈡埛鍚嶇О", prop: "customerName", width: "180" },
+  { label: "寮�绁ㄦ棩鏈�", prop: "invoiceDate", width: "120" },
+  { label: "閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+  { label: "绋庨", prop: "taxAmount", dataType: "slot", slot: "taxAmount" },
+  { label: "浠风◣鍚堣", prop: "totalAmount", dataType: "slot", slot: "totalAmount" },
+  { label: "鍙戠エ绫诲瀷", prop: "invoiceType", width: "130" },
+  { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status", width: "90", align: "center" },
+  { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "260", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isView = ref(false);
+const submitLoading = ref(false);
+
+const customerList = ref([]);
+const fileDialogVisible = ref(false);
+const currentRecordId = ref(0);
+
+const openFileDialog = (row) => {
+  if (!row.accountInvoiceApplicationId) {
+    ElMessage.warning("鏈叧鑱斿紑绁ㄧ敵璇凤紝鏃犳硶鏌ョ湅闄勪欢");
+    return;
+  }
+  currentRecordId.value = row.accountInvoiceApplicationId;
+  fileDialogVisible.value = true;
+};
+
+/** 鐘舵�侊細0姝e父 1浣滃簾 */
+const STATUS_LABEL_MAP = { 0: "姝e父", 1: "浣滃簾" };
+const STATUS_TYPE_MAP = { 0: "success", 1: "info" };
+
+const normalizeStatus = (status) => {
+  if (status === undefined || status === null || status === "") return 0;
+  const num = Number(status);
+  return Number.isNaN(num) ? 0 : num;
+};
+
+const isNormalStatus = (status) => normalizeStatus(status) === 0;
+
+const getStatusLabel = (status) => {
+  const num = normalizeStatus(status);
+  return STATUS_LABEL_MAP[num] ?? "姝e父";
+};
+
+const getStatusType = (status) => {
+  const num = normalizeStatus(status);
+  return STATUS_TYPE_MAP[num] ?? "success";
+};
+
+const form = reactive({
+  invoiceNo: "",
+  customerId: "",
+  invoiceDate: "",
+  invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+  taxRate: 13,
+  amount: 0,
+  taxAmount: 0,
+  totalAmount: 0,
+  content: "",
+  remark: "",
+  accountInvoiceApplicationId: undefined,
+  storageAttachmentId: undefined,
+});
+
+const rules = {
+  invoiceNo: [{ required: true, message: "璇疯緭鍏ュ彂绁ㄥ彿鐮�", trigger: "blur" }],
+  customerId: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
+  invoiceDate: [{ required: true, message: "璇烽�夋嫨寮�绁ㄦ棩鏈�", trigger: "change" }],
+  invoiceType: [{ required: true, message: "璇烽�夋嫨鍙戠エ绫诲瀷", trigger: "change" }],
+  taxRate: [{ required: true, message: "璇烽�夋嫨绋庣巼", trigger: "change" }],
+  amount: [{ required: true, message: "璇疯緭鍏ラ噾棰�", trigger: "blur" }],
+};
+
+const formatMoney = (value) => {
+  if (value === undefined || value === null) return "0.00";
+  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const calculateTax = () => {
+  form.taxAmount = Number((form.amount * form.taxRate / 100).toFixed(2));
+  form.totalAmount = Number((form.amount + form.taxAmount).toFixed(2));
+};
+
+const handleInvoiceTypeChange = () => {
+  calculateTax();
+};
+
+const normalizeTableRow = (row) => ({
+  ...row,
+  invoiceNo: row.invoiceNumber ?? row.invoiceNo,
+  invoiceDate: row.issueDate ?? row.invoiceDate,
+  amount: row.taxExclusivelPrice ?? row.amount,
+  taxAmount: row.taxPrice ?? row.taxAmount,
+  totalAmount: row.taxInclusivePrice ?? row.totalAmount,
+  content: row.invoiceContent ?? row.content,
+  status: normalizeStatus(row.status),
+});
+
+const fillFormFromRow = (row) => {
+  Object.assign(form, {
+    invoiceNo: row.invoiceNo ?? row.invoiceNumber ?? "",
+    customerId: row.customerId,
+    invoiceDate: row.invoiceDate ?? row.issueDate ?? "",
+    invoiceType: row.invoiceType ?? "澧炲�肩◣涓撶敤鍙戠エ",
+    taxRate: row.taxRate ?? 13,
+    amount: row.amount ?? row.taxExclusivelPrice ?? 0,
+    taxAmount: row.taxAmount ?? row.taxPrice ?? 0,
+    totalAmount: row.totalAmount ?? row.taxInclusivePrice ?? 0,
+    content: row.content ?? row.invoiceContent ?? "",
+    remark: row.remark ?? "",
+    accountInvoiceApplicationId: row.accountInvoiceApplicationId,
+    storageAttachmentId: row.storageAttachmentId,
+    status: normalizeStatus(row.status),
+  });
+};
+
+const buildCancelPayload = (row) => ({
+  id: row.id,
+  accountInvoiceApplicationId: row.accountInvoiceApplicationId,
+  invoiceNumber: row.invoiceNumber ?? row.invoiceNo,
+  taxRate: row.taxRate,
+  invoiceType: row.invoiceType,
+  issueDate: row.issueDate ?? row.invoiceDate,
+  taxExclusivelPrice: row.taxExclusivelPrice ?? row.amount,
+  taxPrice: row.taxPrice ?? row.taxAmount,
+  taxInclusivePrice: row.taxInclusivePrice ?? row.totalAmount,
+  remark: row.remark ?? "",
+  invoiceContent: row.invoiceContent ?? row.content,
+  customerId: row.customerId,
+  storageAttachmentId: row.storageAttachmentId,
+  status: 1,
+});
+
+const buildSubmitPayload = () => ({
+  invoiceNumber: form.invoiceNo,
+  customerId: form.customerId,
+  issueDate: form.invoiceDate,
+  invoiceType: form.invoiceType,
+  taxRate: form.taxRate,
+  taxExclusivelPrice: form.amount,
+  taxPrice: form.taxAmount,
+  taxInclusivePrice: form.totalAmount,
+  invoiceContent: form.content,
+  remark: form.remark || "",
+  accountInvoiceApplicationId: form.accountInvoiceApplicationId,
+  storageAttachmentId: form.storageAttachmentId,
+});
+
+const getCustomerList = () => {
+  listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
+    if (res.code === 200) {
+      customerList.value = res.data?.records || [];
+    }
+  });
+};
+
+const appendFilterParams = (params) => {
+  if (filters.invoiceNumber) {
+    params.invoiceNumber = filters.invoiceNumber;
+  }
+  if (filters.customerId) {
+    params.customerId = filters.customerId;
+  }
+  if (filters.dateRange?.length === 2) {
+    params.startDate = filters.dateRange[0];
+    params.endDate = filters.dateRange[1];
+  }
+  if (filters.status !== "" && filters.status != null) {
+    params.status = filters.status;
+  }
+  return params;
+};
+
+const buildListParams = () =>
+  appendFilterParams({
+    current: pagination.currentPage,
+    size: pagination.pageSize,
+  });
+
+const buildExportParams = () => appendFilterParams({});
+
+const handleExport = () => {
+  const params = buildExportParams();
+  proxy.download("/accountSalesInvoice/exportAccountSalesInvoice", params, `閿�椤瑰彂绁╛${Date.now()}.xlsx`);
+};
+
+const getTableData = () => {
+  tableLoading.value = true;
+  listPageAccountSalesInvoice(buildListParams())
+    .then((res) => {
+      if (res.code === 200) {
+        const records = res.data?.records ?? [];
+        dataList.value = records.map(normalizeTableRow);
+        pagination.total = res.data?.total ?? 0;
+      } else {
+        dataList.value = [];
+        pagination.total = 0;
+        ElMessage.error(res.msg || "鏌ヨ澶辫触");
+      }
+    })
+    .catch(() => {
+      dataList.value = [];
+      pagination.total = 0;
+      ElMessage.error("鏌ヨ澶辫触");
+    })
+    .finally(() => {
+      tableLoading.value = false;
+    });
+};
+
+const onSearch = () => {
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const resetFilters = () => {
+  filters.invoiceNumber = "";
+  filters.customerId = "";
+  filters.dateRange = [];
+  filters.status = "";
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const changePage = ({ current, size }) => {
+  pagination.currentPage = current;
+  pagination.pageSize = size;
+  getTableData();
+};
+
+const closeDialog = () => {
+  dialogVisible.value = false;
+  isView.value = false;
+};
+
+const add = () => {
+  isView.value = false;
+  dialogTitle.value = "褰曞叆鍙戠エ";
+  Object.assign(form, {
+    invoiceNo: "",
+    customerId: "",
+    invoiceDate: new Date().toISOString().split("T")[0],
+    invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+    taxRate: 13,
+    amount: 0,
+    taxAmount: 0,
+    totalAmount: 0,
+    content: "",
+    remark: "",
+    accountInvoiceApplicationId: undefined,
+    storageAttachmentId: undefined,
+  });
+  dialogVisible.value = true;
+};
+
+const view = (row) => {
+  isView.value = true;
+  dialogTitle.value = "鏌ョ湅鍙戠エ";
+  fillFormFromRow(row);
+  dialogVisible.value = true;
+};
+
+const handleCancel = (row) => {
+  ElMessageBox.confirm(`纭浣滃簾鍙戠エ銆�${row.invoiceNo ?? row.invoiceNumber}銆嶅悧锛焋, "浣滃簾纭", {
+    confirmButtonText: "纭畾",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(() => {
+    cancelAccountSalesInvoice(buildCancelPayload(row))
+      .then((res) => {
+        if (res.code === 200) {
+          ElMessage.success("浣滃簾鎴愬姛");
+          getTableData();
+        } else {
+          ElMessage.error(res.msg || "浣滃簾澶辫触");
+        }
+      })
+      .catch(() => {
+        ElMessage.error("浣滃簾澶辫触");
+      });
+  });
+};
+
+const handleDelete = (row) => {
+  ElMessageBox.confirm(`纭鍒犻櫎鍙戠エ銆�${row.invoiceNo ?? row.invoiceNumber}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙, "鍒犻櫎纭", {
+    confirmButtonText: "纭畾",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(() => {
+    deleteAccountSalesInvoice([row.id])
+      .then((res) => {
+        if (res.code === 200) {
+          ElMessage.success("鍒犻櫎鎴愬姛");
+          getTableData();
+        } else {
+          ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+        }
+      })
+      .catch(() => {
+        ElMessage.error("鍒犻櫎澶辫触");
+      });
+  });
+};
+
+const submitForm = () => {
+  formRef.value.validate((valid) => {
+    if (!valid) return;
+    submitLoading.value = true;
+    addAccountSalesInvoice(buildSubmitPayload())
+      .then((res) => {
+        if (res.code === 200) {
+          ElMessage.success("褰曞叆鎴愬姛");
+          closeDialog();
+          getTableData();
+        } else {
+          ElMessage.error(res.msg || "褰曞叆澶辫触");
+        }
+      })
+      .catch(() => {
+        ElMessage.error("褰曞叆澶辫触");
+      })
+      .finally(() => {
+        submitLoading.value = false;
+      });
+  });
+};
+
+onMounted(() => {
+  getCustomerList();
+  getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 15px;
+}
+
+.text-primary {
+  color: #409eff;
+  font-weight: bold;
+}
+
+.text-danger {
+  color: #f56c6c;
+  font-weight: bold;
+}
+
+.text-success {
+  color: #67c23a;
+  font-weight: bold;
+}
+</style>
diff --git a/src/views/financialManagement/receivable/receipt.vue b/src/views/financialManagement/receivable/receipt.vue
new file mode 100644
index 0000000..6ddb3fe
--- /dev/null
+++ b/src/views/financialManagement/receivable/receipt.vue
@@ -0,0 +1,855 @@
+<template>
+  <div class="app-container">
+    <el-form :model="filters"
+             :inline="true">
+      <el-form-item label="鏀舵鍗曞彿:">
+        <el-input v-model="filters.collectionNumber"
+                  placeholder="璇疯緭鍏ユ敹娆惧崟鍙�"
+                  clearable
+                  style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="瀹㈡埛:">
+        <el-select v-model="filters.customerId"
+                   placeholder="璇烽�夋嫨瀹㈡埛"
+                   clearable
+                   filterable
+                   style="width: 200px;">
+          <el-option v-for="item in customerList"
+                     :key="item.id"
+                     :label="item.customerName"
+                     :value="item.id" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="鏀舵鏂瑰紡:">
+        <el-select v-model="filters.collectionMethod"
+                   placeholder="璇烽�夋嫨鏀舵鏂瑰紡"
+                   clearable
+                   style="width: 150px;">
+          <el-option v-for="item in payment_methods"
+                     :key="item.value"
+                     :label="item.label"
+                     :value="item.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="鏀舵鏃ユ湡:">
+        <el-date-picker v-model="filters.dateRange"
+                        type="daterange"
+                        value-format="YYYY-MM-DD"
+                        format="YYYY-MM-DD"
+                        range-separator="鑷�"
+                        start-placeholder="寮�濮嬫棩鏈�"
+                        end-placeholder="缁撴潫鏃ユ湡"
+                        clearable
+                        style="width: 240px;" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary"
+                   @click="onSearch">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div>
+          <el-statistic title="鏈〉鏀舵鍚堣"
+                        :value="totalReceiptAmount"
+                        :precision="2"
+                        prefix="楼" />
+        </div>
+        <div>
+          <el-button type="primary"
+                     @click="add"
+                     icon="Plus">鏂板鏀舵</el-button>
+          <el-button type="success"
+                     @click="handleExport"
+                     icon="Download">瀵煎嚭</el-button>
+        </div>
+      </div>
+      <PIMTable rowKey="id"
+                v-loading="tableLoading"
+                :column="columns"
+                :tableData="dataList"
+                :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+                @pagination="changePage">
+        <template #amount="{ row }">
+          <span class="text-success">楼{{ formatMoney(row.amount) }}</span>
+        </template>
+        <template #receiptMethod="{ row }">
+          <span>{{ getReceiptMethodLabel(row.receiptMethod) }}</span>
+        </template>
+        <template #operation="{ row }">
+          <el-button type="primary"
+                     link
+                     @click="view(row)">鏌ョ湅</el-button>
+          <el-button :disabled="row.accountStatemen"
+                     type="primary"
+                     link
+                     @click="edit(row)">缂栬緫</el-button>
+          <el-button :disabled="row.accountStatemen"
+                     type="danger"
+                     link
+                     @click="handleDelete(row)">鍒犻櫎</el-button>
+        </template>
+      </PIMTable>
+    </div>
+    <FormDialog :title="dialogTitle"
+                v-model="dialogVisible"
+                width="800px"
+                :operation-type="isView ? 'detail' : ''"
+                @confirm="submitForm"
+                @cancel="closeDialog">
+      <el-form :model="form"
+               :rules="rules"
+               ref="formRef"
+               label-width="120px">
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="鏀舵鍗曞彿"
+                          prop="receiptCode">
+              <el-input v-model="form.receiptCode"
+                        placeholder="绯荤粺鑷姩鐢熸垚"
+                        disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="瀹㈡埛"
+                          prop="customerId">
+              <el-select v-model="form.customerId"
+                         placeholder="璇烽�夋嫨瀹㈡埛"
+                         style="width: 100%;"
+                         :disabled="isEdit || isView"
+                         filterable
+                         @change="handleCustomerChange">
+                <el-option v-for="item in customerList"
+                           :key="item.id"
+                           :label="item.customerName"
+                           :value="item.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鍏宠仈鍗曟嵁"
+                          prop="stockOutRecordIds">
+              <el-input :model-value="outboundBatchDisplayText"
+                        placeholder="璇峰厛閫夋嫨瀹㈡埛"
+                        readonly
+                        :disabled="!form.customerId || isEdit || isView"
+                        class="outbound-batch-input"
+                        @click="handleOutboundInputClick">
+                <template v-if="!isEdit && !isView"
+                          #append>
+                  <el-button :disabled="!form.customerId"
+                             :loading="outboundBatchLoading"
+                             @click.stop="openOutboundSelectDialog">
+                    閫夋嫨
+                  </el-button>
+                </template>
+              </el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鏀舵鏃ユ湡"
+                          prop="receiptDate">
+              <el-date-picker v-model="form.receiptDate"
+                              type="date"
+                              placeholder="閫夋嫨鏃ユ湡"
+                              value-format="YYYY-MM-DD"
+                              style="width: 100%;"
+                              :disabled="isView" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鏀舵閲戦"
+                          prop="amount">
+              <el-input-number v-model="form.amount"
+                               :min="0"
+                               :precision="2"
+                               style="width: 100%;"
+                               :disabled="isView"
+                               placeholder="鏍规嵁鍏宠仈鍗曟嵁鑷姩姹囨�伙紝鍙慨鏀�" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鏀舵鏂瑰紡"
+                          prop="receiptMethod">
+              <el-select v-model="form.receiptMethod"
+                         placeholder="璇烽�夋嫨鏀舵鏂瑰紡"
+                         style="width: 100%;"
+                         :disabled="isView">
+                <el-option v-for="item in payment_methods"
+                           :key="item.value"
+                           :label="item.label"
+                           :value="item.value" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="澶囨敞"
+                      prop="remark">
+          <el-input v-model="form.remark"
+                    type="textarea"
+                    :rows="3"
+                    placeholder="璇疯緭鍏ュ娉�"
+                    :disabled="isView" />
+        </el-form-item>
+      </el-form>
+      <template v-if="!isView"
+                #footer>
+        <el-button type="primary"
+                   :loading="submitLoading"
+                   @click="submitForm">纭畾</el-button>
+        <el-button @click="closeDialog">鍙栨秷</el-button>
+      </template>
+    </FormDialog>
+    <el-dialog v-model="outboundSelectVisible"
+               title="閫夋嫨鍏宠仈鍗曟嵁"
+               width="1200px"
+               append-to-body
+               destroy-on-close
+               :close-on-click-modal="false"
+               @closed="handleOutboundDialogClosed">
+      <el-table ref="outboundTableRef"
+                v-loading="outboundBatchLoading"
+                :data="outboundBatchList"
+                row-key="id"
+                border
+                stripe
+                max-height="480"
+                @selection-change="handleOutboundDialogSelectionChange">
+        <el-table-column type="selection"
+                         width="55"
+                         align="center" />
+        <el-table-column prop="outboundBatches"
+                         label="鍑哄簱鍗曞彿"
+                         min-width="140"
+                         show-overflow-tooltip />
+        <el-table-column prop="customerName"
+                         label="瀹㈡埛鍚嶇О"
+                         min-width="120"
+                         show-overflow-tooltip />
+        <el-table-column prop="productName"
+                         label="浜у搧鍚嶇О"
+                         min-width="120"
+                         show-overflow-tooltip />
+        <el-table-column prop="specificationModel"
+                         label="瑙勬牸鍨嬪彿"
+                         min-width="140"
+                         show-overflow-tooltip />
+        <el-table-column prop="salesContractNo"
+                         label="閿�鍞悎鍚屽彿"
+                         min-width="140"
+                         show-overflow-tooltip />
+        <el-table-column prop="shippingNo"
+                         label="鍙戣揣鍗曞彿"
+                         min-width="130"
+                         show-overflow-tooltip />
+        <el-table-column prop="shippingDate"
+                         label="鍙戣揣鏃ユ湡"
+                         width="110"
+                         align="center" />
+        <el-table-column prop="outboundAmount"
+                         label="鍑哄簱閲戦"
+                         width="110"
+                         align="right">
+          <template #default="{ row }">楼{{ formatMoney(row.outboundAmount) }}</template>
+        </el-table-column>
+        <el-table-column prop="taxRate"
+                         label="绋庣巼"
+                         width="80"
+                         align="center">
+          <template #default="{ row }">{{ row.taxRate }}%</template>
+        </el-table-column>
+      </el-table>
+      <template #footer>
+        <el-button type="primary"
+                   @click="confirmOutboundSelection">纭畾</el-button>
+        <el-button @click="outboundSelectVisible = false">鍙栨秷</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+  import {
+    ref,
+    reactive,
+    computed,
+    onMounted,
+    nextTick,
+    getCurrentInstance,
+  } from "vue";
+  import { ElMessage, ElMessageBox } from "element-plus";
+  import FormDialog from "@/components/Dialog/FormDialog.vue";
+  import { listCustomer } from "@/api/basicData/customer.js";
+  import {
+    getOutboundBatchesByCustomer,
+    addAccountSalesCollection,
+    listPageAccountSalesCollection,
+    updateAccountSalesCollection,
+    deleteAccountSalesCollection,
+  } from "@/api/financialManagement/accountSalesCollection.js";
+
+  defineOptions({
+    name: "鏀舵鍗�",
+  });
+
+  const { proxy } = getCurrentInstance();
+  const { payment_methods } = proxy.useDict("payment_methods");
+
+  const filters = reactive({
+    collectionNumber: "",
+    customerId: "",
+    collectionMethod: "",
+    dateRange: [],
+  });
+
+  const pagination = reactive({
+    currentPage: 1,
+    pageSize: 10,
+    total: 0,
+  });
+
+  const columns = [
+    { label: "鏀舵鍗曞彿", prop: "receiptCode", width: "150" },
+    { label: "瀹㈡埛鍚嶇О", prop: "customerName", width: "180" },
+    { label: "鏀舵鏃ユ湡", prop: "receiptDate", width: "120" },
+    { label: "鏀舵閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+    {
+      label: "鏀舵鏂瑰紡",
+      prop: "receiptMethod",
+      dataType: "slot",
+      slot: "receiptMethod",
+      width: "120",
+    },
+    { label: "澶囨敞", prop: "remark", showOverflowTooltip: true },
+    {
+      label: "鎿嶄綔",
+      prop: "operation",
+      dataType: "slot",
+      slot: "operation",
+      width: "200",
+      fixed: "right",
+    },
+  ];
+
+  const dataList = ref([]);
+  const tableLoading = ref(false);
+  const dialogVisible = ref(false);
+  const dialogTitle = ref("");
+  const formRef = ref(null);
+  const isEdit = ref(false);
+  const isView = ref(false);
+  const currentId = ref(null);
+  const submitLoading = ref(false);
+
+  const customerList = ref([]);
+  const outboundBatchList = ref([]);
+  const outboundBatchOptions = ref([]);
+  const outboundBatchLoading = ref(false);
+  const outboundSelectVisible = ref(false);
+  const outboundTableRef = ref(null);
+  const dialogOutboundSelection = ref([]);
+
+  const getReceiptMethodLabel = value => {
+    if (value === undefined || value === null || value === "") return "-";
+    const item = payment_methods.value?.find(
+      m => String(m.value) === String(value)
+    );
+    return item?.label ?? value;
+  };
+
+  const getDefaultReceiptMethod = () => payment_methods.value?.[0]?.value ?? "";
+
+  const form = reactive({
+    receiptCode: "",
+    customerId: "",
+    receiptDate: "",
+    amount: 0,
+    receiptMethod: "",
+    stockOutRecordIds: [],
+    outboundBatches: "",
+    remark: "",
+  });
+
+  const rules = {
+    customerId: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
+    stockOutRecordIds: [
+      {
+        required: true,
+        type: "array",
+        min: 1,
+        message: "璇烽�夋嫨鍏宠仈鍗曟嵁",
+        trigger: "change",
+      },
+    ],
+    receiptDate: [
+      { required: true, message: "璇烽�夋嫨鏀舵鏃ユ湡", trigger: "change" },
+    ],
+    amount: [{ required: true, message: "璇疯緭鍏ユ敹娆鹃噾棰�", trigger: "blur" }],
+    receiptMethod: [
+      { required: true, message: "璇烽�夋嫨鏀舵鏂瑰紡", trigger: "change" },
+    ],
+  };
+
+  const totalReceiptAmount = computed(() =>
+    dataList.value.reduce((sum, item) => sum + Number(item.amount || 0), 0)
+  );
+
+  const formatMoney = value => {
+    if (value === undefined || value === null) return "0.00";
+    return Number(value)
+      .toFixed(2)
+      .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+  };
+
+  const parseStockOutRecordIds = value => {
+    if (!value) return [];
+    if (Array.isArray(value)) return value;
+    return String(value)
+      .split(/[,锛宂/)
+      .map(s => s.trim())
+      .filter(Boolean)
+      .map(s => (/^\d+$/.test(s) ? Number(s) : s));
+  };
+
+  const formatOutboundBatches = value => {
+    if (value === undefined || value === null || value === "") return "";
+    if (Array.isArray(value)) return value.filter(Boolean).join("銆�");
+    return String(value)
+      .split(/[,锛宂/)
+      .map(s => s.trim())
+      .filter(Boolean)
+      .join("銆�");
+  };
+
+  const normalizeTableRow = row => ({
+    ...row,
+    receiptCode: row.collectionNumber ?? row.receiptCode,
+    receiptDate: row.collectionDate ?? row.receiptDate,
+    amount: row.collectionAmount ?? row.amount,
+    receiptMethod: row.collectionMethod ?? row.receiptMethod ?? "",
+    stockOutRecordIds: row.stockOutRecordIds ?? row.stockOutRecordId ?? "",
+    outboundBatches: formatOutboundBatches(row.outboundBatches),
+  });
+
+  const getCustomerList = () => {
+    listCustomer({ current: -1, size: -1, type: 0 }).then(res => {
+      if (res.code === 200) {
+        customerList.value = res.data?.records || [];
+      }
+    });
+  };
+
+  const normalizeOutboundBatchOptions = data => {
+    const list = Array.isArray(data) ? data : [];
+    return list.map((item, index) => {
+      if (typeof item === "string" || typeof item === "number") {
+        const text = String(item);
+        return { label: text, value: text, outboundAmount: 0 };
+      }
+      const label =
+        item.outboundBatches ??
+        item.batchNo ??
+        item.shippingNo ??
+        item.outboundNo ??
+        item.label ??
+        `鍑哄簱鍗�${index + 1}`;
+      const value = item.id ?? item.stockOutRecordId ?? label;
+      return {
+        label: String(label),
+        value,
+        outboundAmount: Number(item.outboundAmount) || 0,
+      };
+    });
+  };
+
+  const isSameOutboundId = (a, b) => String(a) === String(b);
+
+  const getOutboundRowId = row => row?.id ?? row?.stockOutRecordId;
+
+  const outboundBatchDisplayText = computed(() => {
+    if (isEdit.value || isView.value) {
+      return form.outboundBatches || "";
+    }
+    if (form.outboundBatches) return form.outboundBatches;
+    const ids = form.stockOutRecordIds || [];
+    if (!ids.length) return "";
+    const labels = outboundBatchOptions.value
+      .filter(opt => ids.some(id => isSameOutboundId(id, opt.value)))
+      .map(opt => opt.label);
+    if (labels.length) return labels.join("銆�");
+    return ids.join("銆�");
+  });
+
+  const handleOutboundInputClick = () => {
+    if (isEdit.value || isView.value) return;
+    openOutboundSelectDialog();
+  };
+
+  /** 涓哄凡閫� ID 琛ュ叏閫夐」锛堢紪杈�/鏌ョ湅鍥炴樉锛� */
+  const ensureOutboundOptionsForSelected = () => {
+    const ids = form.stockOutRecordIds || [];
+    ids.forEach(id => {
+      const exists = outboundBatchOptions.value.some(opt =>
+        isSameOutboundId(opt.value, id)
+      );
+      if (exists) return;
+      const fromList = outboundBatchList.value.find(row =>
+        isSameOutboundId(getOutboundRowId(row), id)
+      );
+      if (fromList) {
+        const [option] = normalizeOutboundBatchOptions([fromList]);
+        if (option) outboundBatchOptions.value.push(option);
+        return;
+      }
+      outboundBatchOptions.value.push({
+        label: String(id),
+        value: id,
+        outboundAmount: 0,
+      });
+    });
+  };
+
+  const syncCollectionAmount = () => {
+    const selected = form.stockOutRecordIds || [];
+    const sum = outboundBatchOptions.value
+      .filter(opt => selected.some(id => isSameOutboundId(id, opt.value)))
+      .reduce((acc, opt) => acc + (Number(opt.outboundAmount) || 0), 0);
+    form.amount = sum > 0 ? Number(sum.toFixed(2)) : 0;
+  };
+
+  const restoreOutboundTableSelection = () => {
+    nextTick(() => {
+      const table = outboundTableRef.value;
+      if (!table) return;
+      table.clearSelection();
+      const selectedIds = new Set(
+        (form.stockOutRecordIds || []).map(id => String(id))
+      );
+      outboundBatchList.value.forEach(row => {
+        const rowId = getOutboundRowId(row);
+        if (
+          rowId !== undefined &&
+          rowId !== null &&
+          selectedIds.has(String(rowId))
+        ) {
+          table.toggleRowSelection(row, true);
+        }
+      });
+    });
+  };
+
+  const loadOutboundBatches = (customerId, keepSelected = false) => {
+    if (!customerId) {
+      outboundBatchList.value = [];
+      outboundBatchOptions.value = [];
+      if (!keepSelected) {
+        form.stockOutRecordIds = [];
+        form.amount = 0;
+      }
+      return Promise.resolve();
+    }
+    outboundBatchLoading.value = true;
+    return getOutboundBatchesByCustomer({ customerId })
+      .then(res => {
+        if (res.code === 200) {
+          const list = res.data?.records ?? res.data ?? [];
+          outboundBatchList.value = Array.isArray(list) ? list : [];
+          outboundBatchOptions.value = normalizeOutboundBatchOptions(list);
+        } else {
+          outboundBatchList.value = [];
+          outboundBatchOptions.value = [];
+        }
+      })
+      .catch(() => {
+        outboundBatchList.value = [];
+        outboundBatchOptions.value = [];
+      })
+      .finally(() => {
+        outboundBatchLoading.value = false;
+        if (keepSelected) {
+          ensureOutboundOptionsForSelected();
+          restoreOutboundTableSelection();
+        }
+      });
+  };
+
+  const handleCustomerChange = customerId => {
+    form.stockOutRecordIds = [];
+    form.outboundBatches = "";
+    form.amount = 0;
+    loadOutboundBatches(customerId);
+  };
+
+  const openOutboundSelectDialog = () => {
+    if (!form.customerId || isEdit.value || isView.value) return;
+    outboundSelectVisible.value = true;
+    loadOutboundBatches(form.customerId, true).then(() => {
+      restoreOutboundTableSelection();
+    });
+  };
+
+  const handleOutboundDialogSelectionChange = selection => {
+    dialogOutboundSelection.value = selection;
+  };
+
+  const confirmOutboundSelection = () => {
+    if (dialogOutboundSelection.value.length === 0) {
+      ElMessage.warning("璇疯嚦灏戦�夋嫨涓�鏉″叧鑱斿崟鎹�");
+      return;
+    }
+    form.stockOutRecordIds = dialogOutboundSelection.value
+      .map(row => getOutboundRowId(row))
+      .filter(id => id !== undefined && id !== null);
+    form.outboundBatches = dialogOutboundSelection.value
+      .map(row => row.outboundBatches ?? row.batchNo ?? row.shippingNo ?? "")
+      .filter(Boolean)
+      .join("銆�");
+    outboundSelectVisible.value = false;
+    syncCollectionAmount();
+    formRef.value?.validateField("stockOutRecordIds");
+  };
+
+  const handleOutboundDialogClosed = () => {
+    dialogOutboundSelection.value = [];
+  };
+
+  const appendFilterParams = params => {
+    if (filters.collectionNumber) {
+      params.collectionNumber = filters.collectionNumber;
+    }
+    if (filters.customerId) {
+      params.customerId = filters.customerId;
+    }
+    if (filters.collectionMethod) {
+      params.collectionMethod = filters.collectionMethod;
+    }
+    if (filters.dateRange?.length === 2) {
+      params.startDate = filters.dateRange[0];
+      params.endDate = filters.dateRange[1];
+    }
+    return params;
+  };
+
+  const buildListParams = () =>
+    appendFilterParams({
+      current: pagination.currentPage,
+      size: pagination.pageSize,
+    });
+
+  const buildExportParams = () => appendFilterParams({});
+
+  const buildSubmitPayload = (forUpdate = false) => {
+    const payload = {
+      customerId: form.customerId,
+      collectionDate: form.receiptDate,
+      collectionAmount: form.amount,
+      collectionMethod: form.receiptMethod,
+      collectionNumber: form.receiptCode || "",
+      remark: form.remark || "",
+      stockOutRecordIds: (form.stockOutRecordIds || []).join(","),
+    };
+    if (forUpdate) {
+      payload.id = currentId.value;
+    }
+    return payload;
+  };
+
+  const fillFormFromRow = row => {
+    const stockOutRecordIds = parseStockOutRecordIds(
+      row.stockOutRecordIds ?? row.stockOutRecordId
+    );
+    Object.assign(form, {
+      receiptCode: row.receiptCode ?? row.collectionNumber ?? "",
+      customerId: row.customerId,
+      receiptDate: row.receiptDate ?? row.collectionDate ?? "",
+      amount: Number(row.amount ?? row.collectionAmount ?? 0),
+      receiptMethod: row.receiptMethod ?? row.collectionMethod ?? "",
+      stockOutRecordIds,
+      outboundBatches: formatOutboundBatches(row.outboundBatches),
+      remark: row.remark ?? "",
+    });
+  };
+
+  const closeDialog = () => {
+    dialogVisible.value = false;
+    outboundSelectVisible.value = false;
+    isView.value = false;
+    isEdit.value = false;
+  };
+
+  const handleExport = () => {
+    const params = buildExportParams();
+    proxy.download(
+      "/accountSalesCollection/exportAccountSalesCollection",
+      params,
+      `鏀舵鍗昣${Date.now()}.xlsx`
+    );
+  };
+
+  const getTableData = () => {
+    tableLoading.value = true;
+    listPageAccountSalesCollection(buildListParams())
+      .then(res => {
+        const ok = res.code === 200 || res.code === 0;
+        if (ok && res.data) {
+          pagination.total = res.data.total ?? 0;
+          dataList.value = (res.data.records ?? []).map(normalizeTableRow);
+        } else {
+          ElMessage.error(res.msg || "鏌ヨ澶辫触");
+          dataList.value = [];
+          pagination.total = 0;
+        }
+      })
+      .catch(() => {
+        dataList.value = [];
+        pagination.total = 0;
+        ElMessage.error("鏌ヨ澶辫触");
+      })
+      .finally(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  const onSearch = () => {
+    pagination.currentPage = 1;
+    getTableData();
+  };
+
+  const resetFilters = () => {
+    filters.collectionNumber = "";
+    filters.customerId = "";
+    filters.collectionMethod = "";
+    filters.dateRange = [];
+    pagination.currentPage = 1;
+    getTableData();
+  };
+
+  const changePage = ({ current, size }) => {
+    pagination.currentPage = current;
+    pagination.pageSize = size;
+    getTableData();
+  };
+
+  const add = () => {
+    isEdit.value = false;
+    isView.value = false;
+    dialogTitle.value = "鏂板鏀舵";
+    Object.assign(form, {
+      receiptCode: "SK" + Date.now().toString().slice(-8),
+      customerId: "",
+      receiptDate: new Date().toISOString().split("T")[0],
+      amount: 0,
+      receiptMethod: getDefaultReceiptMethod(),
+      stockOutRecordIds: [],
+      outboundBatches: "",
+      remark: "",
+    });
+    outboundBatchList.value = [];
+    outboundBatchOptions.value = [];
+    dialogVisible.value = true;
+  };
+
+  const edit = row => {
+    isEdit.value = true;
+    isView.value = false;
+    currentId.value = row.id;
+    dialogTitle.value = "缂栬緫鏀舵";
+    fillFormFromRow(row);
+    dialogVisible.value = true;
+  };
+
+  const view = row => {
+    isView.value = true;
+    isEdit.value = false;
+    dialogTitle.value = "鏌ョ湅鏀舵";
+    fillFormFromRow(row);
+    dialogVisible.value = true;
+  };
+
+  const handleDelete = row => {
+    ElMessageBox.confirm(
+      `纭鍒犻櫎鏀舵鍗曘��${row.receiptCode ?? row.collectionNumber}銆嶅悧锛焋,
+      "鎻愮ず",
+      {
+        confirmButtonText: "纭畾",
+        cancelButtonText: "鍙栨秷",
+        type: "warning",
+      }
+    ).then(() => {
+      deleteAccountSalesCollection([row.id])
+        .then(res => {
+          if (res.code === 200) {
+            ElMessage.success("鍒犻櫎鎴愬姛");
+            getTableData();
+          } else {
+            ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+          }
+        })
+        .catch(() => {
+          ElMessage.error("鍒犻櫎澶辫触");
+        });
+    });
+  };
+
+  const submitForm = () => {
+    formRef.value.validate(valid => {
+      if (!valid) return;
+      submitLoading.value = true;
+      const request = isEdit.value
+        ? updateAccountSalesCollection(buildSubmitPayload(true))
+        : addAccountSalesCollection(buildSubmitPayload());
+      request
+        .then(res => {
+          if (res.code === 200) {
+            ElMessage.success(isEdit.value ? "淇敼鎴愬姛" : "鏂板鎴愬姛");
+            closeDialog();
+            getTableData();
+          } else {
+            ElMessage.error(res.msg || (isEdit.value ? "淇敼澶辫触" : "鏂板澶辫触"));
+          }
+        })
+        .catch(() => {
+          ElMessage.error(isEdit.value ? "淇敼澶辫触" : "鏂板澶辫触");
+        })
+        .finally(() => {
+          submitLoading.value = false;
+        });
+    });
+  };
+
+  onMounted(() => {
+    getCustomerList();
+    getTableData();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .actions {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 15px;
+  }
+
+  .text-success {
+    color: #67c23a;
+    font-weight: bold;
+  }
+
+  .outbound-batch-input:not(.is-disabled) {
+    :deep(.el-input__wrapper) {
+      cursor: pointer;
+    }
+  }
+</style>
diff --git a/src/views/financialManagement/receivable/reconciliation.vue b/src/views/financialManagement/receivable/reconciliation.vue
new file mode 100644
index 0000000..b1bff0e
--- /dev/null
+++ b/src/views/financialManagement/receivable/reconciliation.vue
@@ -0,0 +1,738 @@
+<template>
+  <div class="app-container">
+    <el-form :model="filters" :inline="true">
+      <el-form-item label="瀹㈡埛:">
+        <el-select v-model="filters.customerId" placeholder="璇烽�夋嫨瀹㈡埛" clearable filterable style="width: 200px;">
+          <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="瀵硅处鏈熼棿:">
+        <el-date-picker v-model="filters.startMonth" type="month" placeholder="寮�濮嬫湀浠�" value-format="YYYY-MM" style="width: 140px;" />
+        <span style="margin: 0 10px;">鑷�</span>
+        <el-date-picker v-model="filters.endMonth" type="month" placeholder="缁撴潫鏈堜唤" value-format="YYYY-MM" style="width: 140px;" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div>
+          <el-button type="primary" @click="generateStatement" icon="Document">鐢熸垚瀵硅处鍗�</el-button>
+        </div>
+        <div>
+          <el-button @click="handleOut" icon="Download">瀵煎嚭瀵硅处鍗�</el-button>
+        </div>
+      </div>
+      <PIMTable
+        rowKey="id"
+        :column="columns"
+        :tableData="dataList"
+        :tableLoading="tableLoading"
+        :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+        @pagination="changePage"
+      >
+        <template #openingBalance="{ row }">
+          <span :class="row.openingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.openingBalance) }}</span>
+        </template>
+        <template #currentPlan="{ row }">
+          <span class="text-primary">楼{{ formatMoney(row.currentPlan) }}</span>
+        </template>
+        <template #currentActually="{ row }">
+          <span class="text-success">楼{{ formatMoney(row.currentActually) }}</span>
+        </template>
+        <template #closingBalance="{ row }">
+          <span :class="row.closingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.closingBalance) }}</span>
+        </template>
+        <template #operation="{ row }">
+          <el-button type="primary" link @click="viewDetail(row)">鏌ョ湅鏄庣粏</el-button>
+          <!-- <el-button type="primary" link @click="printStatement(row)">鎵撳嵃</el-button> -->
+          <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+        </template>
+      </PIMTable>
+    </div>
+
+    <FormDialog title="瀵硅处鏄庣粏" v-model="detailDialogVisible" width="900px" @confirm="printDetail" @cancel="detailDialogVisible = false" operationType="detail">
+      <div class="statement-header">
+        <h3>{{ currentCustomer }} 搴旀敹瀵硅处鍗�</h3>
+        <p>瀵硅处鏈熼棿: {{ currentPeriod }}</p>
+      </div>
+      <el-table :data="detailData" border style="width: 100%" v-loading="detailLoading">
+        <el-table-column prop="date" label="鏃ユ湡" width="120" />
+        <el-table-column prop="type" label="绫诲瀷" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.type === '鍑哄簱' ? 'success' : row.type === '閫�璐�' ? 'danger' : 'primary'">{{ row.type }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="code" label="鍗曟嵁缂栧彿" width="150" />
+        <el-table-column prop="debit" label="鍊熸柟(搴旀敹)" width="120">
+          <template #default="{ row }">
+            <span v-if="row.debit > 0" class="text-danger">楼{{ formatMoney(row.debit) }}</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="credit" label="璐锋柟(鏀舵)" width="120">
+          <template #default="{ row }">
+            <span v-if="row.credit > 0" class="text-success">楼{{ formatMoney(row.credit) }}</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="balance" label="浣欓" width="120">
+          <template #default="{ row }">
+            <span :class="row.balance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.balance) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="remark" label="澶囨敞" show-overflow-tooltip />
+      </el-table>
+      <template #footer>
+        <el-button type="primary" @click="printDetail">鎵撳嵃</el-button>
+        <el-button @click="detailDialogVisible = false">鍏抽棴</el-button>
+      </template>
+    </FormDialog>
+
+    <FormDialog title="鐢熸垚瀵硅处鍗�" v-model="generateDialogVisible" width="1000px" @confirm="confirmGenerate" @cancel="generateDialogVisible = false">
+      <el-form :model="generateForm" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="閫夋嫨瀹㈡埛" prop="customerId">
+              <el-select
+                v-model="generateForm.customerId"
+                placeholder="璇烽�夋嫨瀹㈡埛"
+                style="width: 100%;"
+                filterable
+                @change="onCustomerChange"
+              >
+                <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="瀵硅处鏈堜唤" prop="statementMonth">
+              <el-date-picker v-model="generateForm.statementMonth" type="month" placeholder="閫夋嫨鏈堜唤" value-format="YYYY-MM" style="width: 100%;" @change="onStatementMonthChange" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <div v-if="statementDetailLoaded" class="sales-section">
+        <div v-if="salesData.length > 0" class="section-title">鏈湀閿�鍞暟鎹�</div>
+        <el-table
+          v-if="salesData.length > 0"
+          ref="salesTableRef"
+          :data="salesData"
+          border
+          row-key="id"
+          style="width: 100%; margin-bottom: 15px;"
+          v-loading="salesLoading"
+          @selection-change="handleSalesSelectionChange"
+        >
+          <el-table-column type="selection" width="55" align="center" />
+          <el-table-column prop="occurrenceDate" label="鏃ユ湡" width="120" />
+          <el-table-column prop="receiptNumber" label="鍗曟嵁缂栧彿" width="150" />
+          <el-table-column prop="type" label="绫诲瀷" width="100">
+            <template #default="{ row }">
+              <el-tag :type="getDetailTypeTagType(row.type)">{{ row.typeLabel }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="amount" label="閲戦" width="120">
+            <template #default="{ row }">
+              <span :class="getDetailAmountClass(row.type)">楼{{ formatMoney(row.amount) }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="remark" label="澶囨敞" />
+        </el-table>
+        <el-empty v-else description="璇ュ鎴锋湰鏈堟殏鏃犳槑缁嗘暟鎹�" :image-size="80" />
+
+        <div class="summary-row">
+          <span>鏈熷垵浣欓: <strong class="text-primary">楼{{ formatMoney(generateForm.openingBalance) }}</strong></span>
+          <span>鏈湡搴旀敹: <strong class="text-primary">楼{{ formatMoney(generateForm.currentPlan) }}</strong></span>
+          <span>鏈湡鏀舵: <strong class="text-success">楼{{ formatMoney(generateForm.currentActually) }}</strong></span>
+          <span>鏈熸湯浣欓: <strong :class="displayClosingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(displayClosingBalance) }}</strong></span>
+        </div>
+      </div>
+
+      <div v-else-if="generateForm.customerId && generateForm.statementMonth && !salesLoading" class="empty-tip">
+        <el-empty description="璇ュ鎴锋湰鏈堟殏鏃犻攢鍞暟鎹�" />
+      </div>
+
+      <template #footer>
+        <el-button type="primary" @click="confirmGenerate" :disabled="!canGenerate" :loading="submitLoading">纭鐢熸垚</el-button>
+        <el-button @click="generateDialogVisible = false">鍙栨秷</el-button>
+      </template>
+    </FormDialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, nextTick, getCurrentInstance } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { listCustomer } from "@/api/basicData/customer.js";
+import {
+  getAccountStatementDetailsByMonth,
+  addAccountStatement,
+  listPageAccountStatement,
+  deleteAccountStatement,
+} from "@/api/financialManagement/accountStatement.js";
+
+const ACCOUNT_TYPE_RECEIVABLE = 1;
+
+const { proxy } = getCurrentInstance();
+
+defineOptions({
+  name: "搴旀敹瀵硅处",
+});
+
+const filters = reactive({
+  customerId: "",
+  startMonth: "",
+  endMonth: "",
+});
+
+const pagination = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+const columns = [
+  { label: "瀵硅处鍗曞彿", prop: "statementNumber", width: "150" },
+  { label: "瀹㈡埛鍚嶇О", prop: "customerName", width: "180" },
+  { label: "瀵硅处鏈熼棿", prop: "statementMonth", width: "150" },
+  { label: "鏈熷垵浣欓", prop: "openingBalance", dataType: "slot", slot: "openingBalance" },
+  { label: "鏈湡搴旀敹", prop: "currentPlan", dataType: "slot", slot: "currentPlan" },
+  { label: "鏈湡鏀舵", prop: "currentActually", dataType: "slot", slot: "currentActually" },
+  { label: "鏈熸湯浣欓", prop: "closingBalance", dataType: "slot", slot: "closingBalance" },
+  { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "200", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const submitLoading = ref(false);
+const detailDialogVisible = ref(false);
+const currentCustomer = ref("");
+const currentPeriod = ref("");
+const detailData = ref([]);
+const detailLoading = ref(false);
+
+const generateDialogVisible = ref(false);
+const salesLoading = ref(false);
+const statementDetailLoaded = ref(false);
+const salesData = ref([]);
+const selectedSales = ref([]);
+const salesTableRef = ref(null);
+const customerList = ref([]);
+
+/** 鏄庣粏 type锛�1鍑哄簱 2鍏ュ簱 3鏀舵 4浠樻 5閫�璐� */
+const STATEMENT_DETAIL_TYPE_MAP = {
+  1: "鍑哄簱",
+  2: "鍏ュ簱",
+  3: "鏀舵",
+  4: "浠樻",
+  5: "閫�璐�",
+};
+
+const calculateEndBalance = (openingBalance, currentPlan, currentActually) => {
+  return openingBalance + currentPlan - currentActually;
+};
+
+const getDetailTypeLabel = (type) => STATEMENT_DETAIL_TYPE_MAP[Number(type)] ?? "";
+
+const getDetailTypeTagType = (type) => {
+  const t = Number(type);
+  if (t === 1) return "success";
+  if (t === 3) return "primary";
+  if (t === 5) return "danger";
+  return "info";
+};
+
+const getDetailAmountClass = (type) => {
+  const t = Number(type);
+  if (t === 1) return "text-primary";
+  if (t === 3) return "text-success";
+  return "text-danger";
+};
+
+const generateForm = reactive({
+  customerId: "",
+  customerName: "",
+  statementMonth: "",
+  openingBalance: 0,
+  currentPlan: 0,
+  currentActually: 0,
+  closingBalance: 0,
+});
+
+const displayClosingBalance = computed(() => {
+  return calculateEndBalance(
+    generateForm.openingBalance,
+    generateForm.currentPlan,
+    generateForm.currentActually
+  );
+});
+
+const canGenerate = computed(() => {
+  return generateForm.customerId && generateForm.statementMonth && selectedSales.value.length > 0;
+});
+
+const applyStatementSummary = (data) => {
+  generateForm.openingBalance = Number(data.openingBalance ?? 0);
+  generateForm.currentPlan = Number(data.currentPlan ?? 0);
+  generateForm.currentActually = Number(data.currentActually ?? 0);
+  generateForm.closingBalance = Number(
+    data.closingBalance ??
+      calculateEndBalance(
+        generateForm.openingBalance,
+        generateForm.currentPlan,
+        generateForm.currentActually
+      )
+  );
+};
+
+const getCustomerList = () => {
+  listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
+    if (res.code === 200) {
+      customerList.value = res.data?.records || [];
+    }
+  });
+};
+
+const normalizeSalesRows = (list) => {
+  const rows = Array.isArray(list) ? list : [];
+  return rows.map((item, index) => {
+    const type = Number(item.type);
+    return {
+      id: item.id ?? `detail-${index}`,
+      accountStatementId: item.accountStatementId,
+      occurrenceDate: item.occurrenceDate ?? "",
+      receiptNumber: item.receiptNumber ?? "",
+      type,
+      typeLabel: getDetailTypeLabel(type),
+      amount: Math.abs(Number(item.amount ?? 0)),
+      remark: item.remark ?? "",
+    };
+  });
+};
+
+const selectAllSalesRows = (keepApiSummary = false) => {
+  nextTick(() => {
+    const table = salesTableRef.value;
+    if (!table) return;
+    table.clearSelection();
+    salesData.value.forEach((row) => table.toggleRowSelection(row, true));
+    selectedSales.value = [...salesData.value];
+    if (!keepApiSummary) {
+      calculateSummary();
+    }
+  });
+};
+
+const isNumericId = (id) => id !== undefined && id !== null && id !== "" && /^\d+$/.test(String(id));
+
+const buildFilterParams = (params = {}) => {
+  const result = { ...params, accountType: ACCOUNT_TYPE_RECEIVABLE };
+  if (filters.customerId) {
+    result.customerId = filters.customerId;
+  }
+  if (filters.startMonth && filters.endMonth && filters.startMonth === filters.endMonth) {
+    result.statementMonth = filters.startMonth;
+  } else if (filters.startMonth) {
+    result.startMonth = filters.startMonth;
+  }
+  if (filters.endMonth && filters.startMonth !== filters.endMonth) {
+    result.endMonth = filters.endMonth;
+  }
+  return result;
+};
+
+const buildListParams = () =>
+  buildFilterParams({
+    current: pagination.currentPage,
+    size: pagination.pageSize,
+  });
+
+const buildExportParams = () => buildFilterParams({});
+
+const buildDetailSubmitItem = (row) => {
+  const item = {
+    occurrenceDate: row.occurrenceDate,
+    receiptNumber: row.receiptNumber,
+    type: row.type,
+    amount: row.amount,
+    remark: row.remark ?? "",
+  };
+  if (isNumericId(row.id)) {
+    item.id = Number(row.id);
+  }
+  if (row.accountStatementId) {
+    item.accountStatementId = row.accountStatementId;
+  }
+  return item;
+};
+
+const buildAddPayload = () => ({
+  customerId: generateForm.customerId,
+  customerName: generateForm.customerName,
+  statementMonth: generateForm.statementMonth,
+  accountType: ACCOUNT_TYPE_RECEIVABLE,
+  statementNumber: "",
+  openingBalance: generateForm.openingBalance,
+  currentPlan: generateForm.currentPlan,
+  currentActually: generateForm.currentActually,
+  closingBalance: generateForm.closingBalance,
+  accountStatementDetails: selectedSales.value.map(buildDetailSubmitItem),
+});
+
+const formatMoney = (value) => {
+  if (value === undefined || value === null) return "0.00";
+  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getTableData = () => {
+  tableLoading.value = true;
+  listPageAccountStatement(buildListParams())
+    .then((res) => {
+      const ok = res.code === 200 || res.code === 0;
+      if (ok && res.data) {
+        pagination.total = res.data.total ?? 0;
+        dataList.value = res.data.records ?? [];
+      } else {
+        ElMessage.error(res.msg || "鏌ヨ澶辫触");
+        dataList.value = [];
+        pagination.total = 0;
+      }
+    })
+    .catch(() => {
+      dataList.value = [];
+      pagination.total = 0;
+      ElMessage.error("鏌ヨ澶辫触");
+    })
+    .finally(() => {
+      tableLoading.value = false;
+    });
+};
+
+const resetFilters = () => {
+  filters.customerId = "";
+  filters.startMonth = "";
+  filters.endMonth = "";
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const changePage = ({ current, size }) => {
+  pagination.currentPage = current;
+  pagination.pageSize = size;
+  getTableData();
+};
+
+const generateStatement = () => {
+  generateForm.customerId = "";
+  generateForm.customerName = "";
+  generateForm.statementMonth = "";
+  generateForm.openingBalance = 0;
+  generateForm.currentPlan = 0;
+  generateForm.currentActually = 0;
+  generateForm.closingBalance = 0;
+  statementDetailLoaded.value = false;
+  salesData.value = [];
+  selectedSales.value = [];
+  generateDialogVisible.value = true;
+};
+
+const onCustomerChange = (customerId) => {
+  const customer = customerList.value.find((item) => item.id === customerId);
+  generateForm.customerName = customer?.customerName ?? "";
+  loadSalesData();
+};
+
+const onStatementMonthChange = () => {
+  loadSalesData();
+};
+
+const loadSalesData = () => {
+  if (!generateForm.customerId || !generateForm.statementMonth) {
+    salesData.value = [];
+    selectedSales.value = [];
+    statementDetailLoaded.value = false;
+    generateForm.openingBalance = 0;
+    generateForm.currentPlan = 0;
+    generateForm.currentActually = 0;
+    generateForm.closingBalance = 0;
+    return;
+  }
+
+  salesLoading.value = true;
+  selectedSales.value = [];
+  statementDetailLoaded.value = false;
+
+  getAccountStatementDetailsByMonth({
+    accountType: ACCOUNT_TYPE_RECEIVABLE,
+    customerId: generateForm.customerId,
+    statementMonth: generateForm.statementMonth,
+  })
+    .then((res) => {
+      if (res.code === 200) {
+        const data = res.data ?? {};
+        const details = data.accountStatementDetails;
+        const list = Array.isArray(details) ? details : [];
+        salesData.value = normalizeSalesRows(list);
+        applyStatementSummary(data);
+        statementDetailLoaded.value = true;
+
+        if (salesData.value.length > 0) {
+          selectAllSalesRows(true);
+        }
+      } else {
+        salesData.value = [];
+        statementDetailLoaded.value = false;
+        ElMessage.error(res.msg || "鏌ヨ瀵硅处鏄庣粏澶辫触");
+      }
+    })
+    .catch(() => {
+      salesData.value = [];
+      statementDetailLoaded.value = false;
+      ElMessage.error("鏌ヨ瀵硅处鏄庣粏澶辫触");
+    })
+    .finally(() => {
+      salesLoading.value = false;
+    });
+};
+
+const calculateSummary = () => {
+  let receivable = 0;
+  let receipt = 0;
+
+  selectedSales.value.forEach((item) => {
+    if (item.type === 1) {
+      receivable += item.amount;
+    } else if (item.type === 5) {
+      receivable -= item.amount;
+    } else if (item.type === 3) {
+      receipt += item.amount;
+    }
+  });
+
+  generateForm.currentPlan = receivable;
+  generateForm.currentActually = receipt;
+  generateForm.closingBalance = calculateEndBalance(
+    generateForm.openingBalance,
+    generateForm.currentPlan,
+    generateForm.currentActually
+  );
+};
+
+const handleSalesSelectionChange = (selection) => {
+  selectedSales.value = selection;
+  calculateSummary();
+};
+
+const confirmGenerate = () => {
+  if (!canGenerate.value) return;
+  submitLoading.value = true;
+  addAccountStatement(buildAddPayload())
+    .then((res) => {
+      if (res.code === 200) {
+        generateDialogVisible.value = false;
+        ElMessage.success("瀵硅处鍗曠敓鎴愭垚鍔�");
+        pagination.currentPage = 1;
+        getTableData();
+      } else {
+        ElMessage.error(res.msg || "鐢熸垚澶辫触");
+      }
+    })
+    .catch(() => {
+      ElMessage.error("鐢熸垚澶辫触");
+    })
+    .finally(() => {
+      submitLoading.value = false;
+    });
+};
+
+const handleDelete = (row) => {
+  ElMessageBox.confirm(`纭鍒犻櫎瀵硅处鍗曘��${row.statementNumber || row.id}銆嶅悧锛焋, "鎻愮ず", {
+    confirmButtonText: "纭畾",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(() => {
+    deleteAccountStatement([row.id])
+      .then((res) => {
+        if (res.code === 200) {
+          ElMessage.success("鍒犻櫎鎴愬姛");
+          getTableData();
+        } else {
+          ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+        }
+      })
+      .catch(() => {
+        ElMessage.error("鍒犻櫎澶辫触");
+      });
+  });
+};
+
+const buildDetailTableFromApi = (data, statementMonth) => {
+  const details = Array.isArray(data.accountStatementDetails) ? data.accountStatementDetails : [];
+  let runningBalance = Number(data.openingBalance ?? 0);
+  const rows = [
+    {
+      date: statementMonth ?? "",
+      type: "鏈熷垵",
+      code: "-",
+      debit: 0,
+      credit: 0,
+      balance: runningBalance,
+      remark: "鏈熷垵浣欓",
+    },
+  ];
+
+  details.forEach((item) => {
+    const amount = Math.abs(Number(item.amount ?? 0));
+    const type = Number(item.type);
+    let debit = 0;
+    let credit = 0;
+
+    if (type === 1) {
+      debit = amount;
+      runningBalance += amount;
+    } else if (type === 3 || type === 5) {
+      credit = amount;
+      runningBalance -= amount;
+    }
+
+    rows.push({
+      date: item.occurrenceDate ?? "",
+      type: getDetailTypeLabel(type),
+      code: item.receiptNumber ?? "",
+      debit,
+      credit,
+      balance: runningBalance,
+      remark: item.remark ?? "",
+    });
+  });
+
+  return rows;
+};
+
+const viewDetail = (row) => {
+  if (!row.customerId || !row.statementMonth) {
+    ElMessage.warning("缂哄皯瀹㈡埛鎴栧璐︽湀浠斤紝鏃犳硶鏌ヨ鏄庣粏");
+    return;
+  }
+
+  currentCustomer.value = row.customerName ?? "";
+  currentPeriod.value = row.statementMonth ?? "";
+  detailData.value = [];
+  detailDialogVisible.value = true;
+  detailLoading.value = true;
+
+  getAccountStatementDetailsByMonth({
+    accountType: ACCOUNT_TYPE_RECEIVABLE,
+    customerId: row.customerId,
+    statementMonth: row.statementMonth,
+  })
+    .then((res) => {
+      if (res.code === 200) {
+        detailData.value = buildDetailTableFromApi(res.data ?? {}, row.statementMonth);
+      } else {
+        ElMessage.error(res.msg || "鏌ヨ鏄庣粏澶辫触");
+        detailDialogVisible.value = false;
+      }
+    })
+    .catch(() => {
+      ElMessage.error("鏌ヨ鏄庣粏澶辫触");
+      detailDialogVisible.value = false;
+    })
+    .finally(() => {
+      detailLoading.value = false;
+    });
+};
+
+const printStatement = (row) => {
+  ElMessage.info(`鎵撳嵃瀵硅处鍗�: ${row.statementNumber}`);
+};
+
+const printDetail = () => {
+  ElMessage.info("鎵撳嵃鏄庣粏");
+};
+
+const handleOut = () => {
+  const params = buildExportParams();
+  proxy.download("/accountStatement/exportAccountStatement", params, `搴旀敹瀵硅处鍗昣${Date.now()}.xlsx`);
+};
+
+onMounted(() => {
+  getCustomerList();
+  getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 15px;
+}
+
+.text-success {
+  color: #67c23a;
+}
+
+.text-danger {
+  color: #f56c6c;
+}
+
+.text-primary {
+  color: #409eff;
+}
+
+.statement-header {
+  text-align: center;
+  margin-bottom: 20px;
+  h3 {
+    margin: 0 0 10px 0;
+  }
+  p {
+    color: #909399;
+    margin: 0;
+  }
+}
+
+.sales-section {
+  margin-top: 20px;
+
+  .section-title {
+    font-size: 16px;
+    font-weight: bold;
+    margin-bottom: 15px;
+    padding-left: 10px;
+    border-left: 4px solid #409eff;
+  }
+}
+
+.summary-row {
+  display: flex;
+  justify-content: space-around;
+  padding: 15px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  margin-top: 15px;
+
+  span {
+    font-size: 14px;
+
+    strong {
+      font-size: 16px;
+      margin-left: 5px;
+    }
+  }
+}
+
+.empty-tip {
+  margin-top: 30px;
+}
+</style>
diff --git a/src/views/financialManagement/receivable/salesOut.vue b/src/views/financialManagement/receivable/salesOut.vue
new file mode 100644
index 0000000..0e24b37
--- /dev/null
+++ b/src/views/financialManagement/receivable/salesOut.vue
@@ -0,0 +1,180 @@
+<template>
+  <!-- 閿�鍞嚭搴� -->
+  <div class="app-container">
+    <el-form :model="filters"
+             :inline="true">
+      <el-form-item label="鍑哄簱鍗曞彿:">
+        <el-input v-model="filters.outboundBatches"
+                  placeholder="璇疯緭鍏ュ嚭搴撳崟鍙�"
+                  clearable
+                  style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="瀹㈡埛鍚嶇О:">
+        <el-input v-model="filters.customerName"
+                  placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+                  clearable
+                  style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="鍑哄簱鏃ユ湡:">
+        <el-date-picker v-model="filters.dateRange"
+                        value-format="YYYY-MM-DD"
+                        format="YYYY-MM-DD"
+                        type="daterange"
+                        range-separator="鑷�"
+                        start-placeholder="寮�濮嬫棩鏈�"
+                        end-placeholder="缁撴潫鏃ユ湡"
+                        clearable />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary"
+                   @click="onSearch">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div></div>
+        <div>
+          <el-button @click="handleOut"
+                     icon="Download">瀵煎嚭</el-button>
+        </div>
+      </div>
+      <PIMTable rowKey="id"
+                :column="columns"
+                :tableData="dataList"
+                :tableLoading="tableLoading"
+                :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+                @pagination="changePage" />
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+  import { ElMessage } from "element-plus";
+  import { listPageAccountSales } from "@/api/financialManagement/accountSales";
+
+  defineOptions({
+    name: "閿�鍞嚭搴�",
+  });
+
+  const { proxy } = getCurrentInstance();
+
+  const filters = reactive({
+    outboundBatches: "",
+    customerName: "",
+    dateRange: [],
+  });
+
+  const pagination = reactive({
+    currentPage: 1,
+    pageSize: 10,
+    total: 0,
+  });
+
+  const columns = [
+    { label: "鍑哄簱鍗曞彿", prop: "outboundBatches", minWidth: "150" },
+    { label: "瀹㈡埛鍚嶇О", prop: "customerName", minWidth: "180" },
+    { label: "鍑哄簱鏃ユ湡", prop: "shippingDate", width: "170" },
+    { label: "浜у搧鍚嶇О", prop: "productName", minWidth: "140" },
+    { label: "瑙勬牸鍨嬪彿", prop: "specificationModel", minWidth: "140" },
+    {
+      label: "閲戦",
+      prop: "outboundAmount",
+      minWidth: "120",
+      align: "right",
+      formatData: val =>
+        val === null || val === undefined || val === ""
+          ? ""
+          : Number(val).toLocaleString("zh-CN", {
+              minimumFractionDigits: 2,
+              maximumFractionDigits: 2,
+            }),
+    },
+    { label: "鍙戣揣缂栧彿", prop: "shippingNo", minWidth: "140" },
+    { label: "閿�鍞鍗曞彿", prop: "salesContractNo", minWidth: "150" },
+  ];
+
+  const dataList = ref([]);
+  const tableLoading = ref(false);
+
+  function buildFilterParams() {
+    const params = {
+      outboundBatches: filters.outboundBatches || undefined,
+      customerName: filters.customerName || undefined,
+    };
+    if (filters.dateRange && filters.dateRange.length === 2) {
+      params.startDate = filters.dateRange[0];
+      params.endDate = filters.dateRange[1];
+    }
+    return params;
+  }
+
+  const onSearch = () => {
+    pagination.currentPage = 1;
+    getTableData();
+  };
+
+  const getTableData = () => {
+    tableLoading.value = true;
+    listPageAccountSales({
+      ...buildFilterParams(),
+      current: pagination.currentPage,
+      size: pagination.pageSize,
+    })
+      .then(res => {
+        const ok = res.code === 200 || res.code === 0;
+        if (ok && res.data) {
+          pagination.total = res.data.total ?? 0;
+          dataList.value = res.data.records ?? [];
+        } else {
+          ElMessage.error(res.msg || "鏌ヨ澶辫触");
+          dataList.value = [];
+        }
+      })
+      .catch(() => {
+        dataList.value = [];
+      })
+      .finally(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  const resetFilters = () => {
+    filters.outboundBatches = "";
+    filters.customerName = "";
+    filters.dateRange = [];
+    pagination.currentPage = 1;
+    getTableData();
+  };
+
+  const changePage = ({ page, limit }) => {
+    pagination.currentPage = page;
+    pagination.pageSize = limit;
+    getTableData();
+  };
+
+  const handleOut = () => {
+    proxy.download(
+      "/accountSales/exportAccountSalesOutbound",
+      buildFilterParams(),
+      `閿�鍞嚭搴揰${new Date().getTime()}.xlsx`
+    );
+  };
+
+  onMounted(() => {
+    getTableData();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .actions {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 15px;
+  }
+</style>
diff --git a/src/views/financialManagement/receivable/salesReturn.vue b/src/views/financialManagement/receivable/salesReturn.vue
new file mode 100644
index 0000000..c58d330
--- /dev/null
+++ b/src/views/financialManagement/receivable/salesReturn.vue
@@ -0,0 +1,171 @@
+<template>
+  <!-- 閿�鍞��璐� -->
+  <div class="app-container">
+    <el-form :model="filters" :inline="true">
+      <el-form-item label="閫�璐у崟鍙�:">
+        <el-input v-model="filters.returnNo" placeholder="璇疯緭鍏ラ��璐у崟鍙�" clearable style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="瀹㈡埛鍚嶇О:">
+        <el-input v-model="filters.customerName" placeholder="璇疯緭鍏ュ鎴峰悕绉�" clearable style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="閫�璐ф棩鏈�:">
+        <el-date-picker
+          v-model="filters.dateRange"
+          value-format="YYYY-MM-DD"
+          format="YYYY-MM-DD"
+          type="daterange"
+          range-separator="鑷�"
+          start-placeholder="寮�濮嬫棩鏈�"
+          end-placeholder="缁撴潫鏃ユ湡"
+          clearable
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div></div>
+        <div>
+          <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button>
+        </div>
+      </div>
+      <PIMTable
+        rowKey="id"
+        :column="columns"
+        :tableData="dataList"
+        :tableLoading="tableLoading"
+        :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+        @pagination="changePage"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+import { ElMessage } from "element-plus";
+import { listPageAccountSalesReturn } from "@/api/financialManagement/accountSales";
+
+defineOptions({
+  name: "閿�鍞��璐�",
+});
+
+const { proxy } = getCurrentInstance();
+
+const filters = reactive({
+  returnNo: "",
+  customerName: "",
+  dateRange: [],
+});
+
+const pagination = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+const columns = [
+  { label: "閫�璐у崟鍙�", prop: "returnNo", minWidth: "150" },
+  { label: "瀹㈡埛鍚嶇О", prop: "customerName", minWidth: "180" },
+  { label: "鍏宠仈鍙戣揣鍗曞彿", prop: "shippingNo", minWidth: "150" },
+  { label: "閫�璐ф棩鏈�", prop: "makeTime", minWidth: "170" },
+  {
+    label: "閫�娆炬�婚",
+    prop: "refundAmount",
+    minWidth: "120",
+    align: "right",
+    formatData: (val) =>
+      val === null || val === undefined || val === ""
+        ? ""
+        : Number(val).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
+  },
+  { label: "閫�璐у師鍥�", prop: "returnReason", minWidth: "150", showOverflowTooltip: true },
+  { label: "閿�鍞鍗曞彿", prop: "salesContractNo", minWidth: "150" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+
+function buildFilterParams() {
+  const params = {
+    returnNo: filters.returnNo || undefined,
+    customerName: filters.customerName || undefined,
+  };
+  if (filters.dateRange && filters.dateRange.length === 2) {
+    params.startDate = filters.dateRange[0];
+    params.endDate = filters.dateRange[1];
+  }
+  return params;
+}
+
+const onSearch = () => {
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const getTableData = () => {
+  tableLoading.value = true;
+  listPageAccountSalesReturn({
+    ...buildFilterParams(),
+    current: pagination.currentPage,
+    size: pagination.pageSize,
+  })
+    .then((res) => {
+      const ok = res.code === 200 || res.code === 0;
+      if (ok && res.data) {
+        pagination.total = res.data.total ?? 0;
+        dataList.value = res.data.records ?? [];
+      } else {
+        ElMessage.error(res.msg || "鏌ヨ澶辫触");
+        dataList.value = [];
+      }
+    })
+    .catch(() => {
+      dataList.value = [];
+    })
+    .finally(() => {
+      tableLoading.value = false;
+    });
+};
+
+const resetFilters = () => {
+  filters.returnNo = "";
+  filters.customerName = "";
+  filters.dateRange = [];
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+  pagination.currentPage = page;
+  pagination.pageSize = limit;
+  getTableData();
+};
+
+const handleOut = () => {
+  proxy.download(
+    "/accountSales/exportAccountSalesReturn",
+    buildFilterParams(),
+    `閿�鍞��璐${new Date().getTime()}.xlsx`
+  );
+};
+
+onMounted(() => {
+  getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 15px;
+}
+</style>
diff --git a/src/views/financialManagement/voucher/detailLedger.vue b/src/views/financialManagement/voucher/detailLedger.vue
new file mode 100644
index 0000000..c07574c
--- /dev/null
+++ b/src/views/financialManagement/voucher/detailLedger.vue
@@ -0,0 +1,309 @@
+<template>
+  <div class="app-container ledger-page">
+    <div class="ledger-layout">
+      <aside class="subject-panel">
+        <el-input v-model="subjectKeyword" placeholder="璇疯緭鍏ョ鐩悕绉�/缂栧彿" clearable prefix-icon="Search" />
+        <el-scrollbar class="subject-tree-scroll">
+          <el-tree
+            ref="subjectTreeRef"
+            :data="subjectOptions"
+            node-key="code"
+            :props="{ label: 'name', children: 'children' }"
+            highlight-current
+            default-expand-all
+            :expand-on-click-node="false"
+            :filter-node-method="filterSubjectNode"
+            @node-click="handleSubjectClick"
+          >
+            <template #default="{ data }">
+              <span class="subject-node">{{ data.code }} {{ data.name }}</span>
+            </template>
+          </el-tree>
+        </el-scrollbar>
+      </aside>
+
+      <section class="ledger-content">
+        <el-form :model="filters" :inline="true" class="filter-form">
+          <el-form-item label="鏈熼棿:">
+            <el-date-picker v-model="filters.startMonth" type="month" placeholder="寮�濮嬫湀浠�" value-format="YYYY-MM" style="width: 140px;" />
+            <span style="margin: 0 10px;">鑷�</span>
+            <el-date-picker v-model="filters.endMonth" type="month" placeholder="缁撴潫鏈堜唤" value-format="YYYY-MM" style="width: 140px;" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="getTableData">鏌ヨ</el-button>
+            <el-button @click="resetFilters">閲嶇疆</el-button>
+<!--            <el-button @click="handlePrint" icon="Printer">鎵撳嵃</el-button>-->
+            <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button>
+          </el-form-item>
+        </el-form>
+
+        <div class="table_list">
+          <el-table :data="dataList" border style="width: 100%">
+            <el-table-column prop="date" label="鏃ユ湡" width="120" />
+            <el-table-column prop="voucherNo" label="鍑瘉瀛楀彿" width="120" />
+            <el-table-column prop="summary" label="鎽樿" min-width="200" show-overflow-tooltip />
+            <el-table-column prop="debit" label="鍊熸柟" width="150">
+              <template #default="{ row }">
+                <span v-if="row.debit > 0" class="text-danger">楼{{ formatMoney(row.debit) }}</span>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="credit" label="璐锋柟" width="150">
+              <template #default="{ row }">
+                <span v-if="row.credit > 0" class="text-success">楼{{ formatMoney(row.credit) }}</span>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="鏂瑰悜" width="80">
+              <template #default="{ row }">
+                <el-tag :type="row.direction === '鍊�' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="浣欓" width="150">
+              <template #default="{ row }">
+                <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">楼{{ formatMoney(Math.abs(row.balance)) }}</span>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+
+        <el-empty v-if="!currentSubject" description="璇烽�夋嫨浼氳绉戠洰鏌ヨ" style="margin-top: 50px;" />
+      </section>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, watch, nextTick } from "vue";
+import { ElMessage } from "element-plus";
+import { listAccountSubject } from "@/api/financialManagement/accountSubject";
+import { getDetailLedger } from "@/api/financialManagement/ledger";
+
+defineOptions({
+  name: "绉戠洰鏄庣粏璐�",
+});
+
+const filters = reactive({
+  subject: "",
+  startMonth: "",
+  endMonth: "",
+});
+
+const dataList = ref([]);
+const subjectOptions = ref([]);
+const subjectKeyword = ref("");
+const subjectTreeRef = ref();
+
+const getPreviousMonth = () => {
+  const date = new Date();
+  date.setDate(1);
+  date.setMonth(date.getMonth() - 1);
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, "0");
+  return `${year}-${month}`;
+};
+
+const defaultMonth = getPreviousMonth();
+filters.startMonth = defaultMonth;
+filters.endMonth = defaultMonth;
+
+const fallbackSubjects = [
+  { code: "1122", name: "搴旀敹璐︽" },
+  { code: "2202", name: "搴斾粯璐︽" },
+  { code: "6602", name: "绠$悊璐圭敤" },
+];
+
+const toTree = (nodes = []) =>
+  nodes
+    .filter(item => item.subjectCode && item.subjectName)
+    .map(item => ({
+      code: item.subjectCode,
+      name: item.subjectName,
+      children: toTree(item.children || []),
+    }));
+
+const findSubject = (options, code) => {
+  for (const item of options) {
+    if (item.code === code) return item;
+    if (item.children && item.children.length > 0) {
+      const found = findSubject(item.children, code);
+      if (found) return found;
+    }
+  }
+  return null;
+};
+
+const currentSubject = computed(() => {
+  if (!filters.subject) return null;
+  return findSubject(subjectOptions.value, filters.subject);
+});
+
+const getFirstSubjectCode = (nodes = []) => {
+  for (const item of nodes) {
+    if (item.code) return item.code;
+    if (item.children && item.children.length > 0) {
+      const childCode = getFirstSubjectCode(item.children);
+      if (childCode) return childCode;
+    }
+  }
+  return "";
+};
+
+const setDefaultSubjectSelection = async () => {
+  const firstCode = getFirstSubjectCode(subjectOptions.value);
+  if (!firstCode) {
+    filters.subject = "";
+    subjectTreeRef.value?.setCurrentKey(null);
+    return;
+  }
+  filters.subject = firstCode;
+  await nextTick();
+  subjectTreeRef.value?.setCurrentKey(firstCode);
+};
+
+const filterSubjectNode = (value, data) => {
+  const keyword = value?.trim();
+  if (!keyword) return true;
+  return `${data.code}${data.name}`.includes(keyword);
+};
+
+watch(subjectKeyword, (value) => {
+  subjectTreeRef.value?.filter(value || "");
+});
+
+const handleSubjectClick = async (data) => {
+  filters.subject = data.code;
+  await getTableData();
+};
+
+const loadSubjectOptions = async () => {
+  let options = [];
+  try {
+    const { data } = await listAccountSubject({
+      current: 1,
+      size: 1000,
+    });
+    options = toTree(data?.records || []);
+  } catch (error) {
+    // 鍏ㄥ眬鎷︽埅鍣ㄥ凡鎻愮ず锛屼笅闈㈣蛋鍏滃簳绉戠洰
+  }
+  if (options.length === 0) {
+    options = fallbackSubjects.map(item => ({ ...item, children: [] }));
+  }
+  subjectOptions.value = options;
+  await setDefaultSubjectSelection();
+  if (filters.subject) {
+    await getTableData();
+  }
+};
+
+const formatMoney = (value) => {
+  if (value === undefined || value === null) return "0.00";
+  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+// 鑱旇皟绾﹀畾锛氭槑缁嗚处鎸夌鐩笌鏈熼棿杩囨护
+const getTableData = async () => {
+  if (!currentSubject.value) {
+    dataList.value = [];
+    return;
+  }
+  try {
+    const { data } = await getDetailLedger({
+      subjectCode: currentSubject.value.code,
+      startMonth: filters.startMonth,
+      endMonth: filters.endMonth,
+    });
+    dataList.value = Array.isArray(data) ? data : data?.records || [];
+  } catch (error) {
+    // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+  }
+};
+
+const resetFilters = async () => {
+  filters.startMonth = defaultMonth;
+  filters.endMonth = defaultMonth;
+  dataList.value = [];
+  subjectKeyword.value = "";
+  subjectTreeRef.value?.filter("");
+  await setDefaultSubjectSelection();
+  if (filters.subject) {
+    await getTableData();
+  }
+};
+
+const handlePrint = () => {
+  ElMessage.info("鎵撳嵃鍔熻兘");
+};
+
+const handleOut = () => {
+  ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+onMounted(async () => {
+  await loadSubjectOptions();
+});
+</script>
+
+<style lang="scss" scoped>
+.ledger-layout {
+  display: flex;
+  gap: 16px;
+}
+
+.subject-panel {
+  width: 260px;
+  flex-shrink: 0;
+  padding: 12px;
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+  background-color: #fff;
+}
+
+.subject-tree-scroll {
+  height: 600px;
+  margin-top: 12px;
+}
+
+.subject-node {
+  display: inline-flex;
+  align-items: center;
+}
+
+.ledger-content {
+  flex: 1;
+  min-width: 0;
+}
+
+.filter-form {
+  margin-bottom: 12px;
+}
+
+.text-primary {
+  color: #409eff;
+  font-weight: bold;
+}
+
+.text-success {
+  color: #67c23a;
+  font-weight: bold;
+}
+
+.text-danger {
+  color: #f56c6c;
+  font-weight: bold;
+}
+
+.text-warning {
+  color: #e6a23c;
+  font-weight: bold;
+}
+
+.subject-panel :deep(.el-tree-node__content) {
+  height: 34px;
+}
+
+.subject-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
+  background-color: #f0f7ff;
+}
+</style>
diff --git a/src/views/financialManagement/voucher/generalLedger.vue b/src/views/financialManagement/voucher/generalLedger.vue
new file mode 100644
index 0000000..b362279
--- /dev/null
+++ b/src/views/financialManagement/voucher/generalLedger.vue
@@ -0,0 +1,312 @@
+<template>
+  <div class="app-container ledger-page">
+    <div class="ledger-layout">
+      <aside class="subject-panel">
+        <el-input v-model="subjectKeyword" placeholder="璇疯緭鍏ョ鐩悕绉�/缂栧彿" clearable prefix-icon="Search" />
+        <el-scrollbar class="subject-tree-scroll">
+          <el-tree
+            ref="subjectTreeRef"
+            :data="subjectOptions"
+            node-key="code"
+            :props="{ label: 'name', children: 'children' }"
+            highlight-current
+            default-expand-all
+            :expand-on-click-node="false"
+            :filter-node-method="filterSubjectNode"
+            @node-click="handleSubjectClick"
+          >
+            <template #default="{ data }">
+              <span class="subject-node">{{ data.code }} {{ data.name }}</span>
+            </template>
+          </el-tree>
+        </el-scrollbar>
+      </aside>
+
+      <section class="ledger-content">
+        <el-form :model="filters" :inline="true" class="filter-form">
+          <el-form-item label="鏈熼棿:">
+            <el-date-picker v-model="filters.startMonth" type="month" placeholder="寮�濮嬫湀浠�" value-format="YYYY-MM" style="width: 140px;" />
+            <span style="margin: 0 10px;">鑷�</span>
+            <el-date-picker v-model="filters.endMonth" type="month" placeholder="缁撴潫鏈堜唤" value-format="YYYY-MM" style="width: 140px;" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="getTableData">鏌ヨ</el-button>
+            <el-button @click="resetFilters">閲嶇疆</el-button>
+<!--            <el-button @click="handlePrint" icon="Printer">鎵撳嵃</el-button>-->
+            <!-- <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button> -->
+          </el-form-item>
+        </el-form>
+
+        <div class="table_list">
+          <el-table :data="dataList" border style="width: 100%">
+            <el-table-column prop="date" label="鏃ユ湡"/>
+            <!-- <el-table-column prop="voucherNo" label="鍑瘉瀛楀彿" width="120" /> -->
+            <!-- <el-table-column prop="summary" label="鎽樿" min-width="200" show-overflow-tooltip /> -->
+            <el-table-column prop="debit" label="鍊熸柟">
+              <template #default="{ row }">
+                <span v-if="row.debit > 0" class="text-danger">楼{{ formatMoney(row.debit) }}</span>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="credit" label="璐锋柟">
+              <template #default="{ row }">
+                <span v-if="row.credit > 0" class="text-success">楼{{ formatMoney(row.credit) }}</span>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="鏂瑰悜">
+              <template #default="{ row }">
+                <el-tag :type="row.direction === '鍊�' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="浣欓">
+              <template #default="{ row }">
+                <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">楼{{ formatMoney(Math.abs(row.balance)) }}</span>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+
+        <el-empty v-if="!currentSubject" description="璇烽�夋嫨浼氳绉戠洰鏌ヨ" style="margin-top: 50px;" />
+      </section>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, watch, nextTick } from "vue";
+import { ElMessage } from "element-plus";
+import { listAccountSubject } from "@/api/financialManagement/accountSubject";
+import { getGeneralLedger } from "@/api/financialManagement/ledger";
+
+defineOptions({
+  name: "绉戠洰鎬昏处",
+});
+
+const filters = reactive({
+  subject: "",
+  startMonth: "",
+  endMonth: "",
+});
+
+const dataList = ref([]);
+const subjectOptions = ref([]);
+const subjectKeyword = ref("");
+const subjectTreeRef = ref();
+
+const getPreviousMonth = () => {
+  const date = new Date();
+  date.setDate(1);
+  date.setMonth(date.getMonth() - 1);
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, "0");
+  return `${year}-${month}`;
+};
+
+const defaultMonth = getPreviousMonth();
+filters.startMonth = defaultMonth;
+filters.endMonth = defaultMonth;
+
+const fallbackSubjects = [
+  { code: "1001", name: "搴撳瓨鐜伴噾" },
+  { code: "1002", name: "閾惰瀛樻" },
+  { code: "1122", name: "搴旀敹璐︽" },
+  { code: "2202", name: "搴斾粯璐︽" },
+  { code: "6001", name: "涓昏惀涓氬姟鏀跺叆" },
+];
+
+const toTree = (nodes = []) =>
+  nodes
+    .filter(item => item.subjectCode && item.subjectName)
+    .map(item => ({
+      code: item.subjectCode,
+      name: item.subjectName,
+      children: toTree(item.children || []),
+    }));
+
+const findSubject = (options, code) => {
+  for (const item of options) {
+    if (item.code === code) return item;
+    if (item.children && item.children.length > 0) {
+      const found = findSubject(item.children, code);
+      if (found) return found;
+    }
+  }
+  return null;
+};
+
+const currentSubject = computed(() => {
+  if (!filters.subject) return null;
+  return findSubject(subjectOptions.value, filters.subject);
+});
+
+const getFirstSubjectCode = (nodes = []) => {
+  for (const item of nodes) {
+    if (item.code) return item.code;
+    if (item.children && item.children.length > 0) {
+      const childCode = getFirstSubjectCode(item.children);
+      if (childCode) return childCode;
+    }
+  }
+  return "";
+};
+
+const setDefaultSubjectSelection = async () => {
+  const firstCode = getFirstSubjectCode(subjectOptions.value);
+  if (!firstCode) {
+    filters.subject = "";
+    subjectTreeRef.value?.setCurrentKey(null);
+    return;
+  }
+  filters.subject = firstCode;
+  await nextTick();
+  subjectTreeRef.value?.setCurrentKey(firstCode);
+};
+
+const filterSubjectNode = (value, data) => {
+  const keyword = value?.trim();
+  if (!keyword) return true;
+  return `${data.code}${data.name}`.includes(keyword);
+};
+
+watch(subjectKeyword, (value) => {
+  subjectTreeRef.value?.filter(value || "");
+});
+
+const handleSubjectClick = async (data) => {
+  filters.subject = data.code;
+  await getTableData();
+};
+
+const loadSubjectOptions = async () => {
+  let options = [];
+  try {
+    const { data } = await listAccountSubject({
+      current: 1,
+      size: 1000,
+      status: 0,
+    });
+    options = toTree(data?.records || []);
+  } catch (error) {
+    // 鍏ㄥ眬鎷︽埅鍣ㄥ凡鎻愮ず锛屼笅闈㈣蛋鍏滃簳绉戠洰
+  }
+  if (options.length === 0) {
+    options = fallbackSubjects.map(item => ({ ...item, children: [] }));
+  }
+  subjectOptions.value = options;
+  await setDefaultSubjectSelection();
+  if (filters.subject) {
+    await getTableData();
+  }
+};
+
+const formatMoney = (value) => {
+  if (value === undefined || value === null) return "0.00";
+  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+// 鑱旇皟绾﹀畾锛氭�昏处鎺ュ彛杩斿洖琛屾暟缁勶紙rowType/date/voucherNo/summary/debit/credit/direction/balance锛�
+const getTableData = async () => {
+  if (!currentSubject.value) {
+    dataList.value = [];
+    return;
+  }
+  try {
+    const { data } = await getGeneralLedger({
+      subjectCode: currentSubject.value.code,
+      startMonth: filters.startMonth,
+      endMonth: filters.endMonth,
+    });
+    dataList.value = Array.isArray(data) ? data : data?.records || [];
+  } catch (error) {
+    // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+  }
+};
+
+const resetFilters = async () => {
+  filters.startMonth = defaultMonth;
+  filters.endMonth = defaultMonth;
+  dataList.value = [];
+  subjectKeyword.value = "";
+  subjectTreeRef.value?.filter("");
+  await setDefaultSubjectSelection();
+  if (filters.subject) {
+    await getTableData();
+  }
+};
+
+const handlePrint = () => {
+  ElMessage.info("鎵撳嵃鍔熻兘");
+};
+
+const handleOut = () => {
+  ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+onMounted(async () => {
+  await loadSubjectOptions();
+});
+</script>
+
+<style lang="scss" scoped>
+.ledger-layout {
+  display: flex;
+  gap: 16px;
+}
+
+.subject-panel {
+  width: 260px;
+  flex-shrink: 0;
+  padding: 12px;
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+  background-color: #fff;
+}
+
+.subject-tree-scroll {
+  height: 600px;
+  margin-top: 12px;
+}
+
+.subject-node {
+  display: inline-flex;
+  align-items: center;
+}
+
+.ledger-content {
+  flex: 1;
+  min-width: 0;
+}
+
+.filter-form {
+  margin-bottom: 12px;
+}
+
+.text-primary {
+  color: #409eff;
+  font-weight: bold;
+}
+
+.text-success {
+  color: #67c23a;
+  font-weight: bold;
+}
+
+.text-danger {
+  color: #f56c6c;
+  font-weight: bold;
+}
+
+.text-warning {
+  color: #e6a23c;
+  font-weight: bold;
+}
+
+.subject-panel :deep(.el-tree-node__content) {
+  height: 34px;
+}
+
+.subject-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
+  background-color: #f0f7ff;
+}
+</style>
diff --git a/src/views/financialManagement/voucher/index.vue b/src/views/financialManagement/voucher/index.vue
new file mode 100644
index 0000000..b4ee561
--- /dev/null
+++ b/src/views/financialManagement/voucher/index.vue
@@ -0,0 +1,1186 @@
+<template>
+  <div class="app-container">
+    <el-form :model="filters" :inline="true">
+      <el-form-item label="鍑瘉瀛楀彿:">
+        <el-input v-model="filters.voucherNo" placeholder="璇疯緭鍏ュ嚟璇佸瓧鍙�" clearable style="width: 200px;" />
+      </el-form-item>
+      <el-form-item label="鍑瘉鏃ユ湡:">
+        <el-date-picker v-model="filters.dateRange" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange" range-separator="鑷�" start-placeholder="寮�濮嬫棩鏈�" end-placeholder="缁撴潫鏃ユ湡" clearable />
+      </el-form-item>
+      <el-form-item label="鍒跺崟浜�:">
+        <el-select v-model="filters.creator" placeholder="璇烽�夋嫨鍒跺崟浜�" clearable style="width: 150px;">
+          <el-option
+            v-for="item in creatorOptions"
+            :key="item"
+            :label="item"
+            :value="item"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="鐘舵��:">
+        <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+          <el-option label="鏈繃璐�" value="unposted" />
+          <el-option label="宸茶繃璐�" value="posted" />
+          <el-option label="宸蹭綔搴�" value="cancelled" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+        <el-button @click="resetFilters">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="table_list">
+      <div class="actions">
+        <div>
+          <el-statistic title="鍊熸柟鍚堣" :value="totalDebit" :precision="2" prefix="楼" />
+          <el-statistic title="璐锋柟鍚堣" :value="totalCredit" :precision="2" prefix="楼" style="margin-left: 30px;" />
+        </div>
+        <div>
+          <el-button type="primary" @click="add" icon="Plus">鏂板鍑瘉</el-button>
+          <!-- <el-button @click="handleImport" icon="Upload">瀵煎叆</el-button> -->
+          <!-- <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button> -->
+        </div>
+      </div>
+      <PIMTable
+        rowKey="id"
+        :column="columns"
+        :tableData="dataList"
+        :page="{
+          current: pagination.currentPage,
+          size: pagination.pageSize,
+          total: pagination.total,
+        }"
+        @pagination="changePage"
+      >
+        <template #debit="{ row }">
+          <span class="text-danger" v-if="row.debit > 0">楼{{ formatMoney(row.debit) }}</span>
+          <span v-else>-</span>
+        </template>
+        <template #credit="{ row }">
+          <span class="text-success" v-if="row.credit > 0">楼{{ formatMoney(row.credit) }}</span>
+          <span v-else>-</span>
+        </template>
+        <template #status="{ row }">
+          <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
+        </template>
+        <template #operation="{ row }">
+          <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+          <el-button type="primary" link @click="edit(row)" v-if="canEditVoucher(row.status)">缂栬緫</el-button>
+          <el-button type="success" link @click="handlePost(row)" v-if="canEditVoucher(row.status)">杩囪处</el-button>
+          <el-button type="danger" link @click="handleCancel(row)" v-if="canEditVoucher(row.status)">浣滃簾</el-button>
+        </template>
+      </PIMTable>
+    </div>
+
+    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="1200px" @confirm="submitForm" @cancel="dialogVisible = false">
+      <div class="voucher-container">
+        <div class="voucher-header">
+          <h2 class="voucher-title">璁拌处鍑瘉</h2>
+          <div class="voucher-period">{{ form.voucherDate ? form.voucherDate.substring(0, 7) + '鏈�' : '' }}</div>
+        </div>
+        <el-form :model="form" :rules="rules" :disabled="isViewMode" ref="formRef" label-width="0">
+          <div class="voucher-info">
+            <div class="voucher-no-section">
+              <span class="label">鍑瘉瀛楋細</span>
+              <el-select v-model="form.voucherPrefix" :disabled="isViewMode" style="width: 70px;">
+                <el-option label="璁�" value="璁�" />
+                <el-option label="鐜�" value="鐜�" />
+                <el-option label="閾�" value="閾�" />
+                <el-option label="杞�" value="杞�" />
+                <el-option label="鏀�" value="鏀�" />
+                <el-option label="浠�" value="浠�" />
+              </el-select>
+              <el-input v-model="form.voucherNum" :disabled="isViewMode" style="width: 60px;" />
+              <span class="label" style="margin-left: 5px;">鍙�</span>
+            </div>
+            <div class="voucher-date-section">
+              <span class="label">鏃ユ湡锛�</span>
+              <el-date-picker v-model="form.voucherDate" :disabled="isViewMode" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 140px;" />
+            </div>
+            <div class="voucher-attachment-section">
+              <span class="label">闄勪欢锛�</span>
+              <el-input-number v-model="form.attachmentCount" :disabled="isViewMode" :min="0" :controls="false" style="width: 60px;" />
+              <span class="label" style="margin-left: 5px;">寮�</span>
+            </div>
+          </div>
+          <div class="voucher-table">
+            <table class="accounting-voucher">
+              <thead>
+                <tr>
+                  <th class="col-summary" rowspan="2">鎽樿</th>
+                  <th class="col-subject" rowspan="2">浼氳绉戠洰</th>
+                  <th class="col-debit-header" colspan="11">鍊熸柟</th>
+                  <th class="col-credit-header" colspan="11">璐锋柟</th>
+                  <th class="col-action" rowspan="2">鎿嶄綔</th>
+                </tr>
+                <tr class="amount-header">
+                  <th>浜�</th>
+                  <th>鍗�</th>
+                  <th>鐧�</th>
+                  <th>鍗�</th>
+                  <th>涓�</th>
+                  <th>鍗�</th>
+                  <th>鐧�</th>
+                  <th>鍗�</th>
+                  <th>鍏�</th>
+                  <th>瑙�</th>
+                  <th>鍒�</th>
+                  <th>浜�</th>
+                  <th>鍗�</th>
+                  <th>鐧�</th>
+                  <th>鍗�</th>
+                  <th>涓�</th>
+                  <th>鍗�</th>
+                  <th>鐧�</th>
+                  <th>鍗�</th>
+                  <th>鍏�</th>
+                  <th>瑙�</th>
+                  <th>鍒�</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="(entry, rowIndex) in form.entries" :key="rowIndex" @click="selectRow(rowIndex)" :class="{ 'selected-row': selectedRowIndex === rowIndex }">
+                  <td class="col-summary">
+                    <el-input v-model="entry.summary" :disabled="isViewMode" placeholder="璇疯緭鍏ユ憳瑕�" @focus="selectRow(rowIndex)" />
+                  </td>
+                  <td class="col-subject">
+                    <el-tree-select
+                      v-model="entry.subjectCode"
+                      :data="subjectTreeOptions"
+                      :props="subjectTreeSelectProps"
+                      :disabled="isViewMode"
+                      placeholder="閫夋嫨绉戠洰"
+                      filterable
+                      check-strictly
+                      clearable
+                      :render-after-expand="false"
+                      @change="(val) => handleSubjectChange(val, rowIndex)"
+                      @focus="selectRow(rowIndex)"
+                    />
+                    <!-- <div class="subject-name">{{ entry.subjectName }}</div> -->
+                  </td>
+                  <!-- 鍊熸柟11鍒� -->
+                  <template v-if="editingCell.row === rowIndex && editingCell.type === 'debit'">
+                    <td colspan="11" class="debit-input-cell">
+                      <el-input-number ref="amountInputRef" v-model="entry.debit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" :value-on-clear="undefined" size="small" @blur="finishEdit" class="full-width-input" />
+                    </td>
+                  </template>
+                  <template v-else>
+                    <td v-for="(digit, dIndex) in getAmountDigits(entry.debit, 11)" :key="'debit-'+dIndex" class="amount-cell debit-cell" @click="openAmountInput(rowIndex, 'debit')">
+                      <span :class="{ 'text-primary': digit !== '', 'zero': digit === '' }">{{ digit || '' }}</span>
+                    </td>
+                  </template>
+                  <!-- 璐锋柟11鍒� -->
+                  <template v-if="editingCell.row === rowIndex && editingCell.type === 'credit'">
+                    <td colspan="11" class="credit-input-cell">
+                      <el-input-number ref="amountInputRef" v-model="entry.credit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" :value-on-clear="undefined" size="small" @blur="finishEdit" class="full-width-input" />
+                    </td>
+                  </template>
+                  <template v-else>
+                    <td v-for="(digit, dIndex) in getAmountDigits(entry.credit, 11)" :key="'credit-'+dIndex" class="amount-cell credit-cell" @click="openAmountInput(rowIndex, 'credit')">
+                      <span :class="{ 'text-danger': digit !== '', 'zero': digit === '' }">{{ digit || '' }}</span>
+                    </td>
+                  </template>
+                  <td class="col-action">
+                    <el-button type="danger" link size="small" @click="removeEntry(rowIndex)" icon="Delete" :disabled="isViewMode || form.entries.length <= 2">鍒犻櫎</el-button>
+                  </td>
+                </tr>
+                <tr class="total-row">
+                  <td class="col-summary" colspan="2" style="text-align: center; font-weight: bold;">鍚堣锛�</td>
+                  <td v-for="(digit, index) in getAmountDigits(totalDebitEntry, 11)" :key="'total-debit-'+index" class="amount-cell total-debit-cell">
+                    <span :class="{ 'text-primary': digit !== '' }">{{ digit }}</span>
+                  </td>
+                  <td v-for="(digit, index) in getAmountDigits(totalCreditEntry, 11)" :key="'total-credit-'+index" class="amount-cell total-credit-cell">
+                    <span :class="{ 'text-danger': digit !== '' }">{{ digit }}</span>
+                  </td>
+                  <td class="col-action"></td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+          <div class="voucher-toolbar">
+            <el-button type="primary" link @click="addEntry" icon="Plus" :disabled="isViewMode">鏂板琛�</el-button>
+          </div>
+          <div class="voucher-footer">
+            <div class="creator-section">
+              <span class="label">鍒跺崟浜猴細</span>
+              <el-select
+                v-model="form.creator"
+                :disabled="isViewMode"
+                placeholder="璇烽�夋嫨鍒跺崟浜�"
+                filterable
+                clearable
+                style="width: 200px;"
+              >
+                <el-option
+                  v-for="item in creatorOptions"
+                  :key="item"
+                  :label="item"
+                  :value="item"
+                />
+              </el-select>
+            </div>
+          </div>
+          <!-- 闄勪欢鏉愭枡 -->
+          <div class="voucher-attachment-section">
+            <div class="attachment-label">闄勪欢鏉愭枡锛�</div>
+            <el-upload 
+              v-model:file-list="form.attachments" 
+              :action="upload.url" 
+              multiple 
+              ref="fileUpload" 
+              auto-upload
+              :headers="upload.headers" 
+              :before-upload="handleBeforeUpload" 
+              :on-error="handleUploadError"
+              :on-success="handleUploadSuccess" 
+              :on-remove="handleRemove"
+              :on-preview="handlePreview"
+              :disabled="isViewMode">
+              <el-button type="primary" v-if="!isViewMode">涓婁紶</el-button>
+              <template #tip v-if="!isViewMode">
+                <div class="el-upload__tip">
+                  鏂囦欢鏍煎紡鏀寔 doc锛宒ocx锛寈ls锛寈lsx锛宲pt锛宲ptx锛宲df锛宼xt锛寈ml锛宩pg锛宩peg锛宲ng锛実if锛宐mp锛宺ar锛寊ip锛�7z
+                </div>
+              </template>
+            </el-upload>
+          </div>
+        </el-form>
+      </div>
+      <template #footer>
+        <div>
+          <el-button v-if="!isViewMode" type="primary" @click="submitForm" :disabled="!isBalanced">淇濆瓨</el-button>
+          <el-button @click="dialogVisible = false">{{ isViewMode ? '鍏抽棴' : '鍙栨秷' }}</el-button>
+        </div>
+      </template>
+    </FormDialog>
+    <!-- 鏂囦欢棰勮缁勪欢 -->
+    <FilePreview ref="filePreviewRef" />
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, nextTick } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import FilePreview from "@/components/filePreview/index.vue";
+import download from "@/plugins/download.js";
+import { getToken } from "@/utils/auth";
+import useUserStore from "@/store/modules/user";
+import { userListNoPageByTenantId } from "@/api/system/user";
+import { listAccountSubject } from "@/api/financialManagement/accountSubject";
+import {
+  listVoucherPage,
+  addVoucher,
+  updateVoucher,
+  postVoucher,
+  cancelVoucher,
+  getVoucherDetail,
+} from "@/api/financialManagement/voucher";
+
+defineOptions({
+  name: "鍑瘉绠$悊",
+});
+
+const userStore = useUserStore();
+const getDefaultCreator = () => userStore.nickName || userStore.name || "寮犱笁";
+
+const filters = reactive({
+  voucherNo: "",
+  dateRange: [],
+  creator: "",
+  status: "",
+});
+
+const pagination = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+const columns = [
+  { label: "鍑瘉瀛楀彿", prop: "voucherNo", width: "120" },
+  { label: "鍑瘉鏃ユ湡", prop: "voucherDate", width: "120" },
+  { label: "鎽樿", prop: "summary", showOverflowTooltip: true },
+  { label: "鍊熸柟閲戦", prop: "debit", dataType: "slot", slot: "debit" },
+  { label: "璐锋柟閲戦", prop: "credit", dataType: "slot", slot: "credit" },
+  { label: "鍒跺崟浜�", prop: "creator", width: "100" },
+  { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status" },
+  { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "220", fixed: "right" },
+];
+
+const dataList = ref([]);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const dialogMode = ref("add");
+const isEdit = ref(false);
+const currentId = ref(null);
+const isViewMode = computed(() => dialogMode.value === "view");
+const filePreviewRef = ref(null);
+
+// 涓婁紶鐩稿叧閰嶇疆
+const upload = reactive({
+  // 涓婁紶鐨勫湴鍧�
+  url: import.meta.env.VITE_APP_BASE_API + "/file/upload?type=14",
+  // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+  headers: { Authorization: "Bearer " + getToken() },
+});
+const fileUpload = ref(null);
+
+const fallbackSubjectTree = [
+  { subjectCode: "1001", subjectName: "搴撳瓨鐜伴噾", balanceDirection: "鍊熸柟", children: [] },
+  { subjectCode: "1002", subjectName: "閾惰瀛樻", balanceDirection: "鍊熸柟", children: [] },
+  { subjectCode: "1122", subjectName: "搴旀敹璐︽", balanceDirection: "鍊熸柟", children: [] },
+  { subjectCode: "2202", subjectName: "搴斾粯璐︽", balanceDirection: "璐锋柟", children: [] },
+  { subjectCode: "5001", subjectName: "鐢熶骇鎴愭湰", balanceDirection: "鍊熸柟", children: [] },
+  { subjectCode: "6001", subjectName: "涓昏惀涓氬姟鏀跺叆", balanceDirection: "璐锋柟", children: [] },
+  { subjectCode: "6401", subjectName: "涓昏惀涓氬姟鎴愭湰", balanceDirection: "鍊熸柟", children: [] },
+];
+
+const subjectTreeOptions = ref([]);
+const subjectList = ref([]);
+const subjectTreeSelectProps = {
+  children: "children",
+  label: "label",
+  value: "value",
+};
+
+const buildSubjectTreeOptions = (nodes = [], flatList = []) =>
+  (nodes || [])
+    .filter(item => item.subjectCode && item.subjectName)
+    .map(item => {
+      const balanceDirection = item.balanceDirection || "";
+      const flatItem = {
+        code: item.subjectCode,
+        name: item.subjectName,
+        balanceDirection,
+      };
+      flatList.push(flatItem);
+      return {
+        value: flatItem.code,
+        label: `${flatItem.code} ${flatItem.name}${balanceDirection ? ` [${balanceDirection}]` : ""}`,
+        children: buildSubjectTreeOptions(item.children || [], flatList),
+      };
+    });
+
+const createEmptyEntry = () => ({
+  subjectCode: "",
+  subjectName: "",
+  balanceDirection: "",
+  summary: "",
+  debit: undefined,
+  credit: undefined,
+});
+
+const createDefaultForm = () => ({
+  voucherNo: "",
+  voucherPrefix: "璁�",
+  voucherNum: "",
+  voucherDate: "",
+  attachmentCount: 0,
+  attachments: [],
+  entries: [createEmptyEntry(), createEmptyEntry()],
+  creator: getDefaultCreator(),
+  remark: "",
+});
+
+const form = reactive({
+  ...createDefaultForm(),
+});
+
+const userOptions = ref([]);
+
+const creatorOptions = computed(() => {
+  const source = [
+    ...userOptions.value.map(item => item.nickName || item.userName || item.name),
+    getDefaultCreator(),
+    form.creator,
+    filters.creator,
+  ];
+  return [...new Set(source.filter(Boolean))];
+});
+
+const selectedRowIndex = ref(-1);
+const editingCell = reactive({
+  row: -1,
+  type: "",
+});
+const amountInputRef = ref(null);
+
+const isBalanced = computed(() => {
+  return totalDebitEntry.value === totalCreditEntry.value && totalDebitEntry.value > 0;
+});
+
+const rules = {
+  voucherDate: [{ required: true, message: "璇烽�夋嫨鍑瘉鏃ユ湡", trigger: "change" }],
+};
+
+const totalDebit = computed(() => {
+  return dataList.value.reduce((sum, item) => sum + Number(item.debit), 0);
+});
+
+const totalCredit = computed(() => {
+  return dataList.value.reduce((sum, item) => sum + Number(item.credit), 0);
+});
+
+const totalDebitEntry = computed(() => {
+  return form.entries.reduce((sum, item) => sum + Number(item.debit || 0), 0);
+});
+
+const totalCreditEntry = computed(() => {
+  return form.entries.reduce((sum, item) => sum + Number(item.credit || 0), 0);
+});
+
+const formatMoney = (value) => {
+  if (value === undefined || value === null) return "0.00";
+  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const normalizeVoucherStatus = status => String(status || "").toLowerCase();
+
+const canEditVoucher = status => {
+  const key = normalizeVoucherStatus(status);
+  return key === "unposted" || status === "鏈繃璐�";
+};
+
+const getStatusLabel = (status) => {
+  const key = normalizeVoucherStatus(status);
+  const map = { unposted: "鏈繃璐�", posted: "宸茶繃璐�", cancelled: "宸蹭綔搴�" };
+  return map[key] || status;
+};
+
+const getStatusType = (status) => {
+  const key = normalizeVoucherStatus(status);
+  const map = { unposted: "warning", posted: "success", cancelled: "info" };
+  return map[key] || "";
+};
+
+// 鑱旇皟绾﹀畾锛氬垎椤靛弬鏁颁娇鐢� current/size锛屾棩鏈熻寖鍥存媶鍒嗕负 startDate/endDate
+const getTableData = async () => {
+  try {
+    const [startDate, endDate] =
+      filters.dateRange && filters.dateRange.length === 2 ? filters.dateRange : ["", ""];
+    const { data } = await listVoucherPage({
+      current: pagination.currentPage,
+      size: pagination.pageSize,
+      voucherNo: filters.voucherNo,
+      creator: filters.creator,
+      status: filters.status,
+      startDate,
+      endDate,
+    });
+    dataList.value = data?.records || [];
+    pagination.total = Number(data?.total || 0);
+  } catch (error) {
+    // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+  }
+};
+
+// 鍑瘉鍒嗗綍閲岀殑绉戠洰涓嬫媺涓庢�昏处绉戠洰淇濇寔涓�鑷达紝閬垮厤鎻愪氦涓嶅瓨鍦ㄧ鐩�
+const loadSubjectList = async () => {
+  try {
+    const { data } = await listAccountSubject({
+      current: 1,
+      size: 1000,
+      status: 0
+    });
+    const flatList = [];
+    const treeOptions = buildSubjectTreeOptions(data?.records || [], flatList);
+    if (treeOptions.length > 0) {
+      subjectTreeOptions.value = treeOptions;
+      subjectList.value = flatList;
+      return;
+    }
+    const fallbackFlatList = [];
+    subjectTreeOptions.value = buildSubjectTreeOptions(fallbackSubjectTree, fallbackFlatList);
+    subjectList.value = fallbackFlatList;
+  } catch (error) {
+    // 鍏ㄥ眬鎷︽埅鍣ㄥ凡鎻愮ず閿欒锛岃繖閲屼繚鐣欓粯璁ょ鐩綔涓哄厹搴�
+    const fallbackFlatList = [];
+    subjectTreeOptions.value = buildSubjectTreeOptions(fallbackSubjectTree, fallbackFlatList);
+    subjectList.value = fallbackFlatList;
+  }
+};
+
+const loadUserOptions = async () => {
+  try {
+    const { data } = await userListNoPageByTenantId();
+    userOptions.value = Array.isArray(data) ? data : [];
+  } catch (error) {
+    userOptions.value = [];
+  }
+};
+
+const resetFilters = () => {
+  filters.voucherNo = "";
+  filters.dateRange = [];
+  filters.creator = "";
+  filters.status = "";
+  pagination.currentPage = 1;
+  getTableData();
+};
+
+const changePage = ({ current, size }) => {
+  pagination.currentPage = current;
+  pagination.pageSize = size;
+  getTableData();
+};
+
+const addEntry = () => {
+  if (isViewMode.value) {
+    return;
+  }
+  form.entries.push(createEmptyEntry());
+};
+
+const handleAttachmentChange = (fileList) => {
+  form.attachmentCount = fileList?.length || 0;
+};
+
+// 浣跨敤椤圭洰灏佽鐨� filePreview 缁勪欢棰勮鏂囦欢
+const previewFile = (row) => {
+  const url = row.previewURL || row.previewUrl || row.url;
+  if (url && filePreviewRef.value) {
+    filePreviewRef.value.open(url);
+  } else {
+    ElMessage.warning('鏂囦欢鍦板潃鏃犳晥锛屾棤娉曢瑙�');
+  }
+};
+
+// 浣跨敤椤圭洰灏佽鐨� download 鎻掍欢涓嬭浇鏂囦欢
+const downloadFile = (row) => {
+  const url = row.downloadURL || row.downloadUrl || row.url;
+  if (url) {
+    const filename = row.originalFilename || row.name || row.fileName || 'download';
+    download.byUrl(url, filename);
+  } else {
+    ElMessage.warning('鏂囦欢鍦板潃鏃犳晥锛屾棤娉曚笅杞�');
+  }
+};
+
+const selectRow = (index) => {
+  selectedRowIndex.value = index;
+};
+
+const openAmountInput = (index, type) => {
+  if (isViewMode.value) {
+    return;
+  }
+  editingCell.row = index;
+  editingCell.type = type;
+  nextTick(() => {
+    if (amountInputRef.value) {
+      amountInputRef.value.focus();
+    }
+  });
+};
+
+const finishEdit = () => {
+  editingCell.row = -1;
+  editingCell.type = "";
+};
+
+const getAmountDigits = (amount, length) => {
+  if (!amount || amount === 0) {
+    return new Array(length).fill('');
+  }
+
+  const amountStr = Number(amount).toFixed(2);
+  const [intPart, decPart] = amountStr.split('.');
+  const fullAmount = intPart + decPart;
+
+  // 宸﹀~鍏�0鍒版寚瀹氶暱搴�
+  const paddedAmount = fullAmount.padStart(length, '0');
+  const digits = paddedAmount.split('');
+
+  // 鎵惧埌绗竴涓潪闆舵暟瀛楃殑浣嶇疆
+  let firstNonZeroIndex = 0;
+  for (let i = 0; i < digits.length; i++) {
+    if (digits[i] !== '0') {
+      firstNonZeroIndex = i;
+      break;
+    }
+  }
+
+  // 鍙殣钘忓墠瀵奸浂锛堢涓�涓潪闆舵暟瀛椾箣鍓嶇殑闆讹級
+  return digits.map((d, index) => {
+    if (index < firstNonZeroIndex) {
+      return ''; // 鍓嶅闆舵樉绀轰负绌�
+    }
+    return d; // 淇濈暀閲戦涓殑闆�
+  });
+};
+
+const removeEntry = (index) => {
+  if (isViewMode.value) {
+    return;
+  }
+  if (form.entries.length <= 2) {
+    return;
+  }
+  form.entries.splice(index, 1);
+};
+
+const handleSubjectChange = (val, index) => {
+  const subject = subjectList.value.find(item => item.code === val);
+  if (subject) {
+    form.entries[index].subjectName = subject.name;
+    form.entries[index].balanceDirection = subject.balanceDirection || "";
+  } else {
+    form.entries[index].subjectName = "";
+    form.entries[index].balanceDirection = "";
+  }
+};
+
+const add = () => {
+  dialogMode.value = "add";
+  isEdit.value = false;
+  currentId.value = null;
+  dialogTitle.value = "鏂板鍑瘉";
+  const nextNum = String((pagination.total || 0) + 1).padStart(4, "0");
+  Object.assign(form, createDefaultForm(), {
+    voucherPrefix: "璁�",
+    voucherNum: nextNum,
+    voucherNo: `璁�-${nextNum}`,
+    voucherDate: new Date().toISOString().split('T')[0],
+  });
+  selectedRowIndex.value = 0;
+  dialogVisible.value = true;
+};
+
+const openVoucherDialog = async (row, mode = "edit") => {
+  try {
+    dialogMode.value = mode;
+    isEdit.value = mode === "edit";
+    currentId.value = row.id;
+    dialogTitle.value = mode === "view" ? "鏌ョ湅鍑瘉" : "缂栬緫鍑瘉";
+    const { data } = await getVoucherDetail(row.id);
+    const detail = data || row;
+    const parts = (detail.voucherNo || "").split("-");
+    const attachments = detail.salesLedgerFiles || detail.storageBlobVOList || detail.storageBlobDTOs || detail.attachments || [];
+    Object.assign(form, createDefaultForm(), {
+      ...detail,
+      voucherPrefix: parts[0] || "璁�",
+      voucherNum: parts[1] || "",
+      creator: detail.creator || getDefaultCreator(),
+      attachments,
+      entries:
+        detail.entries?.map(item => ({
+          subjectCode: item.subjectCode || "",
+          subjectName: item.subjectName || "",
+          balanceDirection: item.balanceDirection || "",
+          summary: item.summary || "",
+          debit: Number(item.debit || 0),
+          credit: Number(item.credit || 0),
+        })) || [],
+    });
+    if (form.entries.length < 2) {
+      while (form.entries.length < 2) {
+        form.entries.push(createEmptyEntry());
+      }
+    }
+    selectedRowIndex.value = 0;
+    dialogVisible.value = true;
+  } catch (error) {
+    // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+  }
+};
+
+const edit = async row => {
+  await openVoucherDialog(row, "edit");
+};
+
+const view = async row => {
+  await openVoucherDialog(row, "view");
+};
+
+const handlePost = (row) => {
+  ElMessageBox.confirm("纭杩囪处璇ュ嚟璇佸悧锛�", "鎻愮ず", {
+    confirmButtonText: "纭",
+    cancelButtonText: "鍙栨秷",
+    type: "info",
+  }).then(async () => {
+    await postVoucher({ id: row.id });
+    ElMessage.success("杩囪处鎴愬姛");
+    await getTableData();
+  });
+};
+
+const handleCancel = (row) => {
+  ElMessageBox.confirm("纭浣滃簾璇ュ嚟璇佸悧锛�", "鎻愮ず", {
+    confirmButtonText: "纭",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(async () => {
+    await cancelVoucher({ id: row.id });
+    ElMessage.success("浣滃簾鎴愬姛");
+    await getTableData();
+  });
+};
+
+// 鏂囦欢涓婁紶鍓嶆牎楠�
+const handleBeforeUpload = (file) => {
+  const allowedTypes = [
+    'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf', 'txt', 'xml',
+    'jpg', 'jpeg', 'png', 'gif', 'bmp', 'rar', 'zip', '7z'
+  ];
+  const fileExt = file.name.split('.').pop().toLowerCase();
+  const isAllowed = allowedTypes.includes(fileExt);
+  const isLt50M = file.size / 1024 / 1024 < 50;
+
+  if (!isAllowed) {
+    ElMessage.error('鏂囦欢鏍煎紡涓嶆敮鎸侊紒');
+    return false;
+  }
+  if (!isLt50M) {
+    ElMessage.error('鏂囦欢澶у皬涓嶈兘瓒呰繃 50MB锛�');
+    return false;
+  }
+  return true;
+};
+
+// 鏂囦欢涓婁紶鎴愬姛
+const handleUploadSuccess = (response, file, fileList) => {
+  if (response.code === 200) {
+    ElMessage.success('涓婁紶鎴愬姛');
+    // 鏇存柊闄勪欢鍒楄〃锛屼娇鐢ㄥ悗绔繑鍥炵殑鏁版嵁
+    const index = form.attachments.findIndex(item => item.uid === file.uid);
+    if (index !== -1) {
+      const resData = response.data || {};
+      form.attachments[index] = {
+        ...form.attachments[index],
+        ...resData,
+        name: resData.originalFilename || resData.fileName || file.name,
+        url: resData.url || resData.previewUrl || resData.downloadUrl,
+        tempId: resData.tempId || resData.id
+      };
+    }
+  } else {
+    ElMessage.error(response.msg || '涓婁紶澶辫触');
+  }
+};
+
+// 鏂囦欢涓婁紶澶辫触
+const handleUploadError = (error, file, fileList) => {
+  ElMessage.error('鏂囦欢涓婁紶澶辫触锛岃閲嶈瘯');
+  console.error('涓婁紶澶辫触:', error);
+};
+
+// 鏂囦欢绉婚櫎
+const handleRemove = (file, fileList) => {
+  form.attachments = fileList;
+  ElMessage.success('鏂囦欢宸茬Щ闄�');
+};
+
+// 鏂囦欢棰勮
+const handlePreview = (file) => {
+  const fileData = {
+    name: file.name || file.originalFilename || file.fileName,
+    url: file.url || file.previewUrl || file.downloadUrl,
+    id: file.id || file.tempId
+  };
+  if (filePreviewRef.value) {
+    filePreviewRef.value.open(fileData);
+  }
+};
+
+const handleImport = () => {
+  ElMessage.info("瀵煎叆鍔熻兘");
+};
+
+const handleOut = () => {
+  ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+const submitForm = () => {
+  if (isViewMode.value) {
+    dialogVisible.value = false;
+    return;
+  }
+  formRef.value.validate(async valid => {
+    if (valid) {
+      // 鍓嶇疆鏍¢獙锛氫笌鍚庣瑙勫垯瀵归綈锛屽噺灏戞棤鏁堣姹�
+      if (!isBalanced.value) {
+        ElMessage.error("鍊熻捶涓嶅钩琛★紝璇锋鏌ュ垎褰�");
+        return;
+      }
+
+      const validEntries = form.entries.filter(
+        entry => entry.subjectCode && (Number(entry.debit) > 0 || Number(entry.credit) > 0)
+      );
+      if (validEntries.length === 0) {
+        ElMessage.error("璇疯嚦灏戝~鍐欎竴鏉℃湁鏁堝垎褰�");
+        return;
+      }
+
+      const invalidEntry = validEntries.find(
+        entry => Number(entry.debit) > 0 && Number(entry.credit) > 0
+      );
+      if (invalidEntry) {
+        ElMessage.error("鍚屼竴鍒嗗綍涓嶈兘鍚屾椂濉啓鍊熸柟鍜岃捶鏂�");
+        return;
+      }
+
+      const summary = validEntries.find(e => e.debit > 0)?.summary || "";
+
+      // 鎻愬彇闄勪欢鐨� tempFileIds
+      const tempFileIds = (form.attachments || [])
+        .filter(item => item.tempId || item.id)
+        .map(item => item.tempId || item.id);
+
+      const voucherNo = `${form.voucherPrefix}-${form.voucherNum}`;
+      const dataToSave = {
+        voucherNo,
+        voucherDate: form.voucherDate,
+        summary,
+        creator: form.creator,
+        attachmentCount: Number(form.attachmentCount || 0),
+        remark: form.remark,
+        debit: totalDebitEntry.value,
+        credit: totalCreditEntry.value,
+        tempFileIds: tempFileIds,
+        entries: validEntries.map(entry => ({
+          subjectCode: entry.subjectCode,
+          subjectName: entry.subjectName,
+          summary: entry.summary,
+          debit: Number(entry.debit || 0),
+          credit: Number(entry.credit || 0),
+        })),
+      };
+
+      try {
+        if (isEdit.value) {
+          await updateVoucher({
+            id: currentId.value,
+            ...dataToSave,
+          });
+          ElMessage.success("缂栬緫鎴愬姛");
+        } else {
+          await addVoucher(dataToSave);
+          ElMessage.success("鏂板鎴愬姛");
+        }
+        dialogVisible.value = false;
+        await getTableData();
+      } catch (error) {
+        // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+      }
+    }
+  });
+};
+
+onMounted(async () => {
+  await loadUserOptions();
+  await loadSubjectList();
+  await getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+
+  > div:first-child {
+    display: flex;
+    align-items: center;
+  }
+}
+
+.text-success {
+  color: #67c23a;
+  font-weight: bold;
+}
+
+.text-danger {
+  color: #f56c6c;
+  font-weight: bold;
+}
+
+.text-primary {
+  color: #409eff;
+}
+
+.voucher-container {
+  background: #fff;
+  padding: 20px;
+}
+
+.voucher-header {
+  text-align: center;
+  margin-bottom: 15px;
+
+  .voucher-title {
+    font-size: 22px;
+    font-weight: bold;
+    margin: 0 0 5px 0;
+    color: #303133;
+  }
+
+  .voucher-period {
+    font-size: 14px;
+    color: #909399;
+  }
+}
+
+.voucher-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+  padding: 0 10px;
+
+  .label {
+    font-size: 14px;
+    color: #606266;
+  }
+
+  .voucher-no-section,
+  .voucher-date-section,
+  .voucher-attachment-section {
+    display: flex;
+    align-items: center;
+  }
+}
+
+.voucher-attachment-section {
+  margin-top: 15px;
+  padding: 0 10px;
+
+  .attachment-label {
+    font-size: 14px;
+    color: #606266;
+    margin-bottom: 10px;
+    font-weight: 500;
+  }
+
+  :deep(.el-upload-list) {
+    max-height: 200px;
+    overflow-y: auto;
+  }
+}
+
+.voucher-table {
+  border: 1px solid #dcdfe6;
+  border-right: none;
+  margin-bottom: 15px;
+}
+
+.accounting-voucher {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 13px;
+
+  th,
+  td {
+    border: 1px solid #dcdfe6;
+    text-align: center;
+    padding: 0;
+    height: 36px;
+  }
+
+  & th:last-child,
+  & td:last-child {
+    border-right: none !important;
+  }
+
+  thead {
+    background-color: #f5f7fa;
+
+    th {
+      font-weight: normal;
+      color: #606266;
+      font-size: 12px;
+    }
+
+    .col-summary,
+    .col-subject {
+      font-weight: bold;
+      font-size: 13px;
+    }
+
+    .col-debit-header,
+    .col-credit-header {
+      background-color: #ecf5ff;
+      color: #409eff;
+      font-weight: bold;
+    }
+  }
+
+  .amount-header {
+    th {
+      font-size: 11px;
+      padding: 2px 0;
+      background-color: #f5f7fa;
+    }
+  }
+
+  .col-summary {
+    width: 160px;
+    min-width: 160px;
+  }
+
+  .col-subject {
+    width: 180px;
+    min-width: 180px;
+  }
+
+  .col-action {
+    width: 60px;
+    min-width: 60px;
+    text-align: center;
+  }
+
+  .amount-cell {
+    width: 24px;
+    min-width: 24px;
+    max-width: 24px;
+    padding: 0;
+    font-size: 13px;
+    font-family: 'Courier New', monospace;
+    cursor: pointer;
+    text-align: center;
+
+    &:hover {
+      background-color: #f5f7fa;
+    }
+
+    span {
+      display: block;
+      width: 100%;
+      height: 100%;
+      line-height: 36px;
+
+      &.zero {
+        color: #c0c4cc;
+      }
+    }
+  }
+
+  .debit-input-cell,
+  .credit-input-cell {
+    padding: 0;
+    background-color: #ecf5ff;
+
+    .full-width-input {
+      width: 100%;
+
+      :deep(.el-input__wrapper) {
+        padding: 0 10px;
+        box-shadow: none;
+        background-color: transparent;
+      }
+
+      input {
+        text-align: right;
+        font-size: 14px;
+        height: 34px;
+      }
+    }
+  }
+
+  tbody {
+    tr {
+      &:hover {
+        background-color: #f5f7fa;
+      }
+
+      &.selected-row {
+        background-color: #ecf5ff;
+      }
+    }
+
+    td {
+      .el-input {
+        .el-input__wrapper {
+          box-shadow: none;
+          padding: 0 5px;
+        }
+
+        input {
+          text-align: center;
+          height: 34px;
+        }
+      }
+
+      .el-select {
+        width: 100%;
+
+        .el-input__wrapper {
+          box-shadow: none;
+        }
+
+        input {
+          text-align: center;
+          height: 34px;
+        }
+      }
+    }
+
+    .col-summary {
+      .el-input input {
+        text-align: left;
+        padding-left: 10px;
+      }
+    }
+
+    .col-subject {
+      position: relative;
+
+      .el-select,
+      .el-tree-select {
+        .el-input input {
+          font-size: 12px;
+        }
+      }
+
+      .subject-name {
+        font-size: 11px;
+        color: #909399;
+        margin-top: 2px;
+        line-height: 1.2;
+      }
+    }
+  }
+
+  .total-row {
+    background-color: #fdf6ec;
+
+    td {
+      font-weight: bold;
+    }
+
+    .total-cell {
+      background-color: #fdf6ec;
+      font-weight: bold;
+    }
+  }
+}
+
+.voucher-toolbar {
+  display: flex;
+  justify-content: flex-start;
+  padding: 10px 0;
+  margin-top: 5px;
+}
+
+.voucher-footer {
+  display: flex;
+  justify-content: flex-end;
+  padding: 0 10px;
+  margin-top: 10px;
+
+  .creator-section {
+    .label {
+      font-size: 14px;
+      color: #606266;
+    }
+  }
+}
+
+:deep(.el-dialog__body) {
+  padding: 10px 20px;
+}
+</style>

--
Gitblit v1.9.3