From dacc95761cf7090c628fc37a5d4f8bb825ccbbb0 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期六, 16 五月 2026 15:41:45 +0800
Subject: [PATCH] 企业新闻和通知公告

---
 src/views/financialManagement/voucher/index.vue |  935 ++++++++++++++++++++++++++++++++++++++++++++++++----------
 1 files changed, 771 insertions(+), 164 deletions(-)

diff --git a/src/views/financialManagement/voucher/index.vue b/src/views/financialManagement/voucher/index.vue
index 440336f..03c0856 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="鐘舵��:">
@@ -62,98 +65,192 @@
         </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>
 
-    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="900px" append-to-body>
-      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
-        <el-row :gutter="20">
-          <el-col :span="8">
-            <el-form-item label="鍑瘉瀛楀彿" prop="voucherNo">
-              <el-input v-model="form.voucherNo" placeholder="绯荤粺鑷姩鐢熸垚" disabled />
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item label="鍑瘉鏃ユ湡" prop="voucherDate">
-              <el-date-picker v-model="form.voucherDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item label="闄勪欢寮犳暟" prop="attachmentCount">
-              <el-input-number v-model="form.attachmentCount" :min="0" style="width: 100%;" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-form-item label="鍑瘉鍒嗗綍" prop="entries">
-          <el-table :data="form.entries" border style="width: 100%">
-            <el-table-column type="index" label="搴忓彿" width="60" />
-            <el-table-column prop="subjectCode" label="绉戠洰缂栫爜" width="120">
-              <template #default="{ $index }">
-                <el-select v-model="form.entries[$index].subjectCode" placeholder="閫夋嫨绉戠洰" filterable style="width: 100%;" @change="(val) => handleSubjectChange(val, $index)">
-                  <el-option v-for="item in subjectList" :key="item.code" :label="item.code" :value="item.code" />
-                </el-select>
-              </template>
-            </el-table-column>
-            <el-table-column prop="subjectName" label="绉戠洰鍚嶇О" width="150">
-              <template #default="{ $index }">
-                <el-input v-model="form.entries[$index].subjectName" disabled />
-              </template>
-            </el-table-column>
-            <el-table-column prop="summary" label="鎽樿">
-              <template #default="{ $index }">
-                <el-input v-model="form.entries[$index].summary" placeholder="璇疯緭鍏ユ憳瑕�" />
-              </template>
-            </el-table-column>
-            <el-table-column prop="debit" label="鍊熸柟閲戦" width="130">
-              <template #default="{ $index }">
-                <el-input-number v-model="form.entries[$index].debit" :min="0" :precision="2" style="width: 100%;" @change="calculateTotal" />
-              </template>
-            </el-table-column>
-            <el-table-column prop="credit" label="璐锋柟閲戦" width="130">
-              <template #default="{ $index }">
-                <el-input-number v-model="form.entries[$index].credit" :min="0" :precision="2" style="width: 100%;" @change="calculateTotal" />
-              </template>
-            </el-table-column>
-            <el-table-column label="鎿嶄綔" width="80">
-              <template #default="{ $index }">
-                <el-button type="danger" link @click="removeEntry($index)">鍒犻櫎</el-button>
-              </template>
-            </el-table-column>
-          </el-table>
-          <div style="display: flex; justify-content: space-between; margin-top: 10px;">
-            <el-button type="primary" link @click="addEntry">+ 娣诲姞鍒嗗綍</el-button>
-            <div>
-              <span style="margin-right: 20px;">鍚堣: 鍊熸柟 <span :class="totalDebitEntry === totalCreditEntry ? 'text-success' : 'text-danger'">楼{{ formatMoney(totalDebitEntry) }}</span></span>
-              <span>璐锋柟 <span :class="totalDebitEntry === totalCreditEntry ? 'text-success' : 'text-danger'">楼{{ formatMoney(totalCreditEntry) }}</span></span>
+    <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-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>
+              <el-button type="primary" link :disabled="isViewMode" style="margin-left: 10px;">涓婁紶鏂囦欢</el-button>
             </div>
           </div>
-        </el-form-item>
-        <el-form-item label="鍒跺崟浜�" prop="creator">
-          <el-input v-model="form.creator" disabled />
-        </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>
+          <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" 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" 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>
+        </el-form>
+      </div>
       <template #footer>
-        <el-button @click="dialogVisible = false">鍙栨秷</el-button>
-        <el-button type="primary" @click="submitForm" :disabled="totalDebitEntry !== totalCreditEntry">纭畾</el-button>
+        <div>
+          <el-button v-if="!isViewMode" type="primary" @click="submitForm" :disabled="!isBalanced">淇濆瓨</el-button>
+          <el-button @click="dialogVisible = false">{{ isViewMode ? '鍏抽棴' : '鍙栨秷' }}</el-button>
+        </div>
       </template>
-    </el-dialog>
+    </FormDialog>
   </div>
 </template>
 
 <script setup>
-import { ref, reactive, onMounted, computed } from "vue";
+import { ref, reactive, onMounted, computed, nextTick } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+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: "",
@@ -172,48 +269,108 @@
   { 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 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: [],
-  creator: "寮犱笁",
+  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 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);
@@ -236,32 +393,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 = () => {
@@ -280,49 +484,143 @@
 };
 
 const addEntry = () => {
-  form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 });
+  if (isViewMode.value) {
+    return;
+  }
+  form.entries.push(createEmptyEntry());
+};
+
+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);
-  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 = "鏂板鍑瘉";
-  Object.assign(form, {
-    voucherNo: "璁�-" + String(mockData.length + 1).padStart(4, "0"),
+  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 }],
-    creator: "寮犱笁",
-    remark: "",
   });
+  selectedRowIndex.value = 0;
   dialogVisible.value = true;
 };
 
-const edit = (row) => {
-  isEdit.value = true;
-  currentId.value = row.id;
-  dialogTitle.value = "缂栬緫鍑瘉";
-  Object.assign(form, row);
-  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("-");
+    Object.assign(form, createDefaultForm(), detail, {
+      voucherPrefix: parts[0] || "璁�",
+      voucherNum: parts[1] || "",
+      creator: detail.creator || getDefaultCreator(),
+      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 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) => {
@@ -330,13 +628,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();
   });
 };
 
@@ -345,13 +640,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();
   });
 };
 
@@ -364,32 +656,79 @@
 };
 
 const submitForm = () => {
-  formRef.value.validate((valid) => {
+  if (isViewMode.value) {
+    dialogVisible.value = false;
+    return;
+  }
+  formRef.value.validate(async valid => {
     if (valid) {
-      if (totalDebitEntry.value !== totalCreditEntry.value) {
+      // 鍓嶇疆鏍¢獙锛氫笌鍚庣瑙勫垯瀵归綈锛屽噺灏戞棤鏁堣姹�
+      if (!isBalanced.value) {
         ElMessage.error("鍊熻捶涓嶅钩琛★紝璇锋鏌ュ垎褰�");
         return;
       }
-      const summary = form.entries.find(e => e.debit > 0)?.summary || "";
-      if (isEdit.value) {
-        const index = mockData.findIndex(item => item.id === currentId.value);
-        if (index !== -1) {
-          mockData[index] = { ...mockData[index], ...form, summary, debit: totalDebitEntry.value, credit: totalCreditEntry.value };
-        }
-        ElMessage.success("缂栬緫鎴愬姛");
-      } else {
-        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
-        mockData.push({ id: newId, ...form, summary, debit: totalDebitEntry.value, credit: totalCreditEntry.value, status: "unposted" });
-        ElMessage.success("鏂板鎴愬姛");
+
+      const validEntries = form.entries.filter(
+        entry => entry.subjectCode && (Number(entry.debit) > 0 || Number(entry.credit) > 0)
+      );
+      if (validEntries.length === 0) {
+        ElMessage.error("璇疯嚦灏戝~鍐欎竴鏉℃湁鏁堝垎褰�");
+        return;
       }
-      dialogVisible.value = false;
-      getTableData();
+
+      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 = {
+        voucherNo,
+        voucherDate: form.voucherDate,
+        summary,
+        creator: form.creator,
+        attachmentCount: Number(form.attachmentCount || 0),
+        remark: form.remark,
+        debit: totalDebitEntry.value,
+        credit: totalCreditEntry.value,
+        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(() => {
-  getTableData();
+onMounted(async () => {
+  await loadUserOptions();
+  await loadSubjectList();
+  await getTableData();
 });
 </script>
 
@@ -415,4 +754,272 @@
   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-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