| | |
| | | </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="状态:"> |
| | |
| | | <div class="table_list"> |
| | | <div class="actions"> |
| | | <div> |
| | | <el-statistic title="借方合计" :value="totalDebit" precision="2" prefix="¥" /> |
| | | <el-statistic title="贷方合计" :value="totalCredit" precision="2" prefix="¥" style="margin-left: 30px;" /> |
| | | <el-statistic title="借方合计" :value="totalDebit" :precision="2" prefix="¥" /> |
| | | <el-statistic title="贷方合计" :value="totalCredit" :precision="2" prefix="¥" style="margin-left: 30px;" /> |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="add" icon="Plus">新增凭证</el-button> |
| | | <el-button @click="handleImport" icon="Upload">导入</el-button> |
| | | <el-button @click="handleOut" icon="Download">导出</el-button> |
| | | <!-- <el-button @click="handleImport" icon="Upload">导入</el-button> --> |
| | | <!-- <el-button @click="handleOut" icon="Download">导出</el-button> --> |
| | | </div> |
| | | </div> |
| | | <PIMTable |
| | |
| | | </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> |
| | |
| | | <h2 class="voucher-title">记账凭证</h2> |
| | | <div class="voucher-period">{{ form.voucherDate ? form.voucherDate.substring(0, 7) + '期' : '' }}</div> |
| | | </div> |
| | | <el-form :model="form" :rules="rules" ref="formRef" label-width="0"> |
| | | <el-form :model="form" :rules="rules" :disabled="isViewMode" ref="formRef" label-width="0"> |
| | | <div class="voucher-info"> |
| | | <div class="voucher-no-section"> |
| | | <span class="label">凭证字:</span> |
| | | <el-select v-model="form.voucherPrefix" style="width: 70px;"> |
| | | <el-select v-model="form.voucherPrefix" :disabled="isViewMode" style="width: 70px;"> |
| | | <el-option label="记" value="记" /> |
| | | <el-option label="现" value="现" /> |
| | | <el-option label="银" value="银" /> |
| | | <el-option label="转" value="转" /> |
| | | <el-option label="收" value="收" /> |
| | | <el-option label="付" value="付" /> |
| | | </el-select> |
| | | <el-input v-model="form.voucherNum" style="width: 60px;" /> |
| | | <el-input v-model="form.voucherNum" :disabled="isViewMode" style="width: 60px;" /> |
| | | <span class="label" style="margin-left: 5px;">号</span> |
| | | </div> |
| | | <div class="voucher-date-section"> |
| | | <span class="label">日期:</span> |
| | | <el-date-picker v-model="form.voucherDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 140px;" /> |
| | | <el-date-picker v-model="form.voucherDate" :disabled="isViewMode" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 140px;" /> |
| | | </div> |
| | | <div class="voucher-attachment-section"> |
| | | <span class="label">附件:</span> |
| | | <el-input-number v-model="form.attachmentCount" :min="0" :controls="false" style="width: 60px;" /> |
| | | <el-input-number v-model="form.attachmentCount" :disabled="isViewMode" :min="0" :controls="false" style="width: 60px;" /> |
| | | <span class="label" style="margin-left: 5px;">张</span> |
| | | <el-button type="primary" link style="margin-left: 10px;">上传文件</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="voucher-table"> |
| | |
| | | <tbody> |
| | | <tr v-for="(entry, rowIndex) in form.entries" :key="rowIndex" @click="selectRow(rowIndex)" :class="{ 'selected-row': selectedRowIndex === rowIndex }"> |
| | | <td class="col-summary"> |
| | | <el-input v-model="entry.summary" placeholder="请输入摘要" @focus="selectRow(rowIndex)" /> |
| | | <el-input v-model="entry.summary" :disabled="isViewMode" placeholder="请输入摘要" @focus="selectRow(rowIndex)" /> |
| | | </td> |
| | | <td class="col-subject"> |
| | | <el-select v-model="entry.subjectCode" placeholder="选择科目" filterable @change="(val) => handleSubjectChange(val, rowIndex)" @focus="selectRow(rowIndex)"> |
| | | <el-option v-for="item in subjectList" :key="item.code" :label="item.code + item.name" :value="item.code" /> |
| | | </el-select> |
| | | <div class="subject-name">{{ entry.subjectName }}</div> |
| | | <el-tree-select |
| | | v-model="entry.subjectCode" |
| | | :data="subjectTreeOptions" |
| | | :props="subjectTreeSelectProps" |
| | | :disabled="isViewMode" |
| | | placeholder="选择科目" |
| | | filterable |
| | | check-strictly |
| | | clearable |
| | | :render-after-expand="false" |
| | | @change="(val) => handleSubjectChange(val, rowIndex)" |
| | | @focus="selectRow(rowIndex)" |
| | | /> |
| | | <!-- <div class="subject-name">{{ entry.subjectName }}</div> --> |
| | | </td> |
| | | <!-- 借方11列 --> |
| | | <template v-if="editingCell.row === rowIndex && editingCell.type === 'debit'"> |
| | | <td colspan="11" class="debit-input-cell"> |
| | | <el-input-number ref="amountInputRef" v-model="entry.debit" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" /> |
| | | <el-input-number ref="amountInputRef" v-model="entry.debit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" :value-on-clear="undefined" size="small" @blur="finishEdit" class="full-width-input" /> |
| | | </td> |
| | | </template> |
| | | <template v-else> |
| | |
| | | <!-- 贷方11列 --> |
| | | <template v-if="editingCell.row === rowIndex && editingCell.type === 'credit'"> |
| | | <td colspan="11" class="credit-input-cell"> |
| | | <el-input-number ref="amountInputRef" v-model="entry.credit" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" /> |
| | | <el-input-number ref="amountInputRef" v-model="entry.credit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" :value-on-clear="undefined" size="small" @blur="finishEdit" class="full-width-input" /> |
| | | </td> |
| | | </template> |
| | | <template v-else> |
| | |
| | | </td> |
| | | </template> |
| | | <td class="col-action"> |
| | | <el-button type="danger" link size="small" @click="removeEntry(rowIndex)" icon="Delete" :disabled="form.entries.length <= 2">删除</el-button> |
| | | <el-button type="danger" link size="small" @click="removeEntry(rowIndex)" icon="Delete" :disabled="isViewMode || form.entries.length <= 2">删除</el-button> |
| | | </td> |
| | | </tr> |
| | | <tr class="total-row"> |
| | |
| | | </table> |
| | | </div> |
| | | <div class="voucher-toolbar"> |
| | | <el-button type="primary" link @click="addEntry" icon="Plus">新增行</el-button> |
| | | <el-button type="primary" link @click="addEntry" icon="Plus" :disabled="isViewMode">新增行</el-button> |
| | | </div> |
| | | <div class="voucher-footer"> |
| | | <div class="creator-section"> |
| | | <span class="label">制单人:{{ form.creator }}</span> |
| | | <span class="label">制单人:</span> |
| | | <el-select |
| | | v-model="form.creator" |
| | | :disabled="isViewMode" |
| | | placeholder="请选择制单人" |
| | | filterable |
| | | clearable |
| | | style="width: 200px;" |
| | | > |
| | | <el-option |
| | | v-for="item in creatorOptions" |
| | | :key="item" |
| | | :label="item" |
| | | :value="item" |
| | | /> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | <!-- 编辑模式:使用 AttachmentUploadFile 上传组件 --> |
| | | <div class="voucher-attachment-upload" v-if="!isViewMode"> |
| | | <div class="attachment-label">附件上传:</div> |
| | | <AttachmentUploadFile |
| | | v-model:fileList="form.attachments" |
| | | :disabled="isViewMode" |
| | | :limit="10" |
| | | :fileSize="50" |
| | | buttonText="点击上传附件" |
| | | @change="handleAttachmentChange" |
| | | /> |
| | | </div> |
| | | </el-form> |
| | | <!-- 查看模式:展示附件列表(放在 el-form 外面,避免被 disabled) --> |
| | | <div class="voucher-attachment-upload" v-if="isViewMode && form.attachments?.length"> |
| | | <div class="attachment-label">附件列表:</div> |
| | | <el-table :data="form.attachments" border class="attachment-table"> |
| | | <el-table-column label="附件名称" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ scope.row.originalFilename || scope.row.name || scope.row.fileName || '未命名文件' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column fixed="right" label="操作" width="150" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="previewFile(scope.row)">预览</el-button> |
| | | <el-button link type="primary" size="small" @click="downloadFile(scope.row)">下载</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <div> |
| | | <el-button type="primary" @click="submitForm" :disabled="!isBalanced">保存</el-button> |
| | | <el-button @click="dialogVisible = false">取消</el-button> |
| | | <el-button v-if="!isViewMode" type="primary" @click="submitForm" :disabled="!isBalanced">保存</el-button> |
| | | <el-button @click="dialogVisible = false">{{ isViewMode ? '关闭' : '取消' }}</el-button> |
| | | </div> |
| | | </template> |
| | | </FormDialog> |
| | | <!-- 文件预览组件 --> |
| | | <FilePreview ref="filePreviewRef" /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import { ref, reactive, onMounted, computed, nextTick } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import AttachmentUploadFile from "@/components/AttachmentUpload/file/index.vue"; |
| | | import FileList from "@/components/Dialog/FileList.vue"; |
| | | import FilePreview from "@/components/filePreview/index.vue"; |
| | | import download from "@/plugins/download.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { listAccountSubject } from "@/api/financialManagement/accountSubject"; |
| | | import { |
| | | listVoucherPage, |
| | | addVoucher, |
| | | updateVoucher, |
| | | postVoucher, |
| | | cancelVoucher, |
| | | getVoucherDetail, |
| | | } from "@/api/financialManagement/voucher"; |
| | | |
| | | defineOptions({ |
| | | name: "凭证管理", |
| | | }); |
| | | |
| | | const userStore = useUserStore(); |
| | | const getDefaultCreator = () => userStore.nickName || userStore.name || "张三"; |
| | | |
| | | const filters = reactive({ |
| | | voucherNo: "", |
| | |
| | | { label: "凭证字号", prop: "voucherNo", width: "120" }, |
| | | { label: "凭证日期", prop: "voucherDate", width: "120" }, |
| | | { label: "摘要", prop: "summary", showOverflowTooltip: true }, |
| | | { label: "借方金额", prop: "debit", slot: "debit" }, |
| | | { label: "贷方金额", prop: "credit", slot: "credit" }, |
| | | { label: "借方金额", prop: "debit", dataType: "slot", slot: "debit" }, |
| | | { label: "贷方金额", prop: "credit", dataType: "slot", slot: "credit" }, |
| | | { label: "制单人", prop: "creator", width: "100" }, |
| | | { label: "状态", prop: "status", slot: "status" }, |
| | | { label: "操作", prop: "operation", slot: "operation", width: "220", fixed: "right" }, |
| | | { label: "状态", prop: "status", dataType: "slot", slot: "status" }, |
| | | { label: "操作", prop: "operation", dataType: "slot", slot: "operation", width: "220", fixed: "right" }, |
| | | ]; |
| | | |
| | | const dataList = ref([]); |
| | | const dialogVisible = ref(false); |
| | | const dialogTitle = ref(""); |
| | | const formRef = ref(null); |
| | | const dialogMode = ref("add"); |
| | | const isEdit = ref(false); |
| | | const currentId = ref(null); |
| | | const isViewMode = computed(() => dialogMode.value === "view"); |
| | | const filePreviewRef = ref(null); |
| | | |
| | | const subjectList = [ |
| | | { code: "1001", name: "库存现金" }, |
| | | { code: "1002", name: "银行存款" }, |
| | | { code: "1122", name: "应收账款" }, |
| | | { code: "2202", name: "应付账款" }, |
| | | { code: "5001", name: "生产成本" }, |
| | | { code: "6001", name: "主营业务收入" }, |
| | | { code: "6401", name: "主营业务成本" }, |
| | | const fallbackSubjectTree = [ |
| | | { subjectCode: "1001", subjectName: "库存现金", balanceDirection: "借方", children: [] }, |
| | | { subjectCode: "1002", subjectName: "银行存款", balanceDirection: "借方", children: [] }, |
| | | { subjectCode: "1122", subjectName: "应收账款", balanceDirection: "借方", children: [] }, |
| | | { subjectCode: "2202", subjectName: "应付账款", balanceDirection: "贷方", children: [] }, |
| | | { subjectCode: "5001", subjectName: "生产成本", balanceDirection: "借方", children: [] }, |
| | | { subjectCode: "6001", subjectName: "主营业务收入", balanceDirection: "贷方", children: [] }, |
| | | { subjectCode: "6401", subjectName: "主营业务成本", balanceDirection: "借方", children: [] }, |
| | | ]; |
| | | |
| | | const form = reactive({ |
| | | const subjectTreeOptions = ref([]); |
| | | const subjectList = ref([]); |
| | | const subjectTreeSelectProps = { |
| | | children: "children", |
| | | label: "label", |
| | | value: "value", |
| | | }; |
| | | |
| | | const buildSubjectTreeOptions = (nodes = [], flatList = []) => |
| | | (nodes || []) |
| | | .filter(item => item.subjectCode && item.subjectName) |
| | | .map(item => { |
| | | const balanceDirection = item.balanceDirection || ""; |
| | | const flatItem = { |
| | | code: item.subjectCode, |
| | | name: item.subjectName, |
| | | balanceDirection, |
| | | }; |
| | | flatList.push(flatItem); |
| | | return { |
| | | value: flatItem.code, |
| | | label: `${flatItem.code} ${flatItem.name}${balanceDirection ? ` [${balanceDirection}]` : ""}`, |
| | | children: buildSubjectTreeOptions(item.children || [], flatList), |
| | | }; |
| | | }); |
| | | |
| | | const createEmptyEntry = () => ({ |
| | | subjectCode: "", |
| | | subjectName: "", |
| | | balanceDirection: "", |
| | | summary: "", |
| | | debit: undefined, |
| | | credit: undefined, |
| | | }); |
| | | |
| | | const createDefaultForm = () => ({ |
| | | voucherNo: "", |
| | | voucherPrefix: "记", |
| | | voucherNum: "", |
| | | voucherDate: "", |
| | | attachmentCount: 0, |
| | | entries: [], |
| | | creator: "张三", |
| | | attachments: [], |
| | | entries: [createEmptyEntry(), createEmptyEntry()], |
| | | creator: getDefaultCreator(), |
| | | remark: "", |
| | | }); |
| | | |
| | | const form = reactive({ |
| | | ...createDefaultForm(), |
| | | }); |
| | | |
| | | const userOptions = ref([]); |
| | | |
| | | const creatorOptions = computed(() => { |
| | | const source = [ |
| | | ...userOptions.value.map(item => item.nickName || item.userName || item.name), |
| | | getDefaultCreator(), |
| | | form.creator, |
| | | filters.creator, |
| | | ]; |
| | | return [...new Set(source.filter(Boolean))]; |
| | | }); |
| | | |
| | | const selectedRowIndex = ref(-1); |
| | |
| | | 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); |
| | |
| | | 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 = () => { |
| | |
| | | }; |
| | | |
| | | const addEntry = () => { |
| | | form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }); |
| | | if (isViewMode.value) { |
| | | return; |
| | | } |
| | | form.entries.push(createEmptyEntry()); |
| | | }; |
| | | |
| | | const handleAttachmentChange = (fileList) => { |
| | | form.attachmentCount = fileList?.length || 0; |
| | | }; |
| | | |
| | | // 使用项目封装的 filePreview 组件预览文件 |
| | | const previewFile = (row) => { |
| | | const url = row.previewURL || row.previewUrl || row.url; |
| | | if (url && filePreviewRef.value) { |
| | | filePreviewRef.value.open(url); |
| | | } else { |
| | | ElMessage.warning('文件地址无效,无法预览'); |
| | | } |
| | | }; |
| | | |
| | | // 使用项目封装的 download 插件下载文件 |
| | | const downloadFile = (row) => { |
| | | const url = row.downloadURL || row.downloadUrl || row.url; |
| | | if (url) { |
| | | const filename = row.originalFilename || row.name || row.fileName || 'download'; |
| | | download.byUrl(url, filename); |
| | | } else { |
| | | ElMessage.warning('文件地址无效,无法下载'); |
| | | } |
| | | }; |
| | | |
| | | const selectRow = (index) => { |
| | |
| | | }; |
| | | |
| | | const openAmountInput = (index, type) => { |
| | | if (isViewMode.value) { |
| | | return; |
| | | } |
| | | editingCell.row = index; |
| | | editingCell.type = type; |
| | | nextTick(() => { |
| | |
| | | }; |
| | | |
| | | const removeEntry = (index) => { |
| | | if (isViewMode.value) { |
| | | return; |
| | | } |
| | | if (form.entries.length <= 2) { |
| | | return; |
| | | } |
| | | form.entries.splice(index, 1); |
| | | calculateTotal(); |
| | | }; |
| | | |
| | | const handleSubjectChange = (val, index) => { |
| | | const subject = subjectList.find(item => item.code === val); |
| | | const subject = subjectList.value.find(item => item.code === val); |
| | | if (subject) { |
| | | form.entries[index].subjectName = subject.name; |
| | | form.entries[index].balanceDirection = subject.balanceDirection || ""; |
| | | } else { |
| | | form.entries[index].subjectName = ""; |
| | | form.entries[index].balanceDirection = ""; |
| | | } |
| | | }; |
| | | |
| | | const calculateTotal = () => { |
| | | // 自动计算,由computed属性处理 |
| | | }; |
| | | |
| | | const add = () => { |
| | | dialogMode.value = "add"; |
| | | isEdit.value = false; |
| | | currentId.value = null; |
| | | dialogTitle.value = "新增凭证"; |
| | | const nextNum = String(mockData.length + 1).padStart(2, "0"); |
| | | Object.assign(form, { |
| | | voucherNo: "记-" + nextNum, |
| | | const nextNum = String((pagination.total || 0) + 1).padStart(4, "0"); |
| | | Object.assign(form, createDefaultForm(), { |
| | | voucherPrefix: "记", |
| | | voucherNum: nextNum, |
| | | voucherNo: `记-${nextNum}`, |
| | | voucherDate: new Date().toISOString().split('T')[0], |
| | | attachmentCount: 0, |
| | | entries: [ |
| | | { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }, |
| | | { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }, |
| | | { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }, |
| | | { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }, |
| | | ], |
| | | creator: "张三", |
| | | remark: "", |
| | | }); |
| | | selectedRowIndex.value = 0; |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | const edit = (row) => { |
| | | isEdit.value = true; |
| | | currentId.value = row.id; |
| | | dialogTitle.value = "编辑凭证"; |
| | | const parts = row.voucherNo.split('-'); |
| | | Object.assign(form, { |
| | | ...row, |
| | | voucherPrefix: parts[0] || '记', |
| | | voucherNum: parts[1] || '', |
| | | }); |
| | | if (form.entries.length < 4) { |
| | | while (form.entries.length < 4) { |
| | | form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }); |
| | | const openVoucherDialog = async (row, mode = "edit") => { |
| | | try { |
| | | dialogMode.value = mode; |
| | | isEdit.value = mode === "edit"; |
| | | currentId.value = row.id; |
| | | dialogTitle.value = mode === "view" ? "查看凭证" : "编辑凭证"; |
| | | const { data } = await getVoucherDetail(row.id); |
| | | const detail = data || row; |
| | | const parts = (detail.voucherNo || "").split("-"); |
| | | const attachments = detail.storageBlobVOList || detail.storageBlobDTOs || detail.attachments || []; |
| | | Object.assign(form, createDefaultForm(), { |
| | | ...detail, |
| | | voucherPrefix: parts[0] || "记", |
| | | voucherNum: parts[1] || "", |
| | | creator: detail.creator || getDefaultCreator(), |
| | | attachments, |
| | | entries: |
| | | detail.entries?.map(item => ({ |
| | | subjectCode: item.subjectCode || "", |
| | | subjectName: item.subjectName || "", |
| | | balanceDirection: item.balanceDirection || "", |
| | | summary: item.summary || "", |
| | | debit: Number(item.debit || 0), |
| | | credit: Number(item.credit || 0), |
| | | })) || [], |
| | | }); |
| | | if (form.entries.length < 2) { |
| | | while (form.entries.length < 2) { |
| | | form.entries.push(createEmptyEntry()); |
| | | } |
| | | } |
| | | selectedRowIndex.value = 0; |
| | | dialogVisible.value = true; |
| | | } catch (error) { |
| | | // 提示由全局请求拦截器处理,这里仅防止未捕获异常 |
| | | } |
| | | selectedRowIndex.value = 0; |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | const view = (row) => { |
| | | ElMessage.info(`查看凭证: ${row.voucherNo}`); |
| | | const edit = async row => { |
| | | await openVoucherDialog(row, "edit"); |
| | | }; |
| | | |
| | | const view = async row => { |
| | | await openVoucherDialog(row, "view"); |
| | | }; |
| | | |
| | | const handlePost = (row) => { |
| | |
| | | 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(); |
| | | }); |
| | | }; |
| | | |
| | |
| | | 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(); |
| | | }); |
| | | }; |
| | | |
| | |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | formRef.value.validate((valid) => { |
| | | if (isViewMode.value) { |
| | | dialogVisible.value = false; |
| | | return; |
| | | } |
| | | formRef.value.validate(async valid => { |
| | | if (valid) { |
| | | // 前置校验:与后端规则对齐,减少无效请求 |
| | | if (!isBalanced.value) { |
| | | ElMessage.error("借贷不平衡,请检查分录"); |
| | | return; |
| | | } |
| | | |
| | | const validEntries = form.entries.filter(e => e.subjectCode && (e.debit > 0 || e.credit > 0)); |
| | | const validEntries = form.entries.filter( |
| | | entry => entry.subjectCode && (Number(entry.debit) > 0 || Number(entry.credit) > 0) |
| | | ); |
| | | if (validEntries.length === 0) { |
| | | ElMessage.error("请至少填写一条有效分录"); |
| | | return; |
| | | } |
| | | |
| | | const invalidEntry = validEntries.find( |
| | | entry => Number(entry.debit) > 0 && Number(entry.credit) > 0 |
| | | ); |
| | | if (invalidEntry) { |
| | | ElMessage.error("同一分录不能同时填写借方和贷方"); |
| | | return; |
| | | } |
| | | |
| | | const summary = validEntries.find(e => e.debit > 0)?.summary || ""; |
| | | |
| | | const voucherNo = `${form.voucherPrefix}-${form.voucherNum}`; |
| | | const dataToSave = { |
| | | ...form, |
| | | voucherNo, |
| | | voucherDate: form.voucherDate, |
| | | summary, |
| | | creator: form.creator, |
| | | attachmentCount: Number(form.attachmentCount || 0), |
| | | remark: form.remark, |
| | | debit: totalDebitEntry.value, |
| | | credit: totalCreditEntry.value, |
| | | entries: validEntries, |
| | | storageBlobDTOs: form.attachments || [], |
| | | entries: validEntries.map(entry => ({ |
| | | subjectCode: entry.subjectCode, |
| | | subjectName: entry.subjectName, |
| | | summary: entry.summary, |
| | | debit: Number(entry.debit || 0), |
| | | credit: Number(entry.credit || 0), |
| | | })), |
| | | }; |
| | | |
| | | if (isEdit.value) { |
| | | const index = mockData.findIndex(item => item.id === currentId.value); |
| | | if (index !== -1) { |
| | | mockData[index] = { ...mockData[index], ...dataToSave }; |
| | | try { |
| | | if (isEdit.value) { |
| | | await updateVoucher({ |
| | | id: currentId.value, |
| | | ...dataToSave, |
| | | }); |
| | | ElMessage.success("编辑成功"); |
| | | } else { |
| | | await addVoucher(dataToSave); |
| | | ElMessage.success("新增成功"); |
| | | } |
| | | ElMessage.success("编辑成功"); |
| | | } else { |
| | | const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1; |
| | | mockData.push({ id: newId, ...dataToSave, status: "unposted" }); |
| | | ElMessage.success("新增成功"); |
| | | dialogVisible.value = false; |
| | | await getTableData(); |
| | | } catch (error) { |
| | | // 提示由全局请求拦截器处理,这里仅防止未捕获异常 |
| | | } |
| | | dialogVisible.value = false; |
| | | getTableData(); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getTableData(); |
| | | onMounted(async () => { |
| | | await loadUserOptions(); |
| | | await loadSubjectList(); |
| | | await getTableData(); |
| | | }); |
| | | </script> |
| | | |
| | |
| | | .voucher-attachment-section { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | } |
| | | |
| | | .voucher-attachment-upload { |
| | | margin-top: 15px; |
| | | padding: 0 10px; |
| | | |
| | | .attachment-label { |
| | | font-size: 14px; |
| | | color: #606266; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .attachment-table { |
| | | border-radius: 4px; |
| | | } |
| | | } |
| | | |
| | |
| | | .col-subject { |
| | | position: relative; |
| | | |
| | | .el-select { |
| | | .el-select, |
| | | .el-tree-select { |
| | | .el-input input { |
| | | font-size: 12px; |
| | | } |