<template>
|
<div class="app-container">
|
<el-form :model="filters" :inline="true">
|
<el-form-item label="凭证字号:">
|
<el-input v-model="filters.voucherNo" placeholder="请输入凭证字号" clearable style="width: 200px;" />
|
</el-form-item>
|
<el-form-item label="凭证日期:">
|
<el-date-picker v-model="filters.dateRange" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" clearable />
|
</el-form-item>
|
<el-form-item label="制单人:">
|
<el-select v-model="filters.creator" placeholder="请选择制单人" clearable style="width: 150px;">
|
<el-option
|
v-for="item in creatorOptions"
|
:key="item"
|
:label="item"
|
:value="item"
|
/>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="状态:">
|
<el-select v-model="filters.status" placeholder="请选择状态" clearable style="width: 150px;">
|
<el-option label="未过账" value="unposted" />
|
<el-option label="已过账" value="posted" />
|
<el-option label="已作废" value="cancelled" />
|
</el-select>
|
</el-form-item>
|
<el-form-item>
|
<el-button type="primary" @click="getTableData">搜索</el-button>
|
<el-button @click="resetFilters">重置</el-button>
|
</el-form-item>
|
</el-form>
|
<div class="table_list">
|
<div class="actions">
|
<div>
|
<el-statistic title="借方合计" :value="totalDebit" precision="2" prefix="¥" />
|
<el-statistic title="贷方合计" :value="totalCredit" precision="2" prefix="¥" style="margin-left: 30px;" />
|
</div>
|
<div>
|
<el-button type="primary" @click="add" icon="Plus">新增凭证</el-button>
|
<el-button @click="handleImport" icon="Upload">导入</el-button>
|
<el-button @click="handleOut" icon="Download">导出</el-button>
|
</div>
|
</div>
|
<PIMTable
|
rowKey="id"
|
:column="columns"
|
:tableData="dataList"
|
:page="{
|
current: pagination.currentPage,
|
size: pagination.pageSize,
|
total: pagination.total,
|
}"
|
@pagination="changePage"
|
>
|
<template #debit="{ row }">
|
<span class="text-danger" v-if="row.debit > 0">¥{{ formatMoney(row.debit) }}</span>
|
<span v-else>-</span>
|
</template>
|
<template #credit="{ row }">
|
<span class="text-success" v-if="row.credit > 0">¥{{ formatMoney(row.credit) }}</span>
|
<span v-else>-</span>
|
</template>
|
<template #status="{ row }">
|
<el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
|
</template>
|
<template #operation="{ row }">
|
<el-button type="primary" link @click="view(row)">查看</el-button>
|
<el-button type="primary" link @click="edit(row)" v-if="canEditVoucher(row.status)">编辑</el-button>
|
<el-button type="success" link @click="handlePost(row)" v-if="canEditVoucher(row.status)">过账</el-button>
|
<el-button type="danger" link @click="handleCancel(row)" v-if="canEditVoucher(row.status)">作废</el-button>
|
</template>
|
</PIMTable>
|
</div>
|
|
<FormDialog :title="dialogTitle" v-model="dialogVisible" width="1200px" @confirm="submitForm" @cancel="dialogVisible = false">
|
<div class="voucher-container">
|
<div class="voucher-header">
|
<h2 class="voucher-title">记账凭证</h2>
|
<div class="voucher-period">{{ form.voucherDate ? form.voucherDate.substring(0, 7) + '期' : '' }}</div>
|
</div>
|
<el-form :model="form" :rules="rules" :disabled="isViewMode" ref="formRef" label-width="0">
|
<div class="voucher-info">
|
<div class="voucher-no-section">
|
<span class="label">凭证字:</span>
|
<el-select v-model="form.voucherPrefix" :disabled="isViewMode" style="width: 70px;">
|
<el-option label="记" value="记" />
|
</el-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>
|
<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>
|
<div>
|
<el-button v-if="!isViewMode" type="primary" @click="submitForm" :disabled="!isBalanced">保存</el-button>
|
<el-button @click="dialogVisible = false">{{ isViewMode ? '关闭' : '取消' }}</el-button>
|
</div>
|
</template>
|
</FormDialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted, computed, nextTick } from "vue";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
import FormDialog from "@/components/Dialog/FormDialog.vue";
|
import useUserStore from "@/store/modules/user";
|
import { userListNoPageByTenantId } from "@/api/system/user";
|
import { listAccountSubject } from "@/api/financialManagement/accountSubject";
|
import {
|
listVoucherPage,
|
addVoucher,
|
updateVoucher,
|
postVoucher,
|
cancelVoucher,
|
getVoucherDetail,
|
} from "@/api/financialManagement/voucher";
|
|
defineOptions({
|
name: "凭证管理",
|
});
|
|
const userStore = useUserStore();
|
const getDefaultCreator = () => userStore.nickName || userStore.name || "张三";
|
|
const filters = reactive({
|
voucherNo: "",
|
dateRange: [],
|
creator: "",
|
status: "",
|
});
|
|
const pagination = reactive({
|
currentPage: 1,
|
pageSize: 10,
|
total: 0,
|
});
|
|
const columns = [
|
{ label: "凭证字号", prop: "voucherNo", width: "120" },
|
{ label: "凭证日期", prop: "voucherDate", width: "120" },
|
{ label: "摘要", prop: "summary", showOverflowTooltip: true },
|
{ label: "借方金额", prop: "debit", dataType: "slot", slot: "debit" },
|
{ label: "贷方金额", prop: "credit", dataType: "slot", slot: "credit" },
|
{ label: "制单人", prop: "creator", width: "100" },
|
{ label: "状态", prop: "status", dataType: "slot", slot: "status" },
|
{ label: "操作", prop: "operation", dataType: "slot", slot: "operation", width: "220", fixed: "right" },
|
];
|
|
const dataList = ref([]);
|
const dialogVisible = ref(false);
|
const dialogTitle = ref("");
|
const formRef = ref(null);
|
const dialogMode = ref("add");
|
const isEdit = ref(false);
|
const currentId = ref(null);
|
const isViewMode = computed(() => dialogMode.value === "view");
|
|
const fallbackSubjectTree = [
|
{ subjectCode: "1001", subjectName: "库存现金", balanceDirection: "借方", children: [] },
|
{ subjectCode: "1002", subjectName: "银行存款", balanceDirection: "借方", children: [] },
|
{ subjectCode: "1122", subjectName: "应收账款", balanceDirection: "借方", children: [] },
|
{ subjectCode: "2202", subjectName: "应付账款", balanceDirection: "贷方", children: [] },
|
{ subjectCode: "5001", subjectName: "生产成本", balanceDirection: "借方", children: [] },
|
{ subjectCode: "6001", subjectName: "主营业务收入", balanceDirection: "贷方", children: [] },
|
{ subjectCode: "6401", subjectName: "主营业务成本", balanceDirection: "借方", children: [] },
|
];
|
|
const subjectTreeOptions = ref([]);
|
const subjectList = ref([]);
|
const subjectTreeSelectProps = {
|
children: "children",
|
label: "label",
|
value: "value",
|
};
|
|
const buildSubjectTreeOptions = (nodes = [], flatList = []) =>
|
(nodes || [])
|
.filter(item => item.subjectCode && item.subjectName)
|
.map(item => {
|
const balanceDirection = item.balanceDirection || "";
|
const flatItem = {
|
code: item.subjectCode,
|
name: item.subjectName,
|
balanceDirection,
|
};
|
flatList.push(flatItem);
|
return {
|
value: flatItem.code,
|
label: `${flatItem.code} ${flatItem.name}${balanceDirection ? ` [${balanceDirection}]` : ""}`,
|
children: buildSubjectTreeOptions(item.children || [], flatList),
|
};
|
});
|
|
const createEmptyEntry = () => ({
|
subjectCode: "",
|
subjectName: "",
|
balanceDirection: "",
|
summary: "",
|
debit: 0,
|
credit: 0,
|
});
|
|
const createDefaultForm = () => ({
|
voucherNo: "",
|
voucherPrefix: "记",
|
voucherNum: "",
|
voucherDate: "",
|
attachmentCount: 0,
|
entries: [createEmptyEntry(), createEmptyEntry()],
|
creator: getDefaultCreator(),
|
remark: "",
|
});
|
|
const form = reactive({
|
...createDefaultForm(),
|
});
|
|
const userOptions = ref([]);
|
|
const creatorOptions = computed(() => {
|
const source = [
|
...userOptions.value.map(item => item.nickName || item.userName || item.name),
|
getDefaultCreator(),
|
form.creator,
|
filters.creator,
|
];
|
return [...new Set(source.filter(Boolean))];
|
});
|
|
const selectedRowIndex = ref(-1);
|
const editingCell = reactive({
|
row: -1,
|
type: "",
|
});
|
const amountInputRef = ref(null);
|
|
const isBalanced = computed(() => {
|
return totalDebitEntry.value === totalCreditEntry.value && totalDebitEntry.value > 0;
|
});
|
|
const rules = {
|
voucherDate: [{ required: true, message: "请选择凭证日期", trigger: "change" }],
|
};
|
|
const totalDebit = computed(() => {
|
return dataList.value.reduce((sum, item) => sum + Number(item.debit), 0);
|
});
|
|
const totalCredit = computed(() => {
|
return dataList.value.reduce((sum, item) => sum + Number(item.credit), 0);
|
});
|
|
const totalDebitEntry = computed(() => {
|
return form.entries.reduce((sum, item) => sum + Number(item.debit || 0), 0);
|
});
|
|
const totalCreditEntry = computed(() => {
|
return form.entries.reduce((sum, item) => sum + Number(item.credit || 0), 0);
|
});
|
|
const formatMoney = (value) => {
|
if (value === undefined || value === null) return "0.00";
|
return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
};
|
|
const normalizeVoucherStatus = status => String(status || "").toLowerCase();
|
|
const canEditVoucher = status => {
|
const key = normalizeVoucherStatus(status);
|
return key === "unposted" || status === "未过账";
|
};
|
|
const getStatusLabel = (status) => {
|
const key = normalizeVoucherStatus(status);
|
const map = { unposted: "未过账", posted: "已过账", cancelled: "已作废" };
|
return map[key] || status;
|
};
|
|
const getStatusType = (status) => {
|
const key = normalizeVoucherStatus(status);
|
const map = { unposted: "warning", posted: "success", cancelled: "info" };
|
return map[key] || "";
|
};
|
|
// 联调约定:分页参数使用 current/size,日期范围拆分为 startDate/endDate
|
const getTableData = async () => {
|
try {
|
const [startDate, endDate] =
|
filters.dateRange && filters.dateRange.length === 2 ? filters.dateRange : ["", ""];
|
const { data } = await listVoucherPage({
|
current: pagination.currentPage,
|
size: pagination.pageSize,
|
voucherNo: filters.voucherNo,
|
creator: filters.creator,
|
status: filters.status,
|
startDate,
|
endDate,
|
});
|
dataList.value = data?.records || [];
|
pagination.total = Number(data?.total || 0);
|
} catch (error) {
|
// 提示由全局请求拦截器处理,这里仅防止未捕获异常
|
}
|
};
|
|
// 凭证分录里的科目下拉与总账科目保持一致,避免提交不存在科目
|
const loadSubjectList = async () => {
|
try {
|
const { data } = await listAccountSubject({
|
current: 1,
|
size: 1000,
|
status: 0
|
});
|
const flatList = [];
|
const treeOptions = buildSubjectTreeOptions(data?.records || [], flatList);
|
if (treeOptions.length > 0) {
|
subjectTreeOptions.value = treeOptions;
|
subjectList.value = flatList;
|
return;
|
}
|
const fallbackFlatList = [];
|
subjectTreeOptions.value = buildSubjectTreeOptions(fallbackSubjectTree, fallbackFlatList);
|
subjectList.value = fallbackFlatList;
|
} catch (error) {
|
// 全局拦截器已提示错误,这里保留默认科目作为兜底
|
const fallbackFlatList = [];
|
subjectTreeOptions.value = buildSubjectTreeOptions(fallbackSubjectTree, fallbackFlatList);
|
subjectList.value = fallbackFlatList;
|
}
|
};
|
|
const loadUserOptions = async () => {
|
try {
|
const { data } = await userListNoPageByTenantId();
|
userOptions.value = Array.isArray(data) ? data : [];
|
} catch (error) {
|
userOptions.value = [];
|
}
|
};
|
|
const resetFilters = () => {
|
filters.voucherNo = "";
|
filters.dateRange = [];
|
filters.creator = "";
|
filters.status = "";
|
pagination.currentPage = 1;
|
getTableData();
|
};
|
|
const changePage = ({ current, size }) => {
|
pagination.currentPage = current;
|
pagination.pageSize = size;
|
getTableData();
|
};
|
|
const addEntry = () => {
|
if (isViewMode.value) {
|
return;
|
}
|
form.entries.push(createEmptyEntry());
|
};
|
|
const selectRow = (index) => {
|
selectedRowIndex.value = index;
|
};
|
|
const openAmountInput = (index, type) => {
|
if (isViewMode.value) {
|
return;
|
}
|
editingCell.row = index;
|
editingCell.type = type;
|
nextTick(() => {
|
if (amountInputRef.value) {
|
amountInputRef.value.focus();
|
}
|
});
|
};
|
|
const finishEdit = () => {
|
editingCell.row = -1;
|
editingCell.type = "";
|
};
|
|
const getAmountDigits = (amount, length) => {
|
if (!amount || amount === 0) {
|
return new Array(length).fill('');
|
}
|
|
const amountStr = Number(amount).toFixed(2);
|
const [intPart, decPart] = amountStr.split('.');
|
const fullAmount = intPart + decPart;
|
|
// 左填充0到指定长度
|
const paddedAmount = fullAmount.padStart(length, '0');
|
const digits = paddedAmount.split('');
|
|
// 找到第一个非零数字的位置
|
let firstNonZeroIndex = 0;
|
for (let i = 0; i < digits.length; i++) {
|
if (digits[i] !== '0') {
|
firstNonZeroIndex = i;
|
break;
|
}
|
}
|
|
// 只隐藏前导零(第一个非零数字之前的零)
|
return digits.map((d, index) => {
|
if (index < firstNonZeroIndex) {
|
return ''; // 前导零显示为空
|
}
|
return d; // 保留金额中的零
|
});
|
};
|
|
const removeEntry = (index) => {
|
if (isViewMode.value) {
|
return;
|
}
|
if (form.entries.length <= 2) {
|
return;
|
}
|
form.entries.splice(index, 1);
|
};
|
|
const handleSubjectChange = (val, index) => {
|
const subject = subjectList.value.find(item => item.code === val);
|
if (subject) {
|
form.entries[index].subjectName = subject.name;
|
form.entries[index].balanceDirection = subject.balanceDirection || "";
|
} else {
|
form.entries[index].subjectName = "";
|
form.entries[index].balanceDirection = "";
|
}
|
};
|
|
const add = () => {
|
dialogMode.value = "add";
|
isEdit.value = false;
|
currentId.value = null;
|
dialogTitle.value = "新增凭证";
|
const nextNum = String((pagination.total || 0) + 1).padStart(4, "0");
|
Object.assign(form, createDefaultForm(), {
|
voucherPrefix: "记",
|
voucherNum: nextNum,
|
voucherNo: `记-${nextNum}`,
|
voucherDate: new Date().toISOString().split('T')[0],
|
});
|
selectedRowIndex.value = 0;
|
dialogVisible.value = true;
|
};
|
|
const openVoucherDialog = async (row, mode = "edit") => {
|
try {
|
dialogMode.value = mode;
|
isEdit.value = mode === "edit";
|
currentId.value = row.id;
|
dialogTitle.value = mode === "view" ? "查看凭证" : "编辑凭证";
|
const { data } = await getVoucherDetail(row.id);
|
const detail = data || row;
|
const parts = (detail.voucherNo || "").split("-");
|
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 edit = async row => {
|
await openVoucherDialog(row, "edit");
|
};
|
|
const view = async row => {
|
await openVoucherDialog(row, "view");
|
};
|
|
const handlePost = (row) => {
|
ElMessageBox.confirm("确认过账该凭证吗?", "提示", {
|
confirmButtonText: "确认",
|
cancelButtonText: "取消",
|
type: "info",
|
}).then(async () => {
|
await postVoucher({ id: row.id });
|
ElMessage.success("过账成功");
|
await getTableData();
|
});
|
};
|
|
const handleCancel = (row) => {
|
ElMessageBox.confirm("确认作废该凭证吗?", "提示", {
|
confirmButtonText: "确认",
|
cancelButtonText: "取消",
|
type: "warning",
|
}).then(async () => {
|
await cancelVoucher({ id: row.id });
|
ElMessage.success("作废成功");
|
await getTableData();
|
});
|
};
|
|
const handleImport = () => {
|
ElMessage.info("导入功能");
|
};
|
|
const handleOut = () => {
|
ElMessage.success("导出成功");
|
};
|
|
const submitForm = () => {
|
if (isViewMode.value) {
|
dialogVisible.value = false;
|
return;
|
}
|
formRef.value.validate(async valid => {
|
if (valid) {
|
// 前置校验:与后端规则对齐,减少无效请求
|
if (!isBalanced.value) {
|
ElMessage.error("借贷不平衡,请检查分录");
|
return;
|
}
|
|
const validEntries = form.entries.filter(
|
entry => entry.subjectCode && (Number(entry.debit) > 0 || Number(entry.credit) > 0)
|
);
|
if (validEntries.length === 0) {
|
ElMessage.error("请至少填写一条有效分录");
|
return;
|
}
|
|
const invalidEntry = validEntries.find(
|
entry => Number(entry.debit) > 0 && Number(entry.credit) > 0
|
);
|
if (invalidEntry) {
|
ElMessage.error("同一分录不能同时填写借方和贷方");
|
return;
|
}
|
|
const summary = validEntries.find(e => e.debit > 0)?.summary || "";
|
|
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(async () => {
|
await loadUserOptions();
|
await loadSubjectList();
|
await getTableData();
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
.actions {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 15px;
|
|
> div:first-child {
|
display: flex;
|
align-items: center;
|
}
|
}
|
|
.text-success {
|
color: #67c23a;
|
font-weight: bold;
|
}
|
|
.text-danger {
|
color: #f56c6c;
|
font-weight: bold;
|
}
|
|
.text-primary {
|
color: #409eff;
|
}
|
|
.voucher-container {
|
background: #fff;
|
padding: 20px;
|
}
|
|
.voucher-header {
|
text-align: center;
|
margin-bottom: 15px;
|
|
.voucher-title {
|
font-size: 22px;
|
font-weight: bold;
|
margin: 0 0 5px 0;
|
color: #303133;
|
}
|
|
.voucher-period {
|
font-size: 14px;
|
color: #909399;
|
}
|
}
|
|
.voucher-info {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 15px;
|
padding: 0 10px;
|
|
.label {
|
font-size: 14px;
|
color: #606266;
|
}
|
|
.voucher-no-section,
|
.voucher-date-section,
|
.voucher-attachment-section {
|
display: flex;
|
align-items: center;
|
}
|
}
|
|
.voucher-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>
|