From 14363b1ae7cb0d730158ec8dfbee55a85b2fc09f Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 12 五月 2026 15:23:17 +0800
Subject: [PATCH] feat(financial): 实现财务模块数据接口联调

---
 src/views/financialManagement/voucher/index.vue |  333 +++++++++++++++++++++++++++++++++++++-----------------
 1 files changed, 227 insertions(+), 106 deletions(-)

diff --git a/src/views/financialManagement/voucher/index.vue b/src/views/financialManagement/voucher/index.vue
index 817185c..2d34f5c 100644
--- a/src/views/financialManagement/voucher/index.vue
+++ b/src/views/financialManagement/voucher/index.vue
@@ -62,9 +62,9 @@
         </template>
         <template #operation="{ row }">
           <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
-          <el-button type="primary" link @click="edit(row)" v-if="row.status === 'unposted'">缂栬緫</el-button>
-          <el-button type="success" link @click="handlePost(row)" v-if="row.status === 'unposted'">杩囪处</el-button>
-          <el-button type="danger" link @click="handleCancel(row)" v-if="row.status === 'unposted'">浣滃簾</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>
@@ -137,9 +137,18 @@
                     <el-input v-model="entry.summary" placeholder="璇疯緭鍏ユ憳瑕�" @focus="selectRow(rowIndex)" />
                   </td>
                   <td class="col-subject">
-                    <el-select v-model="entry.subjectCode" placeholder="閫夋嫨绉戠洰" filterable @change="(val) => handleSubjectChange(val, rowIndex)" @focus="selectRow(rowIndex)">
-                      <el-option v-for="item in subjectList" :key="item.code" :label="item.code + item.name" :value="item.code" />
-                    </el-select>
+                    <el-tree-select
+                      v-model="entry.subjectCode"
+                      :data="subjectTreeOptions"
+                      :props="subjectTreeSelectProps"
+                      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鍒� -->
@@ -205,6 +214,15 @@
 import { ref, reactive, onMounted, computed, nextTick } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
 import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { listAccountSubject } from "@/api/financialManagement/accountSubject";
+import {
+  listVoucherPage,
+  addVoucher,
+  updateVoucher,
+  postVoucher,
+  cancelVoucher,
+  getVoucherDetail,
+} from "@/api/financialManagement/voucher";
 
 defineOptions({
   name: "鍑瘉绠$悊",
@@ -227,11 +245,11 @@
   { label: "鍑瘉瀛楀彿", prop: "voucherNo", width: "120" },
   { label: "鍑瘉鏃ユ湡", prop: "voucherDate", width: "120" },
   { label: "鎽樿", prop: "summary", showOverflowTooltip: true },
-  { label: "鍊熸柟閲戦", prop: "debit", slot: "debit" },
-  { label: "璐锋柟閲戦", prop: "credit", slot: "credit" },
+  { label: "鍊熸柟閲戦", prop: "debit", dataType: "slot", slot: "debit" },
+  { label: "璐锋柟閲戦", prop: "credit", dataType: "slot", slot: "credit" },
   { label: "鍒跺崟浜�", prop: "creator", width: "100" },
-  { label: "鐘舵��", prop: "status", slot: "status" },
-  { label: "鎿嶄綔", prop: "operation", slot: "operation", width: "220", fixed: "right" },
+  { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status" },
+  { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "220", fixed: "right" },
 ];
 
 const dataList = ref([]);
@@ -241,25 +259,64 @@
 const isEdit = ref(false);
 const currentId = ref(null);
 
-const subjectList = [
-  { code: "1001", name: "搴撳瓨鐜伴噾" },
-  { code: "1002", name: "閾惰瀛樻" },
-  { code: "1122", name: "搴旀敹璐︽" },
-  { code: "2202", name: "搴斾粯璐︽" },
-  { code: "5001", name: "鐢熶骇鎴愭湰" },
-  { code: "6001", name: "涓昏惀涓氬姟鏀跺叆" },
-  { code: "6401", name: "涓昏惀涓氬姟鎴愭湰" },
+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 form = reactive({
+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: 0,
+  credit: 0,
+});
+
+const createDefaultForm = () => ({
   voucherNo: "",
   voucherPrefix: "璁�",
   voucherNum: "",
   voucherDate: "",
   attachmentCount: 0,
-  entries: [],
+  entries: [createEmptyEntry(), createEmptyEntry()],
   creator: "寮犱笁",
   remark: "",
+});
+
+const form = reactive({
+  ...createDefaultForm(),
 });
 
 const selectedRowIndex = ref(-1);
@@ -276,12 +333,6 @@
 const rules = {
   voucherDate: [{ required: true, message: "璇烽�夋嫨鍑瘉鏃ユ湡", trigger: "change" }],
 };
-
-const mockData = [
-  { id: 1, voucherNo: "璁�-0001", voucherDate: "2024-01-15", summary: "閿�鍞敹鍏�", debit: 5650, credit: 5650, creator: "寮犱笁", status: "posted", entries: [{ subjectCode: "1002", subjectName: "閾惰瀛樻", summary: "閿�鍞敹鍏�", debit: 5650, credit: 0 }, { subjectCode: "6001", subjectName: "涓昏惀涓氬姟鏀跺叆", summary: "閿�鍞敹鍏�", debit: 0, credit: 5000 }, { subjectCode: "2221", subjectName: "搴斾氦绋庤垂", summary: "閿�椤圭◣棰�", debit: 0, credit: 650 }] },
-  { id: 2, voucherNo: "璁�-0002", voucherDate: "2024-01-16", summary: "閲囪喘鍘熸潗鏂�", debit: 9040, credit: 9040, creator: "鏉庡洓", status: "unposted", entries: [{ subjectCode: "5001", subjectName: "鐢熶骇鎴愭湰", summary: "閲囪喘鍘熸潗鏂�", debit: 8000, credit: 0 }, { subjectCode: "2221", subjectName: "搴斾氦绋庤垂", summary: "杩涢」绋庨", debit: 1040, credit: 0 }, { subjectCode: "2202", subjectName: "搴斾粯璐︽", summary: "閲囪喘鍘熸潗鏂�", debit: 0, credit: 9040 }] },
-  { id: 3, voucherNo: "璁�-0003", voucherDate: "2024-01-18", summary: "鏀粯璐ф", debit: 5000, credit: 5000, creator: "寮犱笁", status: "posted", entries: [{ subjectCode: "2202", subjectName: "搴斾粯璐︽", summary: "鏀粯璐ф", debit: 5000, credit: 0 }, { subjectCode: "1002", subjectName: "閾惰瀛樻", summary: "鏀粯璐ф", debit: 0, credit: 5000 }] },
-];
 
 const totalDebit = computed(() => {
   return dataList.value.reduce((sum, item) => sum + Number(item.debit), 0);
@@ -304,32 +355,70 @@
   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[status] || status;
+  return map[key] || status;
 };
 
 const getStatusType = (status) => {
+  const key = normalizeVoucherStatus(status);
   const map = { unposted: "warning", posted: "success", cancelled: "info" };
-  return map[status] || "";
+  return map[key] || "";
 };
 
-const getTableData = () => {
-  let result = [...mockData];
-  if (filters.voucherNo) {
-    result = result.filter(item => item.voucherNo.includes(filters.voucherNo));
+// 鑱旇皟绾﹀畾锛氬垎椤靛弬鏁颁娇鐢� 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) {
+    // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
   }
-  if (filters.dateRange && filters.dateRange.length === 2) {
-    result = result.filter(item => item.voucherDate >= filters.dateRange[0] && item.voucherDate <= filters.dateRange[1]);
+};
+
+// 鍑瘉鍒嗗綍閲岀殑绉戠洰涓嬫媺涓庢�昏处绉戠洰淇濇寔涓�鑷达紝閬垮厤鎻愪氦涓嶅瓨鍦ㄧ鐩�
+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;
   }
-  if (filters.creator) {
-    result = result.filter(item => item.creator === filters.creator);
-  }
-  if (filters.status) {
-    result = result.filter(item => item.status === filters.status);
-  }
-  pagination.total = result.length;
-  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
 };
 
 const resetFilters = () => {
@@ -348,7 +437,7 @@
 };
 
 const addEntry = () => {
-  form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 });
+  form.entries.push(createEmptyEntry());
 };
 
 const selectRow = (index) => {
@@ -402,61 +491,69 @@
 };
 
 const removeEntry = (index) => {
+  if (form.entries.length <= 2) {
+    return;
+  }
   form.entries.splice(index, 1);
-  calculateTotal();
 };
 
 const handleSubjectChange = (val, index) => {
-  const subject = subjectList.find(item => item.code === val);
+  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 calculateTotal = () => {
-  // 鑷姩璁$畻锛岀敱computed灞炴�у鐞�
 };
 
 const add = () => {
   isEdit.value = false;
+  currentId.value = null;
   dialogTitle.value = "鏂板鍑瘉";
-  const nextNum = String(mockData.length + 1).padStart(2, "0");
-  Object.assign(form, {
-    voucherNo: "璁�-" + nextNum,
+  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],
-    attachmentCount: 0,
-    entries: [
-      { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 },
-      { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 },
-      { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 },
-      { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 },
-    ],
-    creator: "寮犱笁",
-    remark: "",
   });
   selectedRowIndex.value = 0;
   dialogVisible.value = true;
 };
 
-const edit = (row) => {
-  isEdit.value = true;
-  currentId.value = row.id;
-  dialogTitle.value = "缂栬緫鍑瘉";
-  const parts = row.voucherNo.split('-');
-  Object.assign(form, {
-    ...row,
-    voucherPrefix: parts[0] || '璁�',
-    voucherNum: parts[1] || '',
-  });
-  if (form.entries.length < 4) {
-    while (form.entries.length < 4) {
-      form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 });
+const edit = async row => {
+  try {
+    isEdit.value = true;
+    currentId.value = row.id;
+    dialogTitle.value = "缂栬緫鍑瘉";
+    const { data } = await getVoucherDetail(row.id);
+    const detail = data || row;
+    const parts = (detail.voucherNo || "").split("-");
+    Object.assign(form, createDefaultForm(), detail, {
+      voucherPrefix: parts[0] || "璁�",
+      voucherNum: parts[1] || "",
+      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) {
+    // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
   }
-  selectedRowIndex.value = 0;
-  dialogVisible.value = true;
 };
 
 const view = (row) => {
@@ -468,13 +565,10 @@
     confirmButtonText: "纭",
     cancelButtonText: "鍙栨秷",
     type: "info",
-  }).then(() => {
-    const index = mockData.findIndex(item => item.id === row.id);
-    if (index !== -1) {
-      mockData[index].status = "posted";
-    }
+  }).then(async () => {
+    await postVoucher({ id: row.id });
     ElMessage.success("杩囪处鎴愬姛");
-    getTableData();
+    await getTableData();
   });
 };
 
@@ -483,13 +577,10 @@
     confirmButtonText: "纭",
     cancelButtonText: "鍙栨秷",
     type: "warning",
-  }).then(() => {
-    const index = mockData.findIndex(item => item.id === row.id);
-    if (index !== -1) {
-      mockData[index].status = "cancelled";
-    }
+  }).then(async () => {
+    await cancelVoucher({ id: row.id });
     ElMessage.success("浣滃簾鎴愬姛");
-    getTableData();
+    await getTableData();
   });
 };
 
@@ -502,45 +593,74 @@
 };
 
 const submitForm = () => {
-  formRef.value.validate((valid) => {
+  formRef.value.validate(async valid => {
     if (valid) {
+      // 鍓嶇疆鏍¢獙锛氫笌鍚庣瑙勫垯瀵归綈锛屽噺灏戞棤鏁堣姹�
       if (!isBalanced.value) {
         ElMessage.error("鍊熻捶涓嶅钩琛★紝璇锋鏌ュ垎褰�");
         return;
       }
 
-      const validEntries = form.entries.filter(e => e.subjectCode && (e.debit > 0 || e.credit > 0));
+      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 || "";
 
       const voucherNo = `${form.voucherPrefix}-${form.voucherNum}`;
       const dataToSave = {
-        ...form,
         voucherNo,
+        voucherDate: form.voucherDate,
         summary,
+        creator: form.creator,
+        attachmentCount: Number(form.attachmentCount || 0),
+        remark: form.remark,
         debit: totalDebitEntry.value,
         credit: totalCreditEntry.value,
-        entries: validEntries,
+        entries: validEntries.map(entry => ({
+          subjectCode: entry.subjectCode,
+          subjectName: entry.subjectName,
+          summary: entry.summary,
+          debit: Number(entry.debit || 0),
+          credit: Number(entry.credit || 0),
+        })),
       };
 
-      if (isEdit.value) {
-        const index = mockData.findIndex(item => item.id === currentId.value);
-        if (index !== -1) {
-          mockData[index] = { ...mockData[index], ...dataToSave };
+      try {
+        if (isEdit.value) {
+          await updateVoucher({
+            id: currentId.value,
+            ...dataToSave,
+          });
+          ElMessage.success("缂栬緫鎴愬姛");
+        } else {
+          await addVoucher(dataToSave);
+          ElMessage.success("鏂板鎴愬姛");
         }
-        ElMessage.success("缂栬緫鎴愬姛");
-      } else {
-        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
-        mockData.push({ id: newId, ...dataToSave, status: "unposted" });
-        ElMessage.success("鏂板鎴愬姛");
+        dialogVisible.value = false;
+        await getTableData();
+      } catch (error) {
+        // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
       }
-      dialogVisible.value = false;
-      getTableData();
     }
   });
 };
 
-onMounted(() => {
-  getTableData();
+onMounted(async () => {
+  await loadSubjectList();
+  await getTableData();
 });
 </script>
 
@@ -780,7 +900,8 @@
     .col-subject {
       position: relative;
 
-      .el-select {
+      .el-select,
+      .el-tree-select {
         .el-input input {
           font-size: 12px;
         }

--
Gitblit v1.9.3