Files
estate/src/views/system/report/LeaseManagementDetails.vue
2026-09-02 17:32:15 +08:00

734 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="p-2 h-full flex flex-col">
<pageHeader></pageHeader>
<div class="overflow-auto flex-1">
<el-card shadow="hover">
<el-form ref="queryFormRef" :inline="true" :model="queryParams" :rules="formRules" label-width="140" @submit.prevent="handleQuery">
<div>
<el-form-item label="小区(多选)" prop="villageIds">
<el-select v-model="queryParams.villageIds" :multiple="true" placeholder="请选择小区">
<el-option v-for="item in villageList" :key="item.villageId" :label="item.villageName" :value="item.villageId" />
</el-select>
</el-form-item>
<el-form-item label="承租方" prop="lessee">
<el-input v-model.trim="queryParams.lessee" placeholder="请输入承租方" />
</el-form-item>
<el-form-item label="电话" prop="phone">
<el-input v-model.trim="queryParams.phone" placeholder="请输入电话" type="tel" />
</el-form-item>
<el-form-item label="房号" prop="roomNumber">
<el-input v-model.trim="queryParams.roomNumber" placeholder="请输入房号" />
</el-form-item>
</div>
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]" style="overflow: auto; overflow-y: hidden">
<div>
<!-- 搜索条件: 面积/收费模式/收费标准 -->
<div>
<el-form-item label="面积" prop="areaSize" style="margin-bottom: 0">
<el-input v-model.trim="queryParams.areaSize" placeholder="请输入面积区间" />
</el-form-item>
<el-form-item label="收费模式(多选)" prop="chargeModes" style="margin-bottom: 0">
<el-select v-model="queryParams.chargeModes" :multiple="true" placeholder="请选择收费模式">
<el-option v-for="item in com_house_rental_pricing_model" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="收费标准(多选)" prop="chargeStandards" style="margin-bottom: 0">
<el-select v-model="queryParams.chargeStandards" :multiple="true" placeholder="请选择收费标准">
<el-option v-for="item in com_house_rental_pricing" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</div>
<div v-if="queryParams.chargeTypeFilters.length > 0" class="line"></div>
<el-scrollbar>
<div style="width: fit-content">
<!-- 搜索条件: 应收/已收/未收 ▐╬╬-->
<div class="queryBox_over">
<el-form-item
v-for="(item, index) in queryParams.chargeTypeFilters"
:key="index"
:label="`${item.chargeTypeName}(应收)`"
:prop="`chargeTypeFilters[${index}].receivable`"
>
<el-input v-model.trim="queryParams.chargeTypeFilters[index].receivable" placeholder="区间搜索 示例:100-2000" />
</el-form-item>
</div>
<div class="queryBox_over">
<el-form-item
v-for="(item, index) in queryParams.chargeTypeFilters"
:key="index"
:label="`${item.chargeTypeName}(已收)`"
:prop="`chargeTypeFilters[${index}].received`"
>
<el-input v-model.trim="queryParams.chargeTypeFilters[index].received" placeholder="区间搜索 示例:100-2000" />
</el-form-item>
</div>
<div class="queryBox_over">
<el-form-item
v-for="(item, index) in [...queryParams.chargeTypeFilters]"
:key="index"
:label="`${item.chargeTypeName}(未收)`"
:prop="`chargeTypeFilters[${index}].uncollected`"
style="margin-bottom: 0"
>
<el-input v-model.trim="queryParams.chargeTypeFilters[index].uncollected" placeholder="区间搜索 示例:100-2000" />
</el-form-item>
</div>
</div>
</el-scrollbar>
<div class="line"></div>
<!-- 搜索条件: 租赁起始日期/租赁终止日期 -->
<div class="flex justify-between">
<div>
<el-form-item label="租赁起始日期" prop="leaseStartDate">
<el-date-picker
v-model="leaseStartDate"
end-placeholder="结束日期"
format="YYYY-MM-DD"
placeholder="请选择"
range-separator="至"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD"
@clear="handleClear('leaseStartDate')"
@update:model-value="(val: string[]) => handleUpdateLeaseDate('leaseStartDate', val)"
/>
</el-form-item>
<el-form-item label="租赁终止日期" prop="leaseEndDate">
<el-date-picker
v-model="leaseEndDate"
end-placeholder="结束日期"
format="YYYY-MM-DD"
placeholder="请选择"
range-separator="至"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD"
@clear="handleClear('leaseEndDate')"
@update:model-value="(val: string[]) => handleUpdateLeaseDate('leaseEndDate', val)"
/>
</el-form-item>
</div>
<div v-if="showSearch">
<el-form-item label="">
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
<div v-if="showSearch" class="arrowBox" style="margin-left: 10px" @click="ChangeShowSearch(false)">
<span>收起</span>
<el-icon style="margin-left: 5px"><ArrowUpBold /></el-icon>
</div>
<div v-else class="arrowBox" style="margin-left: 10px" @click="ChangeShowSearch(true)">
<span>展开</span>
<el-icon style="margin-left: 5px"><ArrowDownBold /></el-icon>
</div>
</el-form-item>
</div>
</div>
</div>
</div>
</transition>
<div v-if="!showSearch">
<el-form-item label="">
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
<div v-if="showSearch" class="arrowBox" style="margin-left: 10px" @click="ChangeShowSearch(false)">
<span>收起</span>
<el-icon style="margin-left: 5px"><ArrowUpBold /></el-icon>
</div>
<div v-else class="arrowBox" style="margin-left: 10px" @click="ChangeShowSearch(true)">
<span>展开</span>
<el-icon style="margin-left: 5px"><ArrowDownBold /></el-icon>
</div>
</el-form-item>
</div>
</el-form>
</el-card>
<el-card shadow="never" style="margin-top: 5px; height: 800px">
<template #header>
<div class="flex justify-between items-center">
<span>租赁管理明细</span>
<el-button type="primary" @click="handleExport">导出</el-button>
</div>
</template>
<el-table v-loading="loading" :data="ArrearsDetailList" border>
<el-table-column align="center" label="区域" prop="area" />
<el-table-column align="center" label="承租方" prop="lessee" />
<el-table-column align="center" label="电话" prop="phone" width="120" />
<el-table-column align="center" label="房号" prop="roomNumber" />
<el-table-column align="center" label="面积" prop="areaSize" width="80" />
<el-table-column align="center" label="收费模式" prop="chargeMode" />
<el-table-column align="center" label="收费标准" prop="chargeStandard" />
<el-table-column align="center" label="租赁起始日" prop="leaseStartDate" />
<el-table-column align="center" label="租赁终止日" prop="leaseEndDate" />
<el-table-column align="center" label="应收项目">
<template #default>
<el-table-column v-for="(item, index) in dynamicColumns" :key="index" :label="item.chargeTypeName" align="center" prop="totalAmount">
<template #default="scope">
<span>{{ scope.row.receivableAmounts[item.chargeTypeId] }}</span>
</template>
</el-table-column>
</template>
</el-table-column>
<el-table-column align="center" label="已收项目">
<template #default>
<el-table-column v-for="(item, index) in dynamicColumns" :key="index" :label="item.chargeTypeName" align="center" prop="totalAmount">
<template #default="scope">
<span>{{ scope.row.receivedAmounts[item.chargeTypeId] }}</span>
</template>
</el-table-column>
</template>
</el-table-column>
<el-table-column align="center" label="未收项目">
<template #default>
<el-table-column v-for="(item, index) in dynamicColumns" :key="index" :label="item.chargeTypeName" align="center" prop="totalAmount">
<template #default="scope">
<span>{{ scope.row.uncollectedAmounts[item.chargeTypeId] }}</span>
</template>
</el-table-column>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" v-model:limit="queryParams.pageSize" v-model:page="queryParams.pageNum" :total="total" @pagination="getList" />
</el-card>
</div>
</div>
</template>
<script lang="ts" name="Arrears" setup>
import pagination from '@/components/Pagination/index.vue';
import pageHeader from '@/components/Pageheader/index.vue';
import { getCommunitylistAPI } from '@/api/system/community';
import { ColumnConfig, exportToExcel } from '@/utils/xlsx';
import { ArrowDownBold, ArrowUpBold } from '@element-plus/icons-vue';
import { getLeaseManagementDetailChargeTypesList, getLeaseManagementDetailList } from '@/api/system/report/LeaseManagementDetails';
import { Datum, queryParamsType, Row } from '@/api/system/report/LeaseManagementDetails/type';
import { FormRules } from 'element-plus';
import { validator } from '@/utils/Reg';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { com_house_rental_pricing_model, com_house_rental_pricing } = toRefs<any>(
proxy?.useDict('com_house_rental_pricing_model', 'com_house_rental_pricing')
);
const ArrearsDetailList = ref<Row[]>([]);
const loading = ref(true);
const total = ref(0);
const showSearch = ref(true);
function ChangeShowSearch(bol: boolean) {
showSearch.value = bol;
}
const leaseStartDate = ref([]);
const leaseEndDate = ref([]);
function handleUpdateLeaseDate(type: 'leaseStartDate' | 'leaseEndDate', val: string[]) {
if (type === 'leaseStartDate') {
queryParams.value.leaseStartDateBegin = val[0];
queryParams.value.leaseStartDateEnd = val[1];
} else if (type === 'leaseEndDate') {
queryParams.value.leaseEndDateBegin = val[0];
queryParams.value.leaseEndDateEnd = val[1];
}
}
function handleClear(type: 'leaseStartDate' | 'leaseEndDate') {
if (type === 'leaseStartDate') {
leaseStartDate.value = [];
queryParams.value.leaseStartDateBegin = '';
queryParams.value.leaseStartDateEnd = '';
} else if (type === 'leaseEndDate') {
leaseEndDate.value = [];
queryParams.value.leaseEndDateBegin = '';
queryParams.value.leaseEndDateEnd = '';
}
}
const queryFormRef = ref<ElFormInstance>();
const data = reactive<{
queryParams: queryParamsType;
}>({
queryParams: {
pageNum: 1,
pageSize: 10,
villageIds: [],
lessee: undefined,
phone: undefined,
roomNumber: undefined,
areaSize: undefined,
chargeTypeFilters: [],
chargeModes: [],
chargeStandards: [],
leaseEndDateBegin: undefined,
leaseEndDateEnd: undefined
}
});
const { queryParams } = toRefs(data);
// 动态生成表单验证规则
const formRules = computed<FormRules>(() => {
const dynamicRules: FormRules = {};
queryParams.value.chargeTypeFilters.forEach((_, index) => {
dynamicRules[`chargeTypeFilters[${index}].receivable`] = [
{
validator: (_: any, value: string, callback: (error?: any) => void) => {
if (!value || value === '') {
callback();
return;
}
const rangePattern = /^(-?\d+(\.\d{1,2})?)-(-?\d+(\.\d{1,2})?)?$/;
const singlePattern = /^-?\d+(\.\d{1,2})?$/;
const MAX_AMOUNT = 99999999;
if (rangePattern.test(value)) {
const parts = value.split('-');
const min = Number(parts[0]);
const max = parts[1] !== '' ? Number(parts[1]) : null; // 空字符串才是 null
// 校验下限
if (Math.abs(min) > MAX_AMOUNT) {
return callback(new Error(`金额不能超过 ${MAX_AMOUNT}`));
}
// 只有当 max 存在时才校验
if (max !== null) {
if (Math.abs(max) > MAX_AMOUNT) {
return callback(new Error(`金额不能超过 ${MAX_AMOUNT}`));
}
if (min > max) {
return callback(new Error('下限不能大于上限'));
}
}
return callback();
} else if (singlePattern.test(value)) {
const amount = Number(value);
if (Math.abs(amount) > MAX_AMOUNT) {
return callback(new Error(`金额不能超过 ${MAX_AMOUNT}`));
} else {
return callback();
}
} else {
callback(new Error('格式错误,示例: 0-1000、-1000、0-、1000'));
}
},
trigger: 'blur'
}
];
dynamicRules[`chargeTypeFilters[${index}].received`] = [
{
validator: (_: any, value: string, callback: (error?: any) => void) => {
if (!value || value === '') {
callback();
return;
}
const rangePattern = /^(-?\d+(\.\d{1,2})?)-(-?\d+(\.\d{1,2})?)?$/;
const singlePattern = /^-?\d+(\.\d{1,2})?$/;
const MAX_AMOUNT = 99999999;
if (rangePattern.test(value)) {
const parts = value.split('-');
const min = Number(parts[0]);
const max = parts[1] !== '' ? Number(parts[1]) : null; // 空字符串才是 null
// 校验下限
if (Math.abs(min) > MAX_AMOUNT) {
return callback(new Error(`金额不能超过 ${MAX_AMOUNT}`));
}
// 只有当 max 存在时才校验
if (max !== null) {
if (Math.abs(max) > MAX_AMOUNT) {
return callback(new Error(`金额不能超过 ${MAX_AMOUNT}`));
}
if (min > max) {
return callback(new Error('下限不能大于上限'));
}
}
return callback();
} else if (singlePattern.test(value)) {
const amount = Number(value);
if (Math.abs(amount) > MAX_AMOUNT) {
return callback(new Error(`金额不能超过 ${MAX_AMOUNT}`));
} else {
return callback();
}
} else {
callback(new Error('格式错误,示例: 0-1000、-1000、0-、1000'));
}
},
trigger: 'blur'
}
];
dynamicRules[`chargeTypeFilters[${index}].uncollected`] = [
{
validator: (_: any, value: string, callback: (error?: any) => void) => {
if (!value || value === '') {
callback();
return;
}
const rangePattern = /^(-?\d+(\.\d{1,2})?)-(-?\d+(\.\d{1,2})?)?$/;
const singlePattern = /^-?\d+(\.\d{1,2})?$/;
const MAX_AMOUNT = 99999999;
if (rangePattern.test(value)) {
const parts = value.split('-');
const min = Number(parts[0]);
const max = parts[1] !== '' ? Number(parts[1]) : null; // 空字符串才是 null
// 校验下限
if (Math.abs(min) > MAX_AMOUNT) {
return callback(new Error(`金额不能超过 ${MAX_AMOUNT}`));
}
// 只有当 max 存在时才校验
if (max !== null) {
if (Math.abs(max) > MAX_AMOUNT) {
return callback(new Error(`金额不能超过 ${MAX_AMOUNT}`));
}
if (min > max) {
return callback(new Error('下限不能大于上限'));
}
}
return callback();
} else if (singlePattern.test(value)) {
const amount = Number(value);
if (Math.abs(amount) > MAX_AMOUNT) {
return callback(new Error(`金额不能超过 ${MAX_AMOUNT}`));
} else {
return callback();
}
} else {
callback(new Error('格式错误,示例: 0-1000、-1000、0-、1000'));
}
},
trigger: 'blur'
}
];
});
// 面积
dynamicRules[`areaSize`] = [
{
validator: (_: any, value: string, callback: (error?: any) => void) => {
if (!value || value === '') {
callback();
return;
}
const rangePattern = /^(-?\d+(\.\d{1,2})?)-(-?\d+(\.\d{1,2})?)?$/;
const singlePattern = /^-?\d+(\.\d{1,2})?$/;
const MAX_AMOUNT = 99999999;
if (rangePattern.test(value)) {
const [min, max] = value.split('-').map(Number);
if (Math.abs(min) > MAX_AMOUNT || Math.abs(max) > MAX_AMOUNT) {
return callback(new Error(`面积不能超过 ${MAX_AMOUNT}`));
} else if (min > max) {
return callback(new Error('下限不能大于上限'));
} else {
return callback();
}
} else if (singlePattern.test(value)) {
const amount = Number(value);
if (Math.abs(amount) > MAX_AMOUNT) {
return callback(new Error(`面积不能超过 ${MAX_AMOUNT}`));
} else {
return callback();
}
} else {
callback(new Error('格式错误,示例: 0-100、-100、0-、100'));
}
},
trigger: 'blur'
}
];
// tel
dynamicRules[`tel`] = [
{
validator: (_: any, value: string, callback: (error?: any) => void) => {
if (!value || value === '') {
callback();
return;
}
validator(value, 'phone') ? callback() : callback(new Error('请输入正确的手机号'));
},
trigger: 'blur'
}
];
// 承租方
dynamicRules[`lessee`] = [
{
validator: (_: any, value: string, callback: (error?: any) => void) => {
if (!value || value === '') {
callback();
return;
}
validator(value, 'Character') ? callback() : callback(new Error('请输入正确的承租方姓名'));
},
trigger: 'blur'
}
];
// 房 间 号
dynamicRules[`roomNumber`] = [
{
validator: (_: any, value: string, callback: (error?: any) => void) => {
if (!value || value === '') {
callback();
return;
}
validator(value, 'Character') ? callback() : callback(new Error('请输入正确的房间号'));
},
trigger: 'blur'
}
];
return dynamicRules;
});
// 查询小区
const villageList = ref([]);
function getAllVillage() {
getCommunitylistAPI().then((res) => {
villageList.value = [...res.rows];
});
}
// 动态列名列表
const dynamicColumns = ref<Datum[]>([]);
// 在获取数据后更新动态列
const updateDynamicColumns = () => {
getLeaseManagementDetailChargeTypesList().then((res) => {
dynamicColumns.value = res.data;
queryParams.value.chargeTypeFilters = dynamicColumns.value.map((item) => {
return {
chargeTypeId: item.chargeTypeId,
chargeTypeName: item.chargeTypeName,
receivable: undefined,
received: undefined,
uncollected: undefined,
receivableMin: undefined,
receivableMax: undefined,
receivedMin: undefined,
receivedMax: undefined,
uncollectedMin: undefined,
uncollectedMax: undefined
};
});
});
};
/** 查询电抄记录列表 */
const getList = () => {
loading.value = true;
const search = { ...queryParams.value };
// 移除空字符串字段
Object.keys(search).forEach((key) => {
if (search[key] === '-1') {
search[key] = undefined;
}
if (key === 'chargeTypeFilters') {
search[key] = search[key]
.map((item) => {
// 1000-2000, -1000, 1000-, 1000
// 处理空字符串
if (!item.receivable && !item.received && !item.uncollected) {
return null;
}
const [receivableMin, receivableMax] = item.receivable ? item.receivable.split('-') : [];
const [receivedMin, receivedMax] = item.received ? item.received.split('-') : [];
const [uncollectedMin, uncollectedMax] = item.uncollected ? item.uncollected.split('-') : [];
return {
chargeTypeId: item.chargeTypeId,
receivableMin: receivableMin || undefined,
receivableMax: receivableMax || undefined,
receivedMin: receivedMin || undefined,
receivedMax: receivedMax || undefined,
uncollectedMin: uncollectedMin || undefined,
uncollectedMax: uncollectedMax || undefined
};
})
.filter(Boolean);
}
});
getLeaseManagementDetailList(search)
.then((res) => {
ArrearsDetailList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 搜索按钮操作 */
const handleQuery = () => {
// 校验表单
queryFormRef.value?.validate((valid) => {
if (valid) {
queryParams.value.pageNum = 1;
getList();
}
});
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
leaseStartDate.value = [];
leaseEndDate.value = [];
queryParams.value = {
pageNum: 1,
pageSize: 10,
villageIds: [],
lessee: undefined,
phone: undefined,
roomNumber: undefined,
areaSize: undefined,
chargeTypeFilters: [],
chargeModes: [],
chargeStandards: [],
leaseEndDateBegin: undefined,
leaseEndDateEnd: undefined
};
queryParams.value.chargeTypeFilters = dynamicColumns.value.map((item) => {
return {
chargeTypeId: item.chargeTypeId,
chargeTypeName: item.chargeTypeName,
receivable: undefined,
received: undefined,
uncollected: undefined,
receivableMin: undefined,
receivableMax: undefined,
receivedMin: undefined,
receivedMax: undefined,
uncollectedMin: undefined,
uncollectedMax: undefined
};
});
handleQuery();
};
/** 导出Excel */
const handleExport = async () => {
if (ArrearsDetailList.value.length === 0) {
proxy?.$modal.msgWarning('没有可导出的数据');
return;
}
// const oldSearch = JSON.parse(
// JSON.stringify({
// ...queryParams.value,
// pageNum: undefined,
// pageSize: undefined
// })
// );
loading.value = true;
// let allData = [];
try {
// const res = await getLeaseManagementDetailList(oldSearch);
// allData = res.rows;
// 显示加载提示
// 使用 setTimeout 让出主线程,避免阻塞UI
setTimeout(() => {
try {
const columns: ColumnConfig[] = [
{ label: '区域', prop: 'area' },
{ label: '承租方', prop: 'lessee' },
{ label: '电话', prop: 'phone' },
{ label: '房号', prop: 'roomNumber' },
{ label: '面积', prop: 'areaSize' },
{ label: '收费模式', prop: 'chargeMode' },
{ label: '收费标准', prop: 'chargeStandard' },
{ label: '租赁起始日', prop: 'leaseStartDate' },
{ label: '租赁终止日', prop: 'leaseEndDate' },
{
label: '应收项目',
children: [
...dynamicColumns.value.map((item) => {
return {
label: item.chargeTypeName,
prop: `receivableAmounts.${item.chargeTypeId}`
};
})
]
},
{
label: '已收项目',
children: [
...dynamicColumns.value.map((item) => {
return {
label: item.chargeTypeName,
prop: `receivedAmounts.${item.chargeTypeId}`
};
})
]
},
{
label: '未收项目',
children: [
...dynamicColumns.value.map((item) => {
return {
label: item.chargeTypeName,
prop: `uncollectedAmounts.${item.chargeTypeId}`
};
})
]
}
];
exportToExcel(ArrearsDetailList.value, columns, `租赁管理明细表_${new Date().getTime()}`);
} catch (error) {
proxy?.$modal.msgError('导出失败,请重试');
console.error('导出错误:', error);
} finally {
loading.value = false;
}
}, 100);
} catch (error) {
proxy?.$modal.msgError('导出失败,请重试');
loading.value = false;
}
};
onMounted(() => {
updateDynamicColumns();
getList();
getAllVillage();
});
</script>
<style lang="scss" scoped>
.line {
height: 1px;
background: #eee;
margin: 15px 0;
}
.queryBox_over {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
overflow: auto;
overflow-y: hidden;
& > div {
display: flex;
flex-wrap: nowrap;
}
}
.el-table {
height: calc(100% - 80px);
}
.arrow {
padding-right: 15px;
}
.arrowBox {
cursor: pointer;
&:hover {
color: #409eff;
}
}
</style>