| | |
| | | <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', checkStrictly: true }" placeholder="请选择会计科目" clearable style="width: 250px;" filterable /> |
| | | </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>期间: {{ 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 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;" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, computed } from "vue"; |
| | | import { ref, reactive, onMounted, computed, watch, nextTick } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { listAccountSubject } from "@/api/financialManagement/accountSubject"; |
| | | import { getGeneralLedger } from "@/api/financialManagement/ledger"; |
| | |
| | | }); |
| | | |
| | | const filters = reactive({ |
| | | subject: [], |
| | | subject: "", |
| | | startMonth: "", |
| | | endMonth: "", |
| | | }); |
| | | |
| | | const dataList = ref([]); |
| | | const subjectOptions = ref([]); |
| | | const subjectKeyword = ref(""); |
| | | const subjectTreeRef = ref(); |
| | | |
| | | 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: "1001", name: "库存现金" }, |
| | |
| | | { code: "6001", name: "主营业务收入" }, |
| | | ]; |
| | | |
| | | const toCascaderTree = (nodes = []) => |
| | | const toTree = (nodes = []) => |
| | | nodes |
| | | .filter(item => item.subjectCode && item.subjectName) |
| | | .map(item => ({ |
| | | code: item.subjectCode, |
| | | name: item.subjectName, |
| | | children: toCascaderTree(item.children || []), |
| | | children: toTree(item.children || []), |
| | | })); |
| | | |
| | | const loadSubjectOptions = async () => { |
| | | try { |
| | | const { data } = await listAccountSubject({ |
| | | current: 1, |
| | | size: 1000, |
| | | status: 0, |
| | | }); |
| | | const options = toCascaderTree(data?.records || []); |
| | | if (options.length > 0) { |
| | | subjectOptions.value = options; |
| | | return; |
| | | } |
| | | } catch (error) { |
| | | // 全局拦截器已提示,下面走兜底科目 |
| | | } |
| | | subjectOptions.value = fallbackSubjects.map(item => ({ ...item, children: [] })); |
| | | }; |
| | | |
| | | const currentSubject = computed(() => { |
| | | const code = getSelectedSubjectCode(filters.subject); |
| | | if (!code) return null; |
| | | return findSubject(subjectOptions.value, code); |
| | | }); |
| | | |
| | | const getSelectedSubjectCode = (subjectValue) => { |
| | | if (Array.isArray(subjectValue)) { |
| | | return subjectValue.length ? subjectValue[subjectValue.length - 1] : ""; |
| | | } |
| | | return subjectValue || ""; |
| | | }; |
| | | |
| | | const findSubject = (options, code) => { |
| | | for (const item of options) { |
| | |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | 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, |
| | | status: 0, |
| | | }); |
| | | 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) => { |
| | |
| | | } |
| | | }; |
| | | |
| | | const resetFilters = () => { |
| | | filters.subject = []; |
| | | 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 = () => { |
| | |
| | | </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> |