From 4ab0be7d4441f378add1f242b168d80fb27e65fe Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期五, 22 五月 2026 17:57:44 +0800
Subject: [PATCH] OA部分查询条件变更

---
 src/views/financialManagement/voucher/index.vue |  534 ++++++++++++++++++++++++++++++++++++++++++++--------------
 1 files changed, 404 insertions(+), 130 deletions(-)

diff --git a/src/views/financialManagement/voucher/index.vue b/src/views/financialManagement/voucher/index.vue
index 817185c..1aa6f69 100644
--- a/src/views/financialManagement/voucher/index.vue
+++ b/src/views/financialManagement/voucher/index.vue
@@ -9,9 +9,12 @@
       </el-form-item>
       <el-form-item label="鍒跺崟浜�:">
         <el-select v-model="filters.creator" placeholder="璇烽�夋嫨鍒跺崟浜�" clearable style="width: 150px;">
-          <el-option label="寮犱笁" value="寮犱笁" />
-          <el-option label="鏉庡洓" value="鏉庡洓" />
-          <el-option label="鐜嬩簲" value="鐜嬩簲" />
+          <el-option
+            v-for="item in creatorOptions"
+            :key="item"
+            :label="item"
+            :value="item"
+          />
         </el-select>
       </el-form-item>
       <el-form-item label="鐘舵��:">
@@ -29,13 +32,13 @@
     <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;" />
+          <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>
+          <!-- <el-button @click="handleImport" icon="Upload">瀵煎叆</el-button> -->
+          <!-- <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button> -->
         </div>
       </div>
       <PIMTable
@@ -62,9 +65,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>
@@ -75,25 +78,29 @@
           <h2 class="voucher-title">璁拌处鍑瘉</h2>
           <div class="voucher-period">{{ form.voucherDate ? form.voucherDate.substring(0, 7) + '鏈�' : '' }}</div>
         </div>
-        <el-form :model="form" :rules="rules" ref="formRef" label-width="0">
+        <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" style="width: 70px;">
+              <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" style="width: 60px;" />
+              <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" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 140px;" />
+              <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" :min="0" :controls="false" style="width: 60px;" />
+              <el-input-number v-model="form.attachmentCount" :disabled="isViewMode" :min="0" :controls="false" style="width: 60px;" />
               <span class="label" style="margin-left: 5px;">寮�</span>
-              <el-button type="primary" link style="margin-left: 10px;">涓婁紶鏂囦欢</el-button>
             </div>
           </div>
           <div class="voucher-table">
@@ -134,18 +141,28 @@
               <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" placeholder="璇疯緭鍏ユ憳瑕�" @focus="selectRow(rowIndex)" />
+                    <el-input v-model="entry.summary" :disabled="isViewMode" 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>
-                    <div class="subject-name">{{ entry.subjectName }}</div>
+                    <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" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" />
+                      <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>
@@ -156,7 +173,7 @@
                   <!-- 璐锋柟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" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" />
+                      <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>
@@ -165,7 +182,7 @@
                     </td>
                   </template>
                   <td class="col-action">
-                    <el-button type="danger" link size="small" @click="removeEntry(rowIndex)" icon="Delete" :disabled="form.entries.length <= 2">鍒犻櫎</el-button>
+                    <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">
@@ -182,22 +199,68 @@
             </table>
           </div>
           <div class="voucher-toolbar">
-            <el-button type="primary" link @click="addEntry" icon="Plus">鏂板琛�</el-button>
+            <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">鍒跺崟浜猴細{{ form.creator }}</span>
+              <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>
+          <!-- 缂栬緫妯″紡锛氫娇鐢� AttachmentUploadFile 涓婁紶缁勪欢 -->
+          <div class="voucher-attachment-upload" v-if="!isViewMode">
+            <div class="attachment-label">闄勪欢涓婁紶锛�</div>
+            <AttachmentUploadFile
+              v-model:fileList="form.attachments"
+              :disabled="isViewMode"
+              :limit="10"
+              :fileSize="50"
+              buttonText="鐐瑰嚮涓婁紶闄勪欢"
+              @change="handleAttachmentChange"
+            />
+          </div>
         </el-form>
+        <!-- 鏌ョ湅妯″紡锛氬睍绀洪檮浠跺垪琛紙鏀惧湪 el-form 澶栭潰锛岄伩鍏嶈 disabled锛� -->
+        <div class="voucher-attachment-upload" v-if="isViewMode && form.attachments?.length">
+          <div class="attachment-label">闄勪欢鍒楄〃锛�</div>
+          <el-table :data="form.attachments" border class="attachment-table">
+            <el-table-column label="闄勪欢鍚嶇О" show-overflow-tooltip>
+              <template #default="scope">
+                {{ scope.row.originalFilename || scope.row.name || scope.row.fileName || '鏈懡鍚嶆枃浠�' }}
+              </template>
+            </el-table-column>
+            <el-table-column fixed="right" label="鎿嶄綔" width="150" align="center">
+              <template #default="scope">
+                <el-button link type="primary" size="small" @click="previewFile(scope.row)">棰勮</el-button>
+                <el-button link type="primary" size="small" @click="downloadFile(scope.row)">涓嬭浇</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
       </div>
       <template #footer>
         <div>
-          <el-button type="primary" @click="submitForm" :disabled="!isBalanced">淇濆瓨</el-button>
-          <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+          <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>
 
@@ -205,10 +268,28 @@
 import { ref, reactive, onMounted, computed, nextTick } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
 import FormDialog from "@/components/Dialog/FormDialog.vue";
+import AttachmentUploadFile from "@/components/AttachmentUpload/file/index.vue";
+import FileList from "@/components/Dialog/FileList.vue";
+import FilePreview from "@/components/filePreview/index.vue";
+import download from "@/plugins/download.js";
+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: "",
@@ -227,39 +308,94 @@
   { 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([]);
 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 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: undefined,
+  credit: undefined,
+});
+
+const createDefaultForm = () => ({
   voucherNo: "",
   voucherPrefix: "璁�",
   voucherNum: "",
   voucherDate: "",
   attachmentCount: 0,
-  entries: [],
-  creator: "寮犱笁",
+  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);
@@ -276,12 +412,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 +434,79 @@
   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);
+};
+
+const loadUserOptions = async () => {
+  try {
+    const { data } = await userListNoPageByTenantId();
+    userOptions.value = Array.isArray(data) ? data : [];
+  } catch (error) {
+    userOptions.value = [];
   }
-  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 +525,35 @@
 };
 
 const addEntry = () => {
-  form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 });
+  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) => {
@@ -356,6 +561,9 @@
 };
 
 const openAmountInput = (index, type) => {
+  if (isViewMode.value) {
+    return;
+  }
   editingCell.row = index;
   editingCell.type = type;
   nextTick(() => {
@@ -402,65 +610,86 @@
 };
 
 const removeEntry = (index) => {
+  if (isViewMode.value) {
+    return;
+  }
+  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 = () => {
+  dialogMode.value = "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 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.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) {
+    // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
   }
-  selectedRowIndex.value = 0;
-  dialogVisible.value = true;
 };
 
-const view = (row) => {
-  ElMessage.info(`鏌ョ湅鍑瘉: ${row.voucherNo}`);
+const edit = async row => {
+  await openVoucherDialog(row, "edit");
+};
+
+const view = async row => {
+  await openVoucherDialog(row, "view");
 };
 
 const handlePost = (row) => {
@@ -468,13 +697,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 +709,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 +725,80 @@
 };
 
 const submitForm = () => {
-  formRef.value.validate((valid) => {
+  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(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,
+        storageBlobDTOs: form.attachments || [],
+        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 loadUserOptions();
+  await loadSubjectList();
+  await getTableData();
 });
 </script>
 
@@ -610,6 +868,21 @@
   .voucher-attachment-section {
     display: flex;
     align-items: center;
+  }
+}
+
+.voucher-attachment-upload {
+  margin-top: 15px;
+  padding: 0 10px;
+
+  .attachment-label {
+    font-size: 14px;
+    color: #606266;
+    margin-bottom: 10px;
+  }
+
+  .attachment-table {
+    border-radius: 4px;
   }
 }
 
@@ -780,7 +1053,8 @@
     .col-subject {
       position: relative;
 
-      .el-select {
+      .el-select,
+      .el-tree-select {
         .el-input input {
           font-size: 12px;
         }

--
Gitblit v1.9.3