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