| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-form :model="filters" :inline="true"> |
| | | <el-form-item label="会计科目:"> |
| | | <el-cascader v-model="filters.subject" :options="subjectOptions" :props="{ label: 'name', value: 'code' }" placeholder="请选择会计科目" clearable style="width: 250px;" filterable /> |
| | | </el-form-item> |
| | | <el-form-item label="辅助核算:"> |
| | | <el-select v-model="filters.auxiliary" placeholder="请选择辅助核算" clearable style="width: 180px;"> |
| | | <el-option label="客户" value="customer" /> |
| | | <el-option label="供应商" value="supplier" /> |
| | | <el-option label="部门" value="department" /> |
| | | <el-option label="员工" value="employee" /> |
| | | <el-option label="项目" value="project" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="核算对象:"> |
| | | <el-select v-model="filters.auxiliaryItem" placeholder="请选择核算对象" clearable style="width: 200px;" :disabled="!filters.auxiliary"> |
| | | <el-option v-for="item in auxiliaryItems" :key="item.value" :label="item.label" :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="期间:"> |
| | | <el-date-picker v-model="filters.startMonth" type="month" placeholder="开始月份" value-format="YYYY-MM" style="width: 140px;" /> |
| | | <span style="margin: 0 10px;">至</span> |
| | | <el-date-picker v-model="filters.endMonth" type="month" placeholder="结束月份" value-format="YYYY-MM" style="width: 140px;" /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="getTableData">查询</el-button> |
| | | <el-button @click="resetFilters">重置</el-button> |
| | | <el-button @click="handlePrint" icon="Printer">打印</el-button> |
| | | <el-button @click="handleOut" icon="Download">导出</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="app-container ledger-page"> |
| | | <div class="ledger-layout"> |
| | | <aside class="subject-panel"> |
| | | <el-input v-model="subjectKeyword" placeholder="请输入科目名称/编号" clearable prefix-icon="Search" /> |
| | | <el-scrollbar class="subject-tree-scroll"> |
| | | <el-tree |
| | | ref="subjectTreeRef" |
| | | :data="subjectOptions" |
| | | node-key="code" |
| | | :props="{ label: 'name', children: 'children' }" |
| | | highlight-current |
| | | default-expand-all |
| | | :expand-on-click-node="false" |
| | | :filter-node-method="filterSubjectNode" |
| | | @node-click="handleSubjectClick" |
| | | > |
| | | <template #default="{ data }"> |
| | | <span class="subject-node">{{ data.code }} {{ data.name }}</span> |
| | | </template> |
| | | </el-tree> |
| | | </el-scrollbar> |
| | | </aside> |
| | | |
| | | <div class="ledger-header" v-if="currentSubject"> |
| | | <h2>科目明细账</h2> |
| | | <p>科目: {{ currentSubject.code }} {{ currentSubject.name }}</p> |
| | | <p v-if="filters.auxiliary && filters.auxiliaryItem">辅助核算: {{ getAuxiliaryLabel() }}</p> |
| | | <p>期间: {{ filters.startMonth }} 至 {{ filters.endMonth }}</p> |
| | | <section class="ledger-content"> |
| | | <el-form :model="filters" :inline="true" class="filter-form"> |
| | | <el-form-item label="期间:"> |
| | | <el-date-picker v-model="filters.startMonth" type="month" placeholder="开始月份" value-format="YYYY-MM" style="width: 140px;" /> |
| | | <span style="margin: 0 10px;">至</span> |
| | | <el-date-picker v-model="filters.endMonth" type="month" placeholder="结束月份" value-format="YYYY-MM" style="width: 140px;" /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="getTableData">查询</el-button> |
| | | <el-button @click="resetFilters">重置</el-button> |
| | | <!-- <el-button @click="handlePrint" icon="Printer">打印</el-button>--> |
| | | <el-button @click="handleOut" icon="Download">导出</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <div class="table_list"> |
| | | <el-table :data="dataList" border style="width: 100%"> |
| | | <el-table-column prop="date" label="日期" width="120" /> |
| | | <el-table-column prop="voucherNo" label="凭证字号" width="120" /> |
| | | <el-table-column prop="summary" label="摘要" min-width="200" show-overflow-tooltip /> |
| | | <el-table-column prop="debit" label="借方" width="150"> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.debit > 0" class="text-danger">¥{{ formatMoney(row.debit) }}</span> |
| | | <span v-else>-</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="credit" label="贷方" width="150"> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.credit > 0" class="text-success">¥{{ formatMoney(row.credit) }}</span> |
| | | <span v-else>-</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="方向" width="80"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="row.direction === '借' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="余额" width="150"> |
| | | <template #default="{ row }"> |
| | | <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">¥{{ formatMoney(Math.abs(row.balance)) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <el-empty v-if="!currentSubject" description="请选择会计科目查询" style="margin-top: 50px;" /> |
| | | </section> |
| | | </div> |
| | | |
| | | <div class="table_list"> |
| | | <el-table :data="dataList" border style="width: 100%" show-summary :summary-method="getSummaries"> |
| | | <el-table-column prop="date" label="日期" width="120" /> |
| | | <el-table-column prop="voucherNo" label="凭证字号" width="120" /> |
| | | <el-table-column prop="summary" label="摘要" min-width="200" show-overflow-tooltip /> |
| | | <el-table-column label="借方" width="150"> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.debit > 0" class="text-danger">¥{{ formatMoney(row.debit) }}</span> |
| | | <span v-else>-</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="贷方" width="150"> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.credit > 0" class="text-success">¥{{ formatMoney(row.credit) }}</span> |
| | | <span v-else>-</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="方向" width="80"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="row.direction === '借' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="余额" width="150"> |
| | | <template #default="{ row }"> |
| | | <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">¥{{ formatMoney(Math.abs(row.balance)) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <el-empty v-if="!currentSubject" description="请选择会计科目查询" style="margin-top: 50px;" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, computed, watch } from "vue"; |
| | | import { ref, reactive, onMounted, computed, watch, nextTick } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { listAccountSubject } from "@/api/financialManagement/accountSubject"; |
| | | import { getDetailLedger } from "@/api/financialManagement/ledger"; |
| | | |
| | | defineOptions({ |
| | | name: "科目明细账", |
| | | }); |
| | | |
| | | const filters = reactive({ |
| | | subject: [], |
| | | auxiliary: "", |
| | | auxiliaryItem: "", |
| | | startMonth: "2024-01", |
| | | endMonth: "2024-03", |
| | | subject: "", |
| | | startMonth: "", |
| | | endMonth: "", |
| | | }); |
| | | |
| | | const dataList = ref([]); |
| | | const subjectOptions = ref([]); |
| | | const subjectKeyword = ref(""); |
| | | const subjectTreeRef = ref(); |
| | | |
| | | const subjectOptions = [ |
| | | { |
| | | code: "1122", |
| | | name: "应收账款", |
| | | children: [ |
| | | { code: "112201", name: "北京科技有限公司" }, |
| | | { code: "112202", name: "上海贸易公司" }, |
| | | { code: "112203", name: "广州实业有限公司" }, |
| | | ], |
| | | }, |
| | | { |
| | | code: "2202", |
| | | name: "应付账款", |
| | | children: [ |
| | | { code: "220201", name: "北京原材料供应商" }, |
| | | { code: "220202", name: "上海电子元器件公司" }, |
| | | { code: "220203", name: "广州包装材料厂" }, |
| | | ], |
| | | }, |
| | | { |
| | | code: "6602", |
| | | name: "管理费用", |
| | | children: [ |
| | | { code: "660201", name: "办公费" }, |
| | | { code: "660202", name: "差旅费" }, |
| | | { code: "660203", name: "业务招待费" }, |
| | | ], |
| | | }, |
| | | const getPreviousMonth = () => { |
| | | const date = new Date(); |
| | | date.setDate(1); |
| | | date.setMonth(date.getMonth() - 1); |
| | | const year = date.getFullYear(); |
| | | const month = String(date.getMonth() + 1).padStart(2, "0"); |
| | | return `${year}-${month}`; |
| | | }; |
| | | |
| | | const defaultMonth = getPreviousMonth(); |
| | | filters.startMonth = defaultMonth; |
| | | filters.endMonth = defaultMonth; |
| | | |
| | | const fallbackSubjects = [ |
| | | { code: "1122", name: "应收账款" }, |
| | | { code: "2202", name: "应付账款" }, |
| | | { code: "6602", name: "管理费用" }, |
| | | ]; |
| | | |
| | | const auxiliaryItems = computed(() => { |
| | | const map = { |
| | | customer: [ |
| | | { value: "1", label: "北京科技有限公司" }, |
| | | { value: "2", label: "上海贸易公司" }, |
| | | { value: "3", label: "广州实业有限公司" }, |
| | | ], |
| | | supplier: [ |
| | | { value: "1", label: "北京原材料供应商" }, |
| | | { value: "2", label: "上海电子元器件公司" }, |
| | | { value: "3", label: "广州包装材料厂" }, |
| | | ], |
| | | department: [ |
| | | { value: "1", label: "财务部" }, |
| | | { value: "2", label: "销售部" }, |
| | | { value: "3", label: "采购部" }, |
| | | ], |
| | | employee: [ |
| | | { value: "1", label: "张三" }, |
| | | { value: "2", label: "李四" }, |
| | | { value: "3", label: "王五" }, |
| | | ], |
| | | project: [ |
| | | { value: "1", label: "项目A" }, |
| | | { value: "2", label: "项目B" }, |
| | | { value: "3", label: "项目C" }, |
| | | ], |
| | | }; |
| | | return map[filters.auxiliary] || []; |
| | | }); |
| | | |
| | | watch(() => filters.auxiliary, () => { |
| | | filters.auxiliaryItem = ""; |
| | | }); |
| | | |
| | | const currentSubject = computed(() => { |
| | | if (!filters.subject || filters.subject.length === 0) return null; |
| | | const code = filters.subject[filters.subject.length - 1]; |
| | | return findSubject(subjectOptions, code); |
| | | }); |
| | | const toTree = (nodes = []) => |
| | | nodes |
| | | .filter(item => item.subjectCode && item.subjectName) |
| | | .map(item => ({ |
| | | code: item.subjectCode, |
| | | name: item.subjectName, |
| | | children: toTree(item.children || []), |
| | | })); |
| | | |
| | | const findSubject = (options, code) => { |
| | | for (const item of options) { |
| | |
| | | return null; |
| | | }; |
| | | |
| | | const getAuxiliaryLabel = () => { |
| | | const item = auxiliaryItems.value.find(i => i.value === filters.auxiliaryItem); |
| | | return item ? item.label : ""; |
| | | const currentSubject = computed(() => { |
| | | if (!filters.subject) return null; |
| | | return findSubject(subjectOptions.value, filters.subject); |
| | | }); |
| | | |
| | | const getFirstSubjectCode = (nodes = []) => { |
| | | for (const item of nodes) { |
| | | if (item.code) return item.code; |
| | | if (item.children && item.children.length > 0) { |
| | | const childCode = getFirstSubjectCode(item.children); |
| | | if (childCode) return childCode; |
| | | } |
| | | } |
| | | return ""; |
| | | }; |
| | | |
| | | const setDefaultSubjectSelection = async () => { |
| | | const firstCode = getFirstSubjectCode(subjectOptions.value); |
| | | if (!firstCode) { |
| | | filters.subject = ""; |
| | | subjectTreeRef.value?.setCurrentKey(null); |
| | | return; |
| | | } |
| | | filters.subject = firstCode; |
| | | await nextTick(); |
| | | subjectTreeRef.value?.setCurrentKey(firstCode); |
| | | }; |
| | | |
| | | const filterSubjectNode = (value, data) => { |
| | | const keyword = value?.trim(); |
| | | if (!keyword) return true; |
| | | return `${data.code}${data.name}`.includes(keyword); |
| | | }; |
| | | |
| | | watch(subjectKeyword, (value) => { |
| | | subjectTreeRef.value?.filter(value || ""); |
| | | }); |
| | | |
| | | const handleSubjectClick = async (data) => { |
| | | filters.subject = data.code; |
| | | await getTableData(); |
| | | }; |
| | | |
| | | const loadSubjectOptions = async () => { |
| | | let options = []; |
| | | try { |
| | | const { data } = await listAccountSubject({ |
| | | current: 1, |
| | | size: 1000, |
| | | }); |
| | | options = toTree(data?.records || []); |
| | | } catch (error) { |
| | | // 全局拦截器已提示,下面走兜底科目 |
| | | } |
| | | if (options.length === 0) { |
| | | options = fallbackSubjects.map(item => ({ ...item, children: [] })); |
| | | } |
| | | subjectOptions.value = options; |
| | | await setDefaultSubjectSelection(); |
| | | if (filters.subject) { |
| | | await getTableData(); |
| | | } |
| | | }; |
| | | |
| | | const formatMoney = (value) => { |
| | |
| | | return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); |
| | | }; |
| | | |
| | | const mockData = [ |
| | | { date: "2024-01-01", voucherNo: "-", summary: "期初余额", debit: 0, credit: 0, direction: "借", balance: 10000 }, |
| | | { date: "2024-01-05", voucherNo: "记-0001", summary: "销售出库", debit: 5000, credit: 0, direction: "借", balance: 15000 }, |
| | | { date: "2024-01-10", voucherNo: "记-0002", summary: "收到货款", debit: 0, credit: 3000, direction: "借", balance: 12000 }, |
| | | { date: "2024-01-15", voucherNo: "记-0003", summary: "销售出库", debit: 8000, credit: 0, direction: "借", balance: 20000 }, |
| | | { date: "2024-01-20", voucherNo: "记-0004", summary: "销售退货", debit: 0, credit: 2000, direction: "借", balance: 18000 }, |
| | | { date: "2024-01-25", voucherNo: "记-0005", summary: "收到货款", debit: 0, credit: 5000, direction: "借", balance: 13000 }, |
| | | { date: "2024-01-31", voucherNo: "-", summary: "本月合计", debit: 13000, credit: 10000, direction: "借", balance: 13000 }, |
| | | { date: "2024-02-01", voucherNo: "-", summary: "期初余额", debit: 0, credit: 0, direction: "借", balance: 13000 }, |
| | | { date: "2024-02-10", voucherNo: "记-0006", summary: "销售出库", debit: 6000, credit: 0, direction: "借", balance: 19000 }, |
| | | { date: "2024-02-15", voucherNo: "记-0007", summary: "收到货款", debit: 0, credit: 4000, direction: "借", balance: 15000 }, |
| | | { date: "2024-02-28", voucherNo: "-", summary: "本月合计", debit: 6000, credit: 4000, direction: "借", balance: 15000 }, |
| | | { date: "2024-03-01", voucherNo: "-", summary: "期初余额", debit: 0, credit: 0, direction: "借", balance: 15000 }, |
| | | { date: "2024-03-05", voucherNo: "记-0008", summary: "销售出库", debit: 7000, credit: 0, direction: "借", balance: 22000 }, |
| | | { date: "2024-03-10", voucherNo: "记-0009", summary: "收到货款", debit: 0, credit: 6000, direction: "借", balance: 16000 }, |
| | | { date: "2024-03-31", voucherNo: "-", summary: "本月合计", debit: 7000, credit: 6000, direction: "借", balance: 16000 }, |
| | | { date: "2024-03-31", voucherNo: "-", summary: "本年累计", debit: 26000, credit: 20000, direction: "借", balance: 16000 }, |
| | | ]; |
| | | |
| | | const getTableData = () => { |
| | | // 联调约定:明细账按科目与期间过滤 |
| | | const getTableData = async () => { |
| | | if (!currentSubject.value) { |
| | | dataList.value = []; |
| | | return; |
| | | } |
| | | dataList.value = [...mockData]; |
| | | try { |
| | | const { data } = await getDetailLedger({ |
| | | subjectCode: currentSubject.value.code, |
| | | startMonth: filters.startMonth, |
| | | endMonth: filters.endMonth, |
| | | }); |
| | | dataList.value = Array.isArray(data) ? data : data?.records || []; |
| | | } catch (error) { |
| | | // 提示由全局请求拦截器处理,这里仅防止未捕获异常 |
| | | } |
| | | }; |
| | | |
| | | const resetFilters = () => { |
| | | filters.subject = []; |
| | | filters.auxiliary = ""; |
| | | filters.auxiliaryItem = ""; |
| | | filters.startMonth = "2024-01"; |
| | | filters.endMonth = "2024-03"; |
| | | const resetFilters = async () => { |
| | | filters.startMonth = defaultMonth; |
| | | filters.endMonth = defaultMonth; |
| | | dataList.value = []; |
| | | }; |
| | | |
| | | const getSummaries = (param) => { |
| | | const { columns, data } = param; |
| | | const sums = []; |
| | | columns.forEach((column, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "合计"; |
| | | return; |
| | | } |
| | | if (column.property === "debit") { |
| | | const values = data.map(item => Number(item.debit)); |
| | | const sum = values.reduce((prev, curr) => prev + curr, 0); |
| | | sums[index] = "¥" + formatMoney(sum); |
| | | } else if (column.property === "credit") { |
| | | const values = data.map(item => Number(item.credit)); |
| | | const sum = values.reduce((prev, curr) => prev + curr, 0); |
| | | sums[index] = "¥" + formatMoney(sum); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | subjectKeyword.value = ""; |
| | | subjectTreeRef.value?.filter(""); |
| | | await setDefaultSubjectSelection(); |
| | | if (filters.subject) { |
| | | await getTableData(); |
| | | } |
| | | }; |
| | | |
| | | const handlePrint = () => { |
| | |
| | | ElMessage.success("导出成功"); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | // 默认不加载数据,需要选择科目 |
| | | onMounted(async () => { |
| | | await loadSubjectOptions(); |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .ledger-header { |
| | | text-align: center; |
| | | margin-bottom: 20px; |
| | | h2 { |
| | | margin: 0 0 10px 0; |
| | | } |
| | | p { |
| | | color: #606266; |
| | | margin: 5px 0; |
| | | } |
| | | .ledger-layout { |
| | | display: flex; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .subject-panel { |
| | | width: 260px; |
| | | flex-shrink: 0; |
| | | padding: 12px; |
| | | border: 1px solid #e4e7ed; |
| | | border-radius: 8px; |
| | | background-color: #fff; |
| | | } |
| | | |
| | | .subject-tree-scroll { |
| | | height: 600px; |
| | | margin-top: 12px; |
| | | } |
| | | |
| | | .subject-node { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .ledger-content { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .filter-form { |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .text-primary { |
| | |
| | | color: #e6a23c; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .subject-panel :deep(.el-tree-node__content) { |
| | | height: 34px; |
| | | } |
| | | |
| | | .subject-panel :deep(.el-tree-node.is-current > .el-tree-node__content) { |
| | | background-color: #f0f7ff; |
| | | } |
| | | </style> |