<template>
|
<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>
|
|
<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>
|
</template>
|
|
<script setup>
|
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: "",
|
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: "1122", name: "应收账款" },
|
{ code: "2202", name: "应付账款" },
|
{ code: "6602", name: "管理费用" },
|
];
|
|
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) {
|
if (item.code === code) return item;
|
if (item.children && item.children.length > 0) {
|
const found = findSubject(item.children, code);
|
if (found) return found;
|
}
|
}
|
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,
|
});
|
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) => {
|
if (value === undefined || value === null) return "0.00";
|
return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
};
|
|
// 联调约定:明细账按科目与期间过滤
|
const getTableData = async () => {
|
if (!currentSubject.value) {
|
dataList.value = [];
|
return;
|
}
|
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 = async () => {
|
filters.startMonth = defaultMonth;
|
filters.endMonth = defaultMonth;
|
dataList.value = [];
|
subjectKeyword.value = "";
|
subjectTreeRef.value?.filter("");
|
await setDefaultSubjectSelection();
|
if (filters.subject) {
|
await getTableData();
|
}
|
};
|
|
const handlePrint = () => {
|
ElMessage.info("打印功能");
|
};
|
|
const handleOut = () => {
|
ElMessage.success("导出成功");
|
};
|
|
onMounted(async () => {
|
await loadSubjectOptions();
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
.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: #409eff;
|
font-weight: bold;
|
}
|
|
.text-success {
|
color: #67c23a;
|
font-weight: bold;
|
}
|
|
.text-danger {
|
color: #f56c6c;
|
font-weight: bold;
|
}
|
|
.text-warning {
|
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>
|