| | |
| | | </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" ref="formRef" label-width="0"> |
| | | <div class="voucher-info"> |
| | | <div class="voucher-no-section"> |
| | | <span class="label">凭证字:</span> |
| | | <el-select v-model="form.voucherPrefix" style="width: 70px;"> |
| | | <el-option label="记" value="记" /> |
| | | </el-select> |
| | | <el-input v-model="form.voucherNum" style="width: 60px;" /> |
| | | <span class="label" style="margin-left: 5px;">号</span> |
| | | </div> |
| | | <div class="voucher-date-section"> |
| | | <span class="label">日期:</span> |
| | | <el-date-picker v-model="form.voucherDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 140px;" /> |
| | | </div> |
| | | <div class="voucher-attachment-section"> |
| | | <span class="label">附件:</span> |
| | | <el-input-number v-model="form.attachmentCount" :min="0" :controls="false" style="width: 60px;" /> |
| | | <span class="label" style="margin-left: 5px;">张</span> |
| | | <el-button type="primary" link style="margin-left: 10px;">上传文件</el-button> |
| | | </div> |
| | | </div> |
| | | </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" placeholder="请输入摘要" @focus="selectRow(rowIndex)" /> |
| | | </td> |
| | | <td class="col-subject"> |
| | | <el-select v-model="entry.subjectCode" placeholder="选择科目" filterable @change="(val) => handleSubjectChange(val, rowIndex)" @focus="selectRow(rowIndex)"> |
| | | <el-option v-for="item in subjectList" :key="item.code" :label="item.code + item.name" :value="item.code" /> |
| | | </el-select> |
| | | <div class="subject-name">{{ entry.subjectName }}</div> |
| | | </td> |
| | | <!-- 借方11列 --> |
| | | <template v-if="editingCell.row === rowIndex && editingCell.type === 'debit'"> |
| | | <td colspan="11" class="debit-input-cell"> |
| | | <el-input-number ref="amountInputRef" v-model="entry.debit" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" /> |
| | | </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" :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="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">新增行</el-button> |
| | | </div> |
| | | <div class="voucher-footer"> |
| | | <div class="creator-section"> |
| | | <span class="label">制单人:{{ form.creator }}</span> |
| | | </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 type="primary" @click="submitForm" :disabled="!isBalanced">保存</el-button> |
| | | <el-button @click="dialogVisible = false">取消</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"; |
| | | |
| | | defineOptions({ |
| | | name: "凭证管理", |
| | |
| | | |
| | | const form = reactive({ |
| | | voucherNo: "", |
| | | voucherPrefix: "记", |
| | | voucherNum: "", |
| | | voucherDate: "", |
| | | attachmentCount: 0, |
| | | entries: [], |
| | | creator: "张三", |
| | | remark: "", |
| | | }); |
| | | |
| | | 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 = { |
| | |
| | | form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }); |
| | | }; |
| | | |
| | | const selectRow = (index) => { |
| | | selectedRowIndex.value = index; |
| | | }; |
| | | |
| | | const openAmountInput = (index, type) => { |
| | | 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) => { |
| | | form.entries.splice(index, 1); |
| | | calculateTotal(); |
| | |
| | | const add = () => { |
| | | isEdit.value = false; |
| | | dialogTitle.value = "新增凭证"; |
| | | const nextNum = String(mockData.length + 1).padStart(2, "0"); |
| | | Object.assign(form, { |
| | | voucherNo: "记-" + String(mockData.length + 1).padStart(4, "0"), |
| | | voucherNo: "记-" + nextNum, |
| | | voucherPrefix: "记", |
| | | voucherNum: nextNum, |
| | | voucherDate: new Date().toISOString().split('T')[0], |
| | | attachmentCount: 0, |
| | | entries: [{ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }], |
| | | entries: [ |
| | | { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }, |
| | | { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }, |
| | | { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }, |
| | | { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }, |
| | | ], |
| | | creator: "张三", |
| | | remark: "", |
| | | }); |
| | | selectedRowIndex.value = 0; |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | |
| | | isEdit.value = true; |
| | | currentId.value = row.id; |
| | | dialogTitle.value = "编辑凭证"; |
| | | Object.assign(form, row); |
| | | const parts = row.voucherNo.split('-'); |
| | | Object.assign(form, { |
| | | ...row, |
| | | voucherPrefix: parts[0] || '记', |
| | | voucherNum: parts[1] || '', |
| | | }); |
| | | if (form.entries.length < 4) { |
| | | while (form.entries.length < 4) { |
| | | form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }); |
| | | } |
| | | } |
| | | selectedRowIndex.value = 0; |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | |
| | | const submitForm = () => { |
| | | formRef.value.validate((valid) => { |
| | | if (valid) { |
| | | if (totalDebitEntry.value !== totalCreditEntry.value) { |
| | | if (!isBalanced.value) { |
| | | ElMessage.error("借贷不平衡,请检查分录"); |
| | | return; |
| | | } |
| | | const summary = form.entries.find(e => e.debit > 0)?.summary || ""; |
| | | |
| | | const validEntries = form.entries.filter(e => e.subjectCode && (e.debit > 0 || e.credit > 0)); |
| | | const summary = validEntries.find(e => e.debit > 0)?.summary || ""; |
| | | |
| | | const voucherNo = `${form.voucherPrefix}-${form.voucherNum}`; |
| | | const dataToSave = { |
| | | ...form, |
| | | voucherNo, |
| | | summary, |
| | | debit: totalDebitEntry.value, |
| | | credit: totalCreditEntry.value, |
| | | entries: validEntries, |
| | | }; |
| | | |
| | | 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 }; |
| | | mockData[index] = { ...mockData[index], ...dataToSave }; |
| | | } |
| | | 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" }); |
| | | mockData.push({ id: newId, ...dataToSave, status: "unposted" }); |
| | | ElMessage.success("新增成功"); |
| | | } |
| | | dialogVisible.value = false; |
| | |
| | | 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-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> |