feat: 租客管理功能开发

This commit is contained in:
Zzz1zz 2026-01-23 17:35:56 +08:00
parent f582098dc6
commit f7a710d6d2
13 changed files with 2355 additions and 12 deletions

BIN
dist.zip

Binary file not shown.

View file

@ -98,3 +98,41 @@ export function getRoomOverList(params: any): Promise<HttpResponse> {
export function getRoomsGraphList(params: any): Promise<HttpResponse> {
return axios.post('roomsGraph/getRoomList', params);
}
// ------------- 租客管理 -------------
//创建租客
export function createTenant(params: any): Promise<HttpResponse> {
return axios.post('tenants/createTenant', params);
}
// 租客列表(分页)
export function getTenantList(params: PaginationParams): Promise<HttpResponse> {
return axios.post('tenants/getTenantList', params);
}
// 更新租客信息
export function updateTenant(params: any): Promise<HttpResponse> {
return axios.post('tenants/updateTenant', params);
}
// 删除租客
export function delTenant(id: number): Promise<HttpResponse> {
return axios.get(`tenants/delTenant?id=${id}`);
}
// 租客详情
export function getTenantDetail(id: number): Promise<HttpResponse> {
return axios.get(`tenants/getTenant?id=${id}`);
}
// 租客归档
export function filingTenant(params: any): Promise<HttpResponse> {
return axios.post('tenants/filingTenant', params);
}
// 租客下拉列表(不分页)
export function getTenantAllList(
params: PaginationParams
): Promise<HttpResponse> {
return axios.post('tenants/getTenantAllList', params);
}
// 租客归档列表(分页)
export function getTenantFilingList(
params: PaginationParams
): Promise<HttpResponse> {
return axios.post('tenants/getTenantFilingList', params);
}

View file

@ -69,3 +69,130 @@ export const PRICE_UNIT_MAP = new Map([
[4, '元/月'],
[5, '元/年'],
]);
// 租客来源
export const TENANT_SOURCES = [
{ value: 1, text: '中介' },
{ value: 2, text: '招商获客' },
{ value: 3, text: '线下来访' },
{ value: 4, text: '客户转介' },
{ value: 5, text: '其他转介' },
];
// 租客映射表
export const TENANT_SOURCE_MAP = new Map([
[1, '中介'],
[2, '招商获客'],
[3, '线下来访'],
[4, '客户转介'],
[5, '其他转介'],
]);
// 租客类型
export const TENANT_TYPES = [
{ value: 1, text: '企业' },
{ value: 2, text: '个人' },
{ value: 3, text: '非营利组织' },
{ value: 4, text: '其他' },
];
// 租客映射表
export const TENANT_TYPE_MAP = new Map([
[1, '企业'],
[2, '个人'],
[3, '非营利组织'],
[4, '其他'],
]);
// 租客状态
// 状态:1正常 2欠费 3即将到期 4已到期 5可归档 6已归档
export const TENANT_STATUS = [
{ value: 1, text: '正常' },
{ value: 2, text: '欠费' },
{ value: 3, text: '即将到期' },
{ value: 4, text: '已到期' },
{ value: 5, text: '可归档' },
{ value: 6, text: '已归档' },
];
// 租客状态映射表
export const TENANT_STATUS_MAP = new Map([
[1, '正常'],
[2, '欠费'],
[3, '即将到期'],
[4, '已到期'],
[5, '可归档'],
[6, '已归档'],
]);
// 企业规模
export const COMPANY_SCALES = [
{ value: 1, text: '小微企业' },
{ value: 2, text: '中型企业' },
{ value: 3, text: '大型企业' },
{ value: 4, text: '集团/上市公司' },
];
// 企业规模映射表
export const COMPANY_SCALE_MAP = new Map([
[1, '小微企业'],
[2, '中型企业'],
[3, '大型企业'],
[4, '集团/上市公司'],
]);
// 行业分类
export const INDUSTRY_CATEGORIES = [
{ value: 1, text: '科技/互联网' },
{ value: 2, text: '金融/保险' },
{ value: 3, text: '专业服务(法律/咨询/会计)' },
{ value: 4, text: '医疗/健康' },
{ value: 5, text: '教育培训' },
{ value: 6, text: '零售/消费' },
{ value: 7, text: '制造业' },
{ value: 8, text: '文化/传媒' },
{ value: 9, text: '房地产/建筑' },
{ value: 10, text: '其他' },
];
// 行业分类映射表
export const INDUSTRY_CATEGORY_MAP = new Map([
[1, '科技/互联网'],
[2, '金融/保险'],
[3, '专业服务(法律/咨询/会计)'],
[4, '医疗/健康'],
[5, '教育培训'],
[6, '零售/消费'],
[7, '制造业'],
[8, '文化/传媒'],
[9, '房地产/建筑'],
[10, '其他'],
]);
// 发票类型
export const INVOICE_TYPES = [
{ value: 1, text: '增值税专用发票' },
{ value: 2, text: '增值税电子专用发票' },
{ value: 3, text: '增值税普通发票' },
{ value: 4, text: '增值税电子普通发票' },
{ value: 5, text: '全面数字化电子发票' },
];
// 发票类型映射表
export const INVOICE_TYPE_MAP = new Map([
[1, '增值税专用发票'],
[2, '增值税电子专用发票'],
[3, '增值税普通发票'],
[4, '增值税电子普通发票'],
[5, '全面数字化电子发票'],
]);
// 租客归档原因
export const TENANT_FILING_TYPES = [
{ value: 1, text: '到期退租(自然到期)' },
{ value: 2, text: '提前退租(协商终止)' },
{ value: 3, text: '非正常终止(违约或被清退)' },
{ value: 4, text: '签约未履行(签约后未生效)' },
{ value: 5, text: '其他原因' },
];
// 租客归档原因映射表
export const TENANT_FILING_TYPE_MAP = new Map([
[1, '到期退租(自然到期)'],
[2, '提前退租(协商终止)'],
[3, '非正常终止(违约或被清退)'],
[4, '签约未履行(签约后未生效)'],
[5, '其他原因'],
]);

View file

@ -50,7 +50,7 @@ export default {
'menu.business.contract-template': '合同模板',
'menu.business.tenant': '租客管理',
'menu.business.tenant-list': '租客列表',
'menu.business.tenant.contacts': '租客联系人',
'menu.business.tenant-file': '租客归档',
'add-project': '新建项目',
'edit-project': '编辑项目',
'menu.setting.user': '用户设置',
@ -124,6 +124,8 @@ export default {
'setting.data-dictionary': '数据字典',
'add-house': '新建房源',
'edit-house': '编辑房源',
'add-tenant': '新建租客',
'edit-tenant': '编辑租客',
...localeSettings,
...localeMessageBox,

View file

@ -182,7 +182,7 @@ const BUSINESS: AppRouteRecordRaw = {
component: () =>
import(
/* webpackChunkName: "business-tenant" */
'@/views/business/tenant/list.vue'
'@/views/business/tenant/list/index.vue'
),
meta: {
locale: 'menu.business.tenant-list',
@ -194,15 +194,15 @@ const BUSINESS: AppRouteRecordRaw = {
},
// 租客联系人
{
path: 'tenant-contacts',
name: 'business-tenant-contacts',
path: 'tenant-file',
name: 'business-tenant-file',
component: () =>
import(
/* webpackChunkName: "business-tenant" */
'@/views/business/tenant/contacts.vue'
'@/views/business/tenant/file/index.vue'
),
meta: {
locale: 'menu.business.tenant.contacts',
locale: 'menu.business.tenant-file',
requiresAuth: true,
roles: ['*'],
icon: 'icon-font-colors',

View file

@ -1,3 +0,0 @@
<template>
<div>tenant-contacts</div>
</template>

View file

@ -0,0 +1,300 @@
<template>
<a-drawer
:width="420"
:visible="visible"
title="筛选"
unmount-on-close
@ok="handleOk"
@cancel="handleCancel"
>
<a-form :model="filterForm" layout="vertical">
<!-- 租客类型(单选) -->
<a-form-item label="租客类型">
<a-select v-model="filterForm.tenantType" placeholder="请选择租客类型">
<a-option value="">所有类型</a-option>
<a-option
v-for="item in TENANT_TYPES"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-option>
</a-select>
</a-form-item>
<!-- 租客来源(单选) -->
<a-form-item label="租客来源">
<a-select
v-model="filterForm.tenantSource"
placeholder="请选择租客来源"
>
<a-option value="">所有来源</a-option>
<a-option
v-for="item in TENANT_SOURCES"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-option>
</a-select>
</a-form-item>
<!-- 归档原因(多选) -->
<a-form-item label="归档原因">
<a-select
v-model="filterForm.filingTypeIds"
multiple
placeholder="请选择归档原因"
:max-tag-count="3"
>
<a-option
v-for="item in TENANT_FILING_TYPES"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-option>
</a-select>
</a-form-item>
<!-- 信用评分(范围滑块) -->
<a-form-item label="信用评分">
<a-slider
v-model="filterForm.creditScore"
range
:min="creditScoreMin"
:max="creditScoreMax"
:marks="creditScoreMarks"
/>
</a-form-item>
</a-form>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, watch, computed } from 'vue';
import {
TENANT_TYPES,
TENANT_SOURCES,
TENANT_FILING_TYPES,
} from '@/constants';
const props = defineProps<{
visible: boolean;
tenantList?: any[]; // 租客列表数据,用于提取筛选选项
savedFilterState?: {
tenantType: number | null;
tenantSource: number | null;
filingTypeIds: number[];
creditScore: number[];
} | null; // 保存的筛选状态
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(
e: 'confirm',
payload: {
tenantType: number | null;
tenantSource: number | null;
filingTypeIds: number[];
creditScore: number[];
filteredList: any[]; // 筛选后的列表数据
}
): void;
}>();
// 筛选表单数据
const filterForm = ref({
tenantType: '' as number | string,
tenantSource: '' as number | string,
filingTypeIds: [] as number[],
creditScore: [0, 100] as number[],
});
// 信用评分范围
const creditScoreMin = computed(() => {
if (!props.tenantList || props.tenantList.length === 0) return 0;
const scores = props.tenantList
.map((tenant) => Number(tenant.creditScore))
.filter((score) => !isNaN(score) && score >= 0);
return scores.length > 0 ? Math.min(...scores) : 0;
});
const creditScoreMax = computed(() => {
if (!props.tenantList || props.tenantList.length === 0) return 100;
const scores = props.tenantList
.map((tenant) => Number(tenant.creditScore))
.filter((score) => !isNaN(score) && score >= 0);
return scores.length > 0 ? Math.max(...scores) : 100;
});
const creditScoreMarks = computed(() => {
return {
[creditScoreMin.value]: `${creditScoreMin.value}`,
[creditScoreMax.value]: `${creditScoreMax.value}`,
};
});
// 初始化筛选表单
const initFilterForm = () => {
const min = creditScoreMin.value || 0;
const max = creditScoreMax.value || 100;
// 如果有保存的筛选状态,恢复它
if (props.savedFilterState) {
filterForm.value = {
tenantType:
props.savedFilterState.tenantType === null
? ''
: props.savedFilterState.tenantType,
tenantSource:
props.savedFilterState.tenantSource === null
? ''
: props.savedFilterState.tenantSource,
filingTypeIds: [...props.savedFilterState.filingTypeIds],
creditScore:
props.savedFilterState.creditScore.length === 2
? [...props.savedFilterState.creditScore]
: [min, max],
};
} else {
// 否则初始化默认值
filterForm.value = {
tenantType: '',
tenantSource: '',
filingTypeIds: [],
creditScore: [min, max],
};
}
};
// 筛选列表数据
const filterTenantList = () => {
if (!props.tenantList || props.tenantList.length === 0) return [];
let filtered = [...props.tenantList];
// 筛选租客类型
if (
filterForm.value.tenantType !== '' &&
filterForm.value.tenantType !== null &&
filterForm.value.tenantType !== undefined &&
typeof filterForm.value.tenantType === 'number'
) {
filtered = filtered.filter(
(tenant) => tenant.type === filterForm.value.tenantType
);
}
// 筛选租客来源
if (
filterForm.value.tenantSource !== '' &&
filterForm.value.tenantSource !== null &&
filterForm.value.tenantSource !== undefined &&
typeof filterForm.value.tenantSource === 'number'
) {
filtered = filtered.filter(
(tenant) => tenant.source === filterForm.value.tenantSource
);
}
// 筛选归档原因
if (filterForm.value.filingTypeIds.length > 0) {
filtered = filtered.filter((tenant) =>
filterForm.value.filingTypeIds.includes(tenant.filingType)
);
}
// 筛选信用评分
const [minScore, maxScore] = filterForm.value.creditScore;
filtered = filtered.filter((tenant) => {
const score = Number(tenant.creditScore);
return !isNaN(score) && score >= minScore && score <= maxScore;
});
return filtered;
};
const handleCancel = () => {
emit('update:visible', false);
};
const handleOk = () => {
const filteredList = filterTenantList();
// 确保 undefined 值被转换为 null
const getTenantType = () => {
const value = filterForm.value.tenantType;
if (
value === '' ||
value === null ||
value === undefined ||
typeof value !== 'number'
) {
return null;
}
return value;
};
const getTenantSource = () => {
const value = filterForm.value.tenantSource;
if (
value === '' ||
value === null ||
value === undefined ||
typeof value !== 'number'
) {
return null;
}
return value;
};
emit('confirm', {
tenantType: getTenantType(),
tenantSource: getTenantSource(),
filingTypeIds: filterForm.value.filingTypeIds,
creditScore: filterForm.value.creditScore,
filteredList,
});
handleCancel();
};
// 监听抽屉打开,初始化数据
watch(
() => props.visible,
(val) => {
if (val) {
// 延迟初始化,确保 creditScoreMin 和 creditScoreMax 已计算
setTimeout(() => {
initFilterForm();
}, 0);
}
},
{ immediate: true }
);
// 监听保存的筛选状态变化,更新表单
watch(
() => props.savedFilterState,
() => {
if (props.visible) {
// 延迟更新,确保 creditScoreMin 和 creditScoreMax 已计算
setTimeout(() => {
initFilterForm();
}, 0);
}
},
{ deep: true }
);
</script>
<style lang="less" scoped>
:deep(label.arco-form-item-label) {
width: 100% !important;
}
:deep(.arco-slider-mark) {
text-wrap: nowrap;
}
</style>

View file

@ -0,0 +1,421 @@
<template>
<a-drawer
:width="670"
:visible="visible"
:title="isEdit ? '编辑租客' : '新建租客'"
unmount-on-close
@ok="handleOk"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
auto-label-width
layout="vertical"
>
<!-- 基本信息 -->
<p class="text-[18px] my-[10px] text-black">基本信息</p>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="租客名称" field="name" required>
<a-input v-model="form.name" placeholder="请输入租客名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="租客类型" field="type" required>
<a-select v-model="form.type" placeholder="请选择租客类型">
<a-option
v-for="item in TENANT_TYPES"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="租客来源" field="source">
<a-select v-model="form.source" placeholder="请选择租客来源">
<a-option
v-for="item in TENANT_SOURCES"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="行业分类" field="industry">
<a-select v-model="form.industry" placeholder="请选择租客行业分类">
<a-option
v-for="item in INDUSTRY_CATEGORIES"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="企业规模" field="enterprise">
<a-select v-model="form.enterprise" placeholder="请选择企业规模">
<a-option
v-for="item in COMPANY_SCALES"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="备注" field="remark">
<a-input v-model="form.remark" placeholder="请输入租客备注" />
</a-form-item>
</a-col>
</a-row>
<!-- 联系人信息 -->
<p class="text-[18px] my-[10px] text-black">联系人信息</p>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="联系人姓名" field="contactName" required>
<a-input
v-model="form.contactName"
placeholder="请输入联系人姓名"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="联系人电话" field="contactTel" required>
<a-input v-model="form.contactTel" placeholder="请输入联系人电话" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="联系人邮箱(接收通知单、发票)"
field="contactEmail"
>
<a-input
v-model="form.contactEmail"
placeholder="请输入联系人邮箱"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="联系人备注" field="contactRemark">
<a-input
v-model="form.contactRemark"
placeholder="请输入联系人其他信息"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 开票信息 -->
<p class="text-[18px] my-[10px] text-black">开票信息</p>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="发票类型" field="invoiceType">
<a-select v-model="form.invoiceType" placeholder="请选择发票类型">
<a-option
v-for="item in INVOICE_TYPES"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="纳税人识别号(统一社会信用代码)"
field="taxpayerIdentityNumber"
>
<a-input
v-model="form.taxpayerIdentityNumber"
placeholder="请输入纳税人识别号"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="基本开户账号" field="account">
<a-input v-model="form.account" placeholder="请输入基本开户账号" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="开户银行名称" field="bankName">
<a-input v-model="form.bankName" placeholder="请输入开户银行名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="注册固定电话" field="registerPhone">
<a-input
v-model="form.registerPhone"
placeholder="请输入注册固定电话"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="注册场所地址" field="registerAddress">
<a-input
v-model="form.registerAddress"
placeholder="请输入注册场所地址"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, watch, nextTick } from 'vue';
import { Message } from '@arco-design/web-vue';
import {
TENANT_TYPES,
TENANT_SOURCES,
INDUSTRY_CATEGORIES,
COMPANY_SCALES,
INVOICE_TYPES,
VALIDATION_RULES,
} from '@/constants';
import { createTenant, updateTenant } from '@/api/business';
const props = defineProps<{
visible: boolean;
isEdit?: boolean;
record?: any; // 编辑时的记录数据
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'submitted'): void; // 提交成功后的回调
}>();
const isLoading = ref<boolean>(false);
const formRef = ref();
// 表单数据
const form = ref({
name: '',
type: undefined as number | undefined,
source: undefined as number | undefined,
industry: undefined as number | undefined,
enterprise: undefined as number | undefined,
remark: '',
contactName: '',
contactTel: '',
contactEmail: '',
contactRemark: '',
invoiceType: undefined as number | undefined,
taxpayerIdentityNumber: '',
account: '',
bankName: '',
registerPhone: '',
registerAddress: '',
});
// 表单验证规则
const rules = {
name: [{ required: true, message: '请输入租客名称' }],
type: [{ required: true, message: '请选择租客类型' }],
contactName: [{ required: true, message: '请输入联系人姓名' }],
contactTel: [
{ required: true, message: '请输入联系人电话' },
{ match: VALIDATION_RULES.PHONE, message: '请输入正确的手机号码' },
],
contactEmail: [
{ match: VALIDATION_RULES.EMAIL, message: '请输入正确的邮箱地址' },
],
};
// 初始化表单
const initForm = () => {
if (props.isEdit && props.record) {
// 编辑模式,填充数据
form.value = {
name: props.record.name || '',
type:
props.record.type !== undefined && props.record.type !== null
? props.record.type
: undefined,
source:
props.record.source !== undefined && props.record.source !== null
? props.record.source
: undefined,
industry:
props.record.industry !== undefined && props.record.industry !== null
? props.record.industry
: undefined,
enterprise:
props.record.enterprise !== undefined &&
props.record.enterprise !== null
? props.record.enterprise
: undefined,
remark: props.record.remark || '',
contactName: props.record.contactName || '',
contactTel: props.record.contactTel || '',
contactEmail: props.record.contactEmail || '',
contactRemark: props.record.contactRemark || '',
invoiceType:
props.record.invoiceType !== undefined &&
props.record.invoiceType !== null
? props.record.invoiceType
: undefined,
taxpayerIdentityNumber: props.record.taxpayerIdentityNumber || '',
account: props.record.account || '',
bankName: props.record.bankName || '',
registerPhone: props.record.registerPhone || '',
registerAddress: props.record.registerAddress || '',
};
} else {
// 新建模式,重置表单
form.value = {
name: '',
type: undefined,
source: undefined,
industry: undefined,
enterprise: undefined,
remark: '',
contactName: '',
contactTel: '',
contactEmail: '',
contactRemark: '',
invoiceType: undefined,
taxpayerIdentityNumber: '',
account: '',
bankName: '',
registerPhone: '',
registerAddress: '',
};
}
};
// 处理提交
const handleOk = async () => {
if (isLoading.value) return;
try {
isLoading.value = true;
const valid = await formRef.value.validate();
if (valid) return;
// 构建提交数据,按照接口要求传递所有字段
const params: any = {
name: form.value.name || '',
type: form.value.type,
remark: form.value.remark || '',
contactName: form.value.contactName || '',
contactTel: form.value.contactTel || '',
contactEmail: form.value.contactEmail || '',
contactRemark: form.value.contactRemark || '',
taxpayerIdentityNumber: form.value.taxpayerIdentityNumber || '',
account: form.value.account || '',
bankName: form.value.bankName || '',
registerPhone: form.value.registerPhone || '',
registerAddress: form.value.registerAddress || '',
};
// 可选数字字段,只有选择了才传递
if (form.value.source !== undefined && form.value.source !== null) {
params.source = form.value.source;
}
if (form.value.industry !== undefined && form.value.industry !== null) {
params.industry = form.value.industry;
}
if (
form.value.enterprise !== undefined &&
form.value.enterprise !== null
) {
params.enterprise = form.value.enterprise;
}
if (
form.value.invoiceType !== undefined &&
form.value.invoiceType !== null
) {
params.invoiceType = form.value.invoiceType;
}
let response;
if (props.isEdit && props.record?.id) {
// 编辑模式
params.id = props.record.id;
response = await updateTenant(params);
} else {
// 新建模式
response = await createTenant(params);
}
if (response.code === 200) {
Message.success(
response.message || (props.isEdit ? '编辑成功' : '创建成功')
);
emit('submitted');
handleCancel();
} else {
Message.error(
response.message || (props.isEdit ? '编辑失败' : '创建失败')
);
}
} catch (error: any) {
Message.error(error.message || (props.isEdit ? '编辑失败' : '创建失败'));
} finally {
isLoading.value = false;
}
};
const handleCancel = () => {
formRef.value?.resetFields();
emit('update:visible', false);
};
// 初始化表单的辅助函数
const doInitForm = () => {
if (props.isEdit && props.record) {
// 编辑模式,确保有数据再初始化
initForm();
} else if (!props.isEdit) {
// 新建模式,直接初始化
initForm();
}
};
// 同时监听 visible、isEdit 和 record,确保在条件满足时初始化
watch(
() => [props.visible, props.isEdit, props.record],
([visible, isEdit, record]) => {
if (visible) {
isLoading.value = false;
if (isEdit && record) {
// 编辑模式,有数据时初始化
nextTick(() => {
doInitForm();
});
} else if (!isEdit) {
// 新建模式,直接初始化
nextTick(() => {
doInitForm();
});
}
} else {
// 关闭时重置表单
formRef.value?.resetFields();
}
},
{ immediate: true, deep: true }
);
</script>
<style lang="less" scoped>
:deep(.arco-divider-text) {
font-weight: 500;
font-size: 14px;
}
</style>

View file

@ -0,0 +1,299 @@
<template>
<a-drawer
:width="1000"
:visible="visible"
:footer="false"
unmount-on-close
@cancel="handleCancel"
>
<template #title>
<div class="flex items-center justify-between w-full pr-[20px]">
<span>租客详情</span>
<a-button type="text" @click="handleCancel">
<template #icon>
<icon-close />
</template>
</a-button>
</div>
</template>
<!-- 标题区域 -->
<div class="tenant-detail-header">
<div class="flex items-start justify-between mb-[16px]">
<div class="flex-1">
<h2 class="text-[20px] font-bold mb-[12px]">{{
detailInfo?.name || '-'
}}</h2>
<div class="flex items-center gap-[8px] flex-wrap">
<a-tag :color="getTypeColor(detailInfo?.type)">
{{ detailInfo?.typeName || '-' }}
</a-tag>
<a-tag color="green"> {{ contractCount }} 合同执行 </a-tag>
<a-tag color="red"> {{ overdueBillCount }} 账单逾期 </a-tag>
</div>
</div>
<div class="flex items-center gap-[12px]">
<span class="text-[12px] text-[#86909c]">
{{ formatUpdateTime(detailInfo?.updatedAt) }} 更新
</span>
<a-button @click="handleEdit">
<template #icon>
<icon-edit />
</template>
编辑
</a-button>
<a-popover trigger="click" position="bottom">
<a-button>更多</a-button>
<template #content>
<a-space direction="vertical">
<a-button>归档租客</a-button>
<a-button status="danger">删除租客</a-button>
</a-space>
</template>
</a-popover>
</div>
</div>
</div>
<!-- 标签页 -->
<a-tabs v-model:active-key="currentTab" class="tenant-detail-tabs">
<template #extra>
<!-- 信用评分 -->
<div class="flex items-center gap-[12px]">
<span class="text-[14px] font-medium">信用评分</span>
<a-progress
:percent="Number(detailInfo?.creditScore) || 0"
:color="
getCreditScoreColor(detailInfo?.status, detailInfo?.creditScore)
"
:show-text="false"
:style="{ width: '120px' }"
/>
<span class="text-[16px] font-bold">{{
detailInfo?.creditScore || 0
}}</span>
</div>
</template>
<a-tab-pane key="data" title="资料">
<!-- 基本信息 -->
<div class="mb-[24px]">
<p class="text-[16px] font-medium mb-[16px]">基本信息</p>
<a-descriptions :column="2" bordered>
<a-descriptions-item label="租客名称">
{{ detailInfo?.name || '-' }}
</a-descriptions-item>
<a-descriptions-item label="租客类型">
{{ detailInfo?.typeName || '-' }}
</a-descriptions-item>
<a-descriptions-item label="租客来源">
{{ detailInfo?.sourceName || '-' }}
</a-descriptions-item>
<a-descriptions-item label="行业分类">
{{ detailInfo?.industryName || '-' }}
</a-descriptions-item>
<a-descriptions-item label="企业规模">
{{ detailInfo?.enterpriseName || '-' }}
</a-descriptions-item>
<a-descriptions-item label="备注">
{{ detailInfo?.remark || '-' }}
</a-descriptions-item>
</a-descriptions>
</div>
<!-- 联系人信息 -->
<div class="mb-[24px]">
<p class="text-[16px] font-medium mb-[16px]">联系人信息</p>
<a-descriptions :column="2" bordered>
<a-descriptions-item label="联系人姓名">
{{ detailInfo?.contactName || '-' }}
</a-descriptions-item>
<a-descriptions-item label="联系人电话">
{{ detailInfo?.contactTel || '-' }}
</a-descriptions-item>
<a-descriptions-item label="联系人邮箱(接收通知单、发票)">
{{ detailInfo?.contactEmail || '-' }}
</a-descriptions-item>
<a-descriptions-item label="联系人备注">
{{ detailInfo?.contactRemark || '-' }}
</a-descriptions-item>
</a-descriptions>
</div>
<!-- 开票信息 -->
<div class="mb-[24px]">
<p class="text-[16px] font-medium mb-[16px]">开票信息</p>
<a-descriptions :column="2" bordered>
<a-descriptions-item label="发票类型">
{{ detailInfo?.invoiceTypeName || '-' }}
</a-descriptions-item>
<a-descriptions-item label="纳税人识别号(统一社会信用代码)">
{{ detailInfo?.taxpayerIdentityNumber || '-' }}
</a-descriptions-item>
<a-descriptions-item label="基本开户账号">
{{ detailInfo?.account || '-' }}
</a-descriptions-item>
<a-descriptions-item label="开户银行名称">
{{ detailInfo?.bankName || '-' }}
</a-descriptions-item>
<a-descriptions-item label="注册固定电话">
{{ detailInfo?.registerPhone || '-' }}
</a-descriptions-item>
<a-descriptions-item label="注册场所地址">
{{ detailInfo?.registerAddress || '-' }}
</a-descriptions-item>
</a-descriptions>
</div>
<!-- 备忘录 -->
<div>
<p class="text-[16px] font-medium mb-[16px]">备忘录</p>
<a-textarea
v-model="memoContent"
placeholder="可输入租客备忘或重要信息"
:auto-size="{ minRows: 6, maxRows: 10 }"
class="mb-[12px]"
/>
<div class="flex justify-end">
<a-button type="primary" :disabled="true">保存</a-button>
</div>
</div>
</a-tab-pane>
<a-tab-pane key="contract" title="合同">
<div class="text-center py-[40px] text-[#86909c]">合同功能待开发</div>
</a-tab-pane>
<a-tab-pane key="bill" title="账单">
<div class="text-center py-[40px] text-[#86909c]">账单功能待开发</div>
</a-tab-pane>
<a-tab-pane key="file" title="文件">
<div class="text-center py-[40px] text-[#86909c]">文件功能待开发</div>
</a-tab-pane>
</a-tabs>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, watch, computed } from 'vue';
import { TENANT_TYPE_MAP } from '@/constants';
const props = defineProps<{
visible: boolean;
detailInfo: any; // 租客详情数据
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'edit', id: number): void; // 编辑事件
}>();
const currentTab = ref<string>('data');
const memoContent = ref<string>('');
// 合同执行数量(暂时写死,后续从接口获取)
const contractCount = computed(() => {
// TODO: 从接口获取合同执行数量
// 暂时从 detailInfo 中获取,如果没有则显示 0
return props.detailInfo?.contractCount || 0;
});
// 账单逾期数量(暂时写死,后续从接口获取)
const overdueBillCount = computed(() => {
// TODO: 从接口获取账单逾期数量
// 暂时从 detailInfo 中获取,如果没有则显示 0
return props.detailInfo?.overdueBillCount || 0;
});
// 获取租客类型颜色
const getTypeColor = (type: number) => {
const colorMap: Record<number, string> = {
1: 'red', // 企业
2: 'blue', // 个人
3: 'green', // 非营利组织
4: 'orange', // 其他
};
return colorMap[type] || 'default';
};
// 根据状态获取信用评分进度条颜色
const getCreditScoreColor = (status: number, creditScore: number) => {
// 状态:1正常 2欠费 3即将到期 4已到期 5可归档 6已归档
// 欠费(状态2)和已到期(状态4)显示红色
if (status === 2 || status === 4) {
return 'red';
}
// 正常(状态1)显示绿色
if (status === 1) {
return 'green';
}
// 即将到期(状态3)显示橙色
if (status === 3) {
return 'orange';
}
// 可归档(状态5)和已归档(状态6)显示青色
if (status === 5 || status === 6) {
return 'cyan';
}
// 默认根据分数判断:低分(<60)显示红色,中分(60-80)显示橙色,高分(>80)显示绿色
const score = Number(creditScore) || 0;
if (score < 60) {
return 'red';
} else if (score < 80) {
return 'orange';
} else {
return 'green';
}
};
// 格式化更新时间
const formatUpdateTime = (time: string) => {
if (!time) return '-';
// 将 "2025-07-17 14:21:37" 格式化为 "2025-06-09 11:37:23"
return time;
};
const handleCancel = () => {
emit('update:visible', false);
};
const handleEdit = () => {
if (props.detailInfo?.id) {
emit('edit', props.detailInfo.id);
}
};
// 监听 visible 变化,重置备忘录内容
watch(
() => props.visible,
(val) => {
if (val) {
// 打开时重置备忘录(暂时不保存,所以每次打开都重置)
memoContent.value = '';
currentTab.value = 'data';
}
}
);
</script>
<style lang="less" scoped>
.tenant-detail-header {
padding-bottom: 16px;
}
.tenant-detail-tabs {
:deep(.arco-tabs-content) {
padding-top: 20px;
}
:deep(.arco-tabs-nav) {
margin-bottom: 0;
}
:deep(.arco-descriptions-item-label) {
background-color: #f7f8fa;
font-weight: 500;
}
}
:deep(.arco-descriptions-item-value) {
color: #1d2129;
}
</style>

View file

@ -0,0 +1,296 @@
<template>
<a-drawer
:width="420"
:visible="visible"
title="筛选"
unmount-on-close
@ok="handleOk"
@cancel="handleCancel"
>
<a-form :model="filterForm" layout="vertical">
<!-- 租客类型(单选) -->
<a-form-item label="租客类型">
<a-select v-model="filterForm.tenantType" placeholder="请选择租客类型">
<a-option value="">所有类型</a-option>
<a-option
v-for="item in TENANT_TYPES"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-option>
</a-select>
</a-form-item>
<!-- 租客来源(单选) -->
<a-form-item label="租客来源">
<a-select
v-model="filterForm.tenantSource"
placeholder="请选择租客来源"
>
<a-option value="">所有来源</a-option>
<a-option
v-for="item in TENANT_SOURCES"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-option>
</a-select>
</a-form-item>
<!-- 租客状态(多选) -->
<a-form-item label="租客状态">
<a-select
v-model="filterForm.statusIds"
multiple
placeholder="请选择租客状态"
:max-tag-count="3"
>
<a-option
v-for="item in TENANT_STATUS"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-option>
</a-select>
</a-form-item>
<!-- 信用评分(范围滑块) -->
<a-form-item label="信用评分">
<a-slider
v-model="filterForm.creditScore"
range
:min="creditScoreMin"
:max="creditScoreMax"
:marks="creditScoreMarks"
/>
</a-form-item>
</a-form>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, watch, computed } from 'vue';
import { TENANT_TYPES, TENANT_SOURCES, TENANT_STATUS } from '@/constants';
const props = defineProps<{
visible: boolean;
tenantList?: any[]; // 租客列表数据,用于提取筛选选项
savedFilterState?: {
tenantType: number | null;
tenantSource: number | null;
statusIds: number[];
creditScore: number[];
} | null; // 保存的筛选状态
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(
e: 'confirm',
payload: {
tenantType: number | null;
tenantSource: number | null;
statusIds: number[];
creditScore: number[];
filteredList: any[]; // 筛选后的列表数据
}
): void;
}>();
// 筛选表单数据
const filterForm = ref({
tenantType: '' as number | string,
tenantSource: '' as number | string,
statusIds: [] as number[],
creditScore: [0, 100] as number[],
});
// 信用评分范围
const creditScoreMin = computed(() => {
if (!props.tenantList || props.tenantList.length === 0) return 0;
const scores = props.tenantList
.map((tenant) => Number(tenant.creditScore))
.filter((score) => !isNaN(score) && score >= 0);
return scores.length > 0 ? Math.min(...scores) : 0;
});
const creditScoreMax = computed(() => {
if (!props.tenantList || props.tenantList.length === 0) return 100;
const scores = props.tenantList
.map((tenant) => Number(tenant.creditScore))
.filter((score) => !isNaN(score) && score >= 0);
return scores.length > 0 ? Math.max(...scores) : 100;
});
const creditScoreMarks = computed(() => {
return {
[creditScoreMin.value]: `${creditScoreMin.value}`,
[creditScoreMax.value]: `${creditScoreMax.value}`,
};
});
// 初始化筛选表单
const initFilterForm = () => {
const min = creditScoreMin.value || 0;
const max = creditScoreMax.value || 100;
// 如果有保存的筛选状态,恢复它
if (props.savedFilterState) {
filterForm.value = {
tenantType:
props.savedFilterState.tenantType === null
? ''
: props.savedFilterState.tenantType,
tenantSource:
props.savedFilterState.tenantSource === null
? ''
: props.savedFilterState.tenantSource,
statusIds: [...props.savedFilterState.statusIds],
creditScore:
props.savedFilterState.creditScore.length === 2
? [...props.savedFilterState.creditScore]
: [min, max],
};
} else {
// 否则初始化默认值
filterForm.value = {
tenantType: '',
tenantSource: '',
statusIds: [],
creditScore: [min, max],
};
}
};
// 筛选列表数据
const filterTenantList = () => {
if (!props.tenantList || props.tenantList.length === 0) return [];
let filtered = [...props.tenantList];
// 筛选租客类型
if (
filterForm.value.tenantType !== '' &&
filterForm.value.tenantType !== null &&
filterForm.value.tenantType !== undefined &&
typeof filterForm.value.tenantType === 'number'
) {
filtered = filtered.filter(
(tenant) => tenant.type === filterForm.value.tenantType
);
}
// 筛选租客来源
if (
filterForm.value.tenantSource !== '' &&
filterForm.value.tenantSource !== null &&
filterForm.value.tenantSource !== undefined &&
typeof filterForm.value.tenantSource === 'number'
) {
filtered = filtered.filter(
(tenant) => tenant.source === filterForm.value.tenantSource
);
}
// 筛选状态
if (filterForm.value.statusIds.length > 0) {
filtered = filtered.filter((tenant) =>
filterForm.value.statusIds.includes(tenant.status)
);
}
// 筛选信用评分
const [minScore, maxScore] = filterForm.value.creditScore;
filtered = filtered.filter((tenant) => {
const score = Number(tenant.creditScore);
return !isNaN(score) && score >= minScore && score <= maxScore;
});
return filtered;
};
const handleCancel = () => {
emit('update:visible', false);
};
const handleOk = () => {
const filteredList = filterTenantList();
// 确保空字符串或非数字值被转换为 null,只传递数字类型
const getTenantType = () => {
const value = filterForm.value.tenantType;
if (
value === '' ||
value === null ||
value === undefined ||
typeof value !== 'number'
) {
return null;
}
return value;
};
const getTenantSource = () => {
const value = filterForm.value.tenantSource;
if (
value === '' ||
value === null ||
value === undefined ||
typeof value !== 'number'
) {
return null;
}
return value;
};
emit('confirm', {
tenantType: getTenantType(),
tenantSource: getTenantSource(),
statusIds: filterForm.value.statusIds,
creditScore: filterForm.value.creditScore,
filteredList,
});
handleCancel();
};
// 监听抽屉打开,初始化数据
watch(
() => props.visible,
(val) => {
if (val) {
// 延迟初始化,确保 creditScoreMin 和 creditScoreMax 已计算
setTimeout(() => {
initFilterForm();
}, 0);
}
},
{ immediate: true }
);
// 监听保存的筛选状态变化,更新表单
watch(
() => props.savedFilterState,
() => {
if (props.visible) {
// 延迟更新,确保 creditScoreMin 和 creditScoreMax 已计算
setTimeout(() => {
initFilterForm();
}, 0);
}
},
{ deep: true }
);
</script>
<style lang="less" scoped>
:deep(label.arco-form-item-label) {
width: 100% !important;
}
:deep(.arco-slider-mark) {
text-wrap: nowrap;
}
</style>

View file

@ -0,0 +1,343 @@
<template>
<div>
<Breadcrumb :items="['menu.business', 'menu.business.tenant-file']" />
<a-card class="general-card full_height">
<template #title>
<div class="card-title_between">
<p class="title">{{ t('menu.business.tenant-file') }}</p>
<div class="flex_center_between">
<a-space>
<a-input-search
v-model="queryParams.keyword"
:style="{ width: '320px' }"
:placeholder="t('please.enter', { placeholder: t('content') })"
@search="handleSearch"
@press-enter="handleSearch"
/>
<a-button @click="handleFilter">
<template #icon>
<icon-filter />
</template>
{{ t('table.filter') }}
</a-button>
<a-button>
<template #icon>
<icon-export />
</template>
{{ t('table.export') }}
</a-button>
</a-space>
</div>
</div>
</template>
<!-- 使用新的 DataTable 组件 -->
<DataTable
:data="renderData"
:columns="tenantColumn"
:loading="loading"
:total="total"
:current="current"
:page-size="pageSize"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
>
<template #filingType="{ record }">
<a-tag :color="getFilingTypeColor(record?.filingType)">
{{ record?.filingTypeName || '-' }}
</a-tag>
</template>
<template #creditScore="{ record }">
<a-progress
:show-text="false"
:percent="Number(record.creditScore) || 0"
:color="getCreditScoreColor(record?.creditScore)"
:style="{ width: '120px' }"
/>
<span class="ml-[10px]">{{ record.creditScore }}</span>
</template>
<template #operate="{ record }">
<a-space>
<a-button @click="handleDetail(record.id)">查看</a-button>
</a-space>
</template>
</DataTable>
</a-card>
<!-- 筛选抽屉 -->
<tenant-filing-filter-drawer
v-model:visible="showListFilterDrawer"
:tenant-list="tenantList"
:saved-filter-state="savedFilterState"
@confirm="handleFilterConfirm"
/>
<!-- 租客详情抽屉 -->
<tenant-info-drawer
v-model:visible="showTenantInfoDrawer"
:detail-info="currentRecordInfo"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import useLoading from '@/hooks/loading';
import { useI18n } from 'vue-i18n';
import type { TableColumnData } from '@arco-design/web-vue';
import useDataTable from '@/composables/useDataTable';
import { getTenantFilingList, getTenantDetail } from '@/api/business';
import { ProjectPaginationParams } from '@/types/comm';
import { Message } from '@arco-design/web-vue';
import { TENANT_FILING_TYPE_MAP } from '@/constants';
import TenantFilingFilterDrawer from '../drawer/tenant-filing-filter-drawer.vue';
import TenantInfoDrawer from '../drawer/tenant-info-drawer.vue';
const { t } = useI18n();
const { loading } = useLoading(false);
const showTenantInfoDrawer = ref<boolean>(false);
const showListFilterDrawer = ref<boolean>(false);
const isLoadingDetail = ref<boolean>(false);
const currentRecordInfo = ref<any | null>(null);
const tenantList = ref<any[]>([]);
// 保存的筛选状态
const savedFilterState = ref<{
tenantType: number | null;
tenantSource: number | null;
filingTypeIds: number[];
creditScore: number[];
} | null>(null);
const {
data: renderData,
columns: tenantColumn,
total,
current,
pageSize,
setData,
setColumns,
setTotal,
setCurrent,
setPageSize,
setLoading,
} = useDataTable({
initialPage: 1,
initialPageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
});
// 通用空值处理函数
const renderEmpty = (value: any) => (value && value != '' ? value : '-');
// 列配置
const columns: TableColumnData[] = [
{
title: '租客名称',
dataIndex: 'name',
key: 'name',
render: ({ record }) => renderEmpty(record.name),
},
{
title: '租客类型',
dataIndex: 'typeName',
key: 'typeName',
render: ({ record }) => renderEmpty(record.typeName),
},
{
title: '联系人',
dataIndex: 'contactName',
key: 'contactName',
render: ({ record }) => renderEmpty(record.contactName),
},
{
title: '联系人电话',
dataIndex: 'contactTel',
key: 'contactTel',
render: ({ record }) => renderEmpty(record.contactTel),
},
{
title: '联系人邮箱',
dataIndex: 'contactEmail',
key: 'contactEmail',
render: ({ record }) => renderEmpty(record.contactEmail),
},
{
title: '租客来源',
dataIndex: 'sourceName',
key: 'sourceName',
render: ({ record }) => renderEmpty(record.sourceName),
},
{
title: '归档原因',
dataIndex: 'filingTypeName',
key: 'filingTypeName',
slotName: 'filingType',
},
{
title: '信用评分',
dataIndex: 'creditScore',
key: 'creditScore',
slotName: 'creditScore',
},
{ title: '操作', key: 'operate', slotName: 'operate', width: 200 },
];
const queryParams = ref<ProjectPaginationParams>({
page: current.value,
page_size: pageSize.value,
keyword: '',
});
const getTenantFilingListData = async () => {
setLoading(true);
const { code, data } = await getTenantFilingList(queryParams.value);
if (code === 200 && data) {
const { list, page } = data as {
list?: any[];
page?: { total?: number };
};
tenantList.value = Array.isArray(list) ? list : [];
setData(tenantList.value);
setTotal(page?.total ? page.total : 0);
}
setLoading(false);
};
const getTenantData = async (id: number) => {
if (currentRecordInfo.value && currentRecordInfo.value?.id === id) return;
isLoadingDetail.value = true;
try {
const { code, data } = await getTenantDetail(id);
if (code === 200 && data) {
// 根据接口返回结构,数据在 data.detail 中
const tenantData = (data as { detail?: any }).detail;
if (tenantData) {
currentRecordInfo.value = tenantData;
} else {
currentRecordInfo.value = null;
}
} else {
currentRecordInfo.value = null;
}
} catch (error) {
console.error('获取租客详情失败:', error);
currentRecordInfo.value = null;
} finally {
isLoadingDetail.value = false;
}
};
const handleFilterConfirm = (payload: {
tenantType: number | null;
tenantSource: number | null;
filingTypeIds: number[];
creditScore: number[];
filteredList: any[]; // 筛选后的列表数据
}) => {
// 保存筛选状态
savedFilterState.value = {
tenantType: payload.tenantType,
tenantSource: payload.tenantSource,
filingTypeIds: [...payload.filingTypeIds],
creditScore: [...payload.creditScore],
};
// 更新查询参数(用于后端筛选)
// keyword 已经在 queryParams 中,由搜索框输入
// 只有当值不为 null 或 undefined 时才设置,否则删除该字段
if (payload.tenantType !== null && payload.tenantType !== undefined) {
(queryParams.value as any).type = payload.tenantType;
} else {
delete (queryParams.value as any).type;
}
if (payload.tenantSource !== null && payload.tenantSource !== undefined) {
(queryParams.value as any).source = payload.tenantSource;
} else {
delete (queryParams.value as any).source;
}
(queryParams.value as any).filingType =
payload.filingTypeIds.length > 0 ? payload.filingTypeIds : undefined;
(queryParams.value as any).creditScoreMin = payload.creditScore?.[0];
(queryParams.value as any).creditScoreMax = payload.creditScore?.[1];
// 重置到第一页
setCurrent(1);
current.value = 1;
queryParams.value.page = 1;
// 重新加载列表数据
getTenantFilingListData();
};
// 分页事件处理
const onPageChange = (page: number) => {
setCurrent(page);
current.value = page;
queryParams.value.page = page;
getTenantFilingListData();
};
const onPageSizeChange = (size: number) => {
setPageSize(size);
pageSize.value = size;
queryParams.value.page_size = size;
// 切换页容量时重置到第一页
setCurrent(1);
current.value = 1;
queryParams.value.page = 1;
getTenantFilingListData();
};
const handleDetail = async (id: number) => {
if (isLoadingDetail.value) return;
// 先清空,确保能触发更新
currentRecordInfo.value = null;
await getTenantData(id);
// 确保数据已加载后再打开抽屉
if (currentRecordInfo.value) {
showTenantInfoDrawer.value = true;
} else {
Message.error('获取租客详情失败');
}
};
const handleSearch = () => {
// 搜索时重置到第一页
setCurrent(1);
current.value = 1;
queryParams.value.page = 1;
getTenantFilingListData();
};
const handleFilter = () => {
showListFilterDrawer.value = true;
};
// 获取归档原因颜色
const getFilingTypeColor = (filingType: number) => {
const colorMap: Record<number, string> = {
1: 'blue', // 到期退租
2: 'green', // 提前退租
3: 'red', // 非正常终止
4: 'orange', // 签约未履行
5: 'cyan', // 其他原因
};
return colorMap[filingType] || 'default';
};
// 根据信用评分获取进度条颜色
const getCreditScoreColor = (creditScore: number) => {
const score = Number(creditScore) || 0;
if (score < 60) {
return 'red';
} else if (score < 80) {
return 'orange';
} else {
return 'green';
}
};
onMounted(() => {
setColumns(columns);
getTenantFilingListData();
});
</script>

View file

@ -1,3 +0,0 @@
<template>
<div>tenant-list</div>
</template>

View file

@ -0,0 +1,523 @@
<template>
<div>
<Breadcrumb :items="['menu.business', 'menu.business.tenant-list']" />
<a-card class="general-card full_height">
<template #title>
<div class="card-title_between">
<p class="title">{{ t('menu.business.tenant-list') }}</p>
<div class="flex_center_between">
<a-space>
<a-input-search
v-model="queryParams.keyword"
:style="{ width: '320px' }"
:placeholder="t('please.enter', { placeholder: t('content') })"
@search="handleSearch"
@press-enter="handleSearch"
/>
<a-button @click="handleFilter">
<template #icon>
<icon-filter />
</template>
{{ t('table.filter') }}
</a-button>
<a-button>
<template #icon>
<icon-export />
</template>
{{ t('table.export') }}
</a-button>
<a-button>
<template #icon>
<icon-download />
</template>
{{ t('table.import') }}
</a-button>
<a-button type="primary" @click="handleAddTenant">
<template #icon>
<icon-plus />
</template>
{{ t('add-tenant') }}
</a-button>
</a-space>
</div>
</div>
</template>
<!-- 使用新的 DataTable 组件 -->
<DataTable
:data="renderData"
:columns="tenantColumn"
:loading="loading"
:total="total"
:current="current"
:page-size="pageSize"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
>
<template #status="{ record }">
<a-tag :color="getTagsColor(record?.status)">{{
record?.statusStr
}}</a-tag>
</template>
<template #creditScore="{ record }">
<a-progress
:show-text="false"
:percent="Number(record.creditScore) || 0"
:color="getCreditScoreColor(record?.status, record?.creditScore)"
:style="{ width: '120px' }"
/>
<span class="ml-[10px]">{{ record.creditScore }}</span>
</template>
<template #operate="{ record }">
<a-space>
<a-button @click="handleDetail(record.id)">查看</a-button>
<a-popover trigger="click" position="bottom">
<a-button>{{ $t('more') }}</a-button>
<template #content>
<a-space direction="vertical">
<a-button @click="handleEditTenant(record.id)">
编辑租客
</a-button>
<a-popconfirm
:ok-text="$t('cancel')"
:cancel-text="$t('delete')"
:ok-button-props="{ type: 'outline' }"
:cancel-button-props="{
type: 'primary',
status: 'danger',
}"
content="确定删除该租客?"
type="error"
@cancel="handleDel(record?.id)"
>
<a-button status="danger">删除租客</a-button>
</a-popconfirm>
<a-button @click="handleFilingTenant(record.id)">
归档租客
</a-button>
</a-space>
</template>
</a-popover>
</a-space>
</template>
</DataTable>
</a-card>
<!-- 筛选抽屉 -->
<tenant-list-filter-drawer
v-model:visible="showListFilterDrawer"
:tenant-list="tenantList"
:saved-filter-state="savedFilterState"
@confirm="handleFilterConfirm"
/>
<!-- 租客表单抽屉 -->
<tenant-form-drawer
v-model:visible="showDrawer"
:is-edit="isEdit"
:record="currentRecordInfo"
@submitted="handleFormSubmitted"
/>
<!-- 租客详情抽屉 -->
<tenant-info-drawer
v-model:visible="showTenantInfoDrawer"
:detail-info="currentRecordInfo"
@edit="handleEditFromDetail"
/>
<!-- 归档租客对话框 -->
<a-modal
v-model:visible="showFilingModal"
title="归档租客"
@ok="handleFilingConfirm"
@cancel="handleFilingCancel"
>
<a-form layout="vertical">
<a-form-item label="请选择归档原因" required>
<a-select v-model="filingType" placeholder="请选择归档原因">
<a-option
v-for="item in TENANT_FILING_TYPES"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import useLoading from '@/hooks/loading';
import { useI18n } from 'vue-i18n';
import type { TableColumnData } from '@arco-design/web-vue';
import useDataTable from '@/composables/useDataTable';
import {
getTenantList,
getTenantDetail,
delTenant,
filingTenant,
} from '@/api/business';
import { ProjectPaginationParams } from '@/types/comm';
import { Message, Modal } from '@arco-design/web-vue';
import { TENANT_FILING_TYPES } from '@/constants';
import TenantListFilterDrawer from '../drawer/tenant-list-filter-drawer.vue';
import TenantFormDrawer from '../drawer/tenant-form-drawer.vue';
import TenantInfoDrawer from '../drawer/tenant-info-drawer.vue';
const { t } = useI18n();
const { loading } = useLoading(false);
const showDrawer = ref<boolean>(false);
const showTenantInfoDrawer = ref<boolean>(false);
const showListFilterDrawer = ref<boolean>(false);
const showFilingModal = ref<boolean>(false);
const currentTenantId = ref<number | null>(null);
const filingType = ref<number | undefined>(undefined);
// 保存的筛选状态
const savedFilterState = ref<{
tenantType: number | null;
tenantSource: number | null;
statusIds: number[];
creditScore: number[];
} | null>(null);
const isLoadingEdit = ref<boolean>(false);
const isLoadingDetail = ref<boolean>(false);
const isEdit = ref<boolean>(false);
const currentRecordInfo = ref<any | null>(null);
const tenantList = ref<any[]>([]);
const {
data: renderData,
columns: tenantColumn,
total,
current,
pageSize,
setData,
setColumns,
setTotal,
setCurrent,
setPageSize,
setLoading,
} = useDataTable({
initialPage: 1,
initialPageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
});
// 通用空值处理函数
const renderEmpty = (value: any) => (value && value != '' ? value : '-');
// 列配置
const columns: TableColumnData[] = [
{
title: '租客名称',
dataIndex: 'name',
key: 'name',
render: ({ record }) => renderEmpty(record.name),
},
{
title: '租客类型',
dataIndex: 'typeName',
key: 'typeName',
render: ({ record }) => renderEmpty(record.typeName),
},
{
title: '联系人',
dataIndex: 'contactName',
key: 'contactName',
render: ({ record }) => renderEmpty(record.contactName),
},
{
title: '联系人电话',
dataIndex: 'contactTel',
key: 'contactTel',
render: ({ record }) => renderEmpty(record.contactTel),
},
{
title: '联系人邮箱',
dataIndex: 'contactEmail',
key: 'contactEmail',
render: ({ record }) => renderEmpty(record.contactEmail),
},
{
title: '租客来源',
dataIndex: 'sourceName',
key: 'sourceName',
render: ({ record }) => renderEmpty(record.sourceName),
},
{
title: '租客状态',
dataIndex: 'statusStr',
key: 'statusStr',
slotName: 'status',
},
{
title: '信用评分',
dataIndex: 'creditScore',
key: 'creditScore',
slotName: 'creditScore',
},
{ title: '操作', key: 'operate', slotName: 'operate', width: 200 },
];
const queryParams = ref<ProjectPaginationParams>({
page: current.value,
page_size: pageSize.value,
keyword: '',
});
const getTenantListData = async () => {
setLoading(true);
const { code, data } = await getTenantList(queryParams.value);
if (code === 200 && data) {
const { list, page } = data as {
list?: any[];
page?: { total?: number };
};
tenantList.value = Array.isArray(list) ? list : [];
setData(tenantList.value);
setTotal(page?.total ? page.total : 0);
}
setLoading(false);
};
const getTenantData = async (id: number, edit: boolean) => {
if (currentRecordInfo.value && currentRecordInfo.value?.id === id && !edit)
return;
if (edit) {
isLoadingEdit.value = true;
} else {
isLoadingDetail.value = true;
}
try {
const { code, data } = await getTenantDetail(id);
if (code === 200 && data) {
// 根据接口返回结构,数据在 data.detail 中
const tenantData = (data as { detail?: any }).detail;
if (tenantData) {
currentRecordInfo.value = tenantData;
} else {
currentRecordInfo.value = null;
}
} else {
currentRecordInfo.value = null;
}
} catch (error) {
console.error('获取租客详情失败:', error);
currentRecordInfo.value = null;
} finally {
isLoadingEdit.value = false;
isLoadingDetail.value = false;
}
};
const handleFilterConfirm = (payload: {
tenantType: number | null;
tenantSource: number | null;
statusIds: number[];
creditScore: number[];
filteredList: any[]; // 筛选后的列表数据
}) => {
// 保存筛选状态
savedFilterState.value = {
tenantType: payload.tenantType,
tenantSource: payload.tenantSource,
statusIds: [...payload.statusIds],
creditScore: [...payload.creditScore],
};
// 更新查询参数(用于后端筛选)
// keyword 已经在 queryParams 中,由搜索框输入
// 只有当值不为 null 或 undefined 时才设置,否则删除该字段
if (payload.tenantType !== null && payload.tenantType !== undefined) {
(queryParams.value as any).type = payload.tenantType;
} else {
delete (queryParams.value as any).type;
}
if (payload.tenantSource !== null && payload.tenantSource !== undefined) {
(queryParams.value as any).source = payload.tenantSource;
} else {
delete (queryParams.value as any).source;
}
(queryParams.value as any).status =
payload.statusIds.length > 0 ? payload.statusIds : undefined;
(queryParams.value as any).creditScoreMin = payload.creditScore?.[0];
(queryParams.value as any).creditScoreMax = payload.creditScore?.[1];
// 重置到第一页
setCurrent(1);
current.value = 1;
queryParams.value.page = 1;
// 重新加载列表数据
getTenantListData();
};
const handleDel = async (id: number) => {
const { code, message } = await delTenant(id);
if (code === 200) {
Message.success(message || '删除成功');
// 重新加载列表数据
await getTenantListData();
} else {
Message.error(message || '删除失败');
}
};
// 分页事件处理
const onPageChange = (page: number) => {
setCurrent(page);
current.value = page;
queryParams.value.page = page;
getTenantListData();
};
const onPageSizeChange = (size: number) => {
setPageSize(size);
pageSize.value = size;
queryParams.value.page_size = size;
// 切换页容量时重置到第一页
setCurrent(1);
current.value = 1;
queryParams.value.page = 1;
getTenantListData();
};
const handleAddTenant = () => {
isEdit.value = false;
currentRecordInfo.value = null;
showDrawer.value = true;
};
const handleEditTenant = async (id: number) => {
if (isLoadingEdit.value) return;
isEdit.value = true;
currentRecordInfo.value = null; // 先清空,确保能触发更新
// 先获取数据,再打开抽屉
await getTenantData(id, true);
// 确保数据已加载后再打开抽屉
if (currentRecordInfo.value) {
showDrawer.value = true;
} else {
Message.error('获取租客详情失败');
}
};
const handleFormSubmitted = () => {
// 表单提交成功后,重新加载列表数据
getTenantListData();
};
const handleDetail = async (id: number) => {
if (isLoadingDetail.value) return;
// 先清空,确保能触发更新
currentRecordInfo.value = null;
await getTenantData(id, false);
// 确保数据已加载后再打开抽屉
if (currentRecordInfo.value) {
showTenantInfoDrawer.value = true;
} else {
Message.error('获取租客详情失败');
}
};
// 从详情页点击编辑
const handleEditFromDetail = (id: number) => {
showTenantInfoDrawer.value = false;
handleEditTenant(id);
};
const handleSearch = () => {
// 搜索时重置到第一页
setCurrent(1);
current.value = 1;
queryParams.value.page = 1;
getTenantListData();
};
const handleFilter = () => {
showListFilterDrawer.value = true;
};
// 归档租客
const handleFilingTenant = (id: number) => {
currentTenantId.value = id;
filingType.value = undefined;
showFilingModal.value = true;
};
// 确认归档
const handleFilingConfirm = async () => {
if (!filingType.value) {
Message.warning('请选择归档原因');
return;
}
if (!currentTenantId.value) {
Message.error('租客ID不存在');
return;
}
try {
const { code, message } = await filingTenant({
id: currentTenantId.value,
filingType: filingType.value,
});
if (code === 200) {
Message.success(message || '归档成功');
showFilingModal.value = false;
filingType.value = undefined;
currentTenantId.value = null;
// 重新加载列表数据
await getTenantListData();
} else {
Message.error(message || '归档失败');
}
} catch (error: any) {
Message.error(error.message || '归档失败');
}
};
// 取消归档
const handleFilingCancel = () => {
showFilingModal.value = false;
filingType.value = undefined;
currentTenantId.value = null;
};
const getTagsColor = (status) => {
// "status": 1, //状态:1正常 2欠费 3即将到期 4已到期 5可归档 6已归档
const colorMap = {
1: 'green',
2: 'red',
3: 'orange',
4: 'red',
5: 'cyan',
6: 'cyan',
};
return colorMap[status];
};
// 根据状态获取信用评分进度条颜色
const getCreditScoreColor = (status, creditScore) => {
// 状态:1正常 2欠费 3即将到期 4已到期 5可归档 6已归档
// 欠费(状态2)和已到期(状态4)显示红色
if (status === 2 || status === 4) {
return 'red';
}
// 正常(状态1)显示绿色
if (status === 1) {
return 'green';
}
// 即将到期(状态3)显示橙色
if (status === 3) {
return 'orange';
}
// 可归档(状态5)和已归档(状态6)显示青色
if (status === 5 || status === 6) {
return 'cyan';
}
// 默认根据分数判断:低分(<60)显示红色,中分(60-80)显示橙色,高分(>80)显示绿色
const score = Number(creditScore) || 0;
if (score < 60) {
return 'red';
} else if (score < 80) {
return 'orange';
} else {
return 'green';
}
};
onMounted(() => {
setColumns(columns);
getTenantListData();
});
</script>