mirror of
http://183.237.82.114:10086/kilo/leaser_front.git
synced 2026-10-23 19:35:25 +00:00
feat: 空间管理 -> 房源列表开发
This commit is contained in:
parent
39ef0b7785
commit
a53cd0e8b8
15 changed files with 1637 additions and 28 deletions
|
|
@ -5,7 +5,11 @@ import {
|
|||
CreateProjectParam,
|
||||
ProjectDetailRes,
|
||||
} from '@/types/business';
|
||||
import { ProjectPaginationParams, CommonCityRes } from '@/types/comm';
|
||||
import {
|
||||
ProjectPaginationParams,
|
||||
CommonCityRes,
|
||||
PaginationParams,
|
||||
} from '@/types/comm';
|
||||
import { HttpResponse } from './interceptor';
|
||||
// import type { RouteRecordNormalized } from 'vue-router';
|
||||
// import { UserState } from '@/store/modules/user/types';
|
||||
|
|
@ -46,3 +50,43 @@ export function updateProject(
|
|||
export function getProjectCityList(): Promise<CommonCityRes> {
|
||||
return axios.get(`project/getCityList`);
|
||||
}
|
||||
|
||||
// ------------- 空间管理 -------------
|
||||
// 创建房源
|
||||
export function createRoom(params: any): Promise<HttpResponse> {
|
||||
return axios.post('rooms/createRoom', params);
|
||||
}
|
||||
// 房源列表(分页)
|
||||
export function getRoomList(params: PaginationParams): Promise<HttpResponse> {
|
||||
return axios.post('rooms/getRoomList', params);
|
||||
}
|
||||
// 更新房源
|
||||
export function updateRoom(params: any): Promise<HttpResponse> {
|
||||
return axios.post('rooms/updateRoom', params);
|
||||
}
|
||||
// 删除房源
|
||||
export function delRoom(id: number): Promise<HttpResponse> {
|
||||
return axios.get(`rooms/delRoom?id=${id}`);
|
||||
}
|
||||
// 房源详情
|
||||
export function getRoom(id: number): Promise<HttpResponse> {
|
||||
return axios.get(`rooms/getRoom?id=${id}`);
|
||||
}
|
||||
// 修改房源状态 status:1招商 2保留 3下架 4在租
|
||||
export function changeRoomStatus(params: any): Promise<HttpResponse> {
|
||||
return axios.post('rooms/changeRoomStatus', params);
|
||||
}
|
||||
// 房源获取头部楼栋tags
|
||||
export function getTopBuildingTagsByProjectId(
|
||||
id: number
|
||||
): Promise<HttpResponse> {
|
||||
return axios.get(`roomOver/getTopBuildingTags?project_id=${id}`);
|
||||
}
|
||||
// 房源剖视-数据统计
|
||||
export function getStatisticsData(params: any): Promise<HttpResponse> {
|
||||
return axios.post('roomOver/getStatisticsData', params);
|
||||
}
|
||||
// 房源剖视-房源列表
|
||||
export function getRoomOverList(params: any): Promise<HttpResponse> {
|
||||
return axios.post('roomOver/getRoomList', params);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,3 +42,30 @@ export const VALIDATION_RULES = {
|
|||
ID_CARD:
|
||||
/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/,
|
||||
} as const;
|
||||
|
||||
// 房源状态
|
||||
//状态:1招商 2保留 3下架 4在租
|
||||
export const HOUSE_STATUS = [
|
||||
{ id: 1, text: '招商' },
|
||||
{ id: 2, text: '保留' },
|
||||
{ id: 3, text: '下架' },
|
||||
{ id: 4, text: '在租' },
|
||||
];
|
||||
|
||||
// 价格单位选项
|
||||
export const PRICE_UNITS = [
|
||||
{ value: 1, text: '元/㎡*月' },
|
||||
{ value: 2, text: '元/㎡*天' },
|
||||
{ value: 3, text: '元/天' },
|
||||
{ value: 4, text: '元/月' },
|
||||
{ value: 5, text: '元/年' },
|
||||
] as const;
|
||||
|
||||
// 价格单位映射表
|
||||
export const PRICE_UNIT_MAP = new Map([
|
||||
[1, '元/㎡*月'],
|
||||
[2, '元/㎡*天'],
|
||||
[3, '元/天'],
|
||||
[4, '元/月'],
|
||||
[5, '元/年'],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export default {
|
|||
'content': '内容',
|
||||
'table.filter': '筛选',
|
||||
'table.export': '导出',
|
||||
'table.import': '导入',
|
||||
'watch-detail': '查看详情',
|
||||
'menu.business.project': '项目管理',
|
||||
'menu.business.space': '空间管理',
|
||||
|
|
@ -54,6 +55,7 @@ export default {
|
|||
'menu.business.tenant-list': '租客列表',
|
||||
'menu.business.tenant.contacts': '租客联系人',
|
||||
'add-project': '新建项目',
|
||||
'edit-project': '编辑项目',
|
||||
'menu.setting.user': '用户设置',
|
||||
'menu.setting.user-list': '用户列表',
|
||||
'menu.setting.role': '角色权限',
|
||||
|
|
@ -123,6 +125,8 @@ export default {
|
|||
'setting.contract': '合同设置',
|
||||
'setting.finance': '财务设置',
|
||||
'setting.data-dictionary': '数据字典',
|
||||
'add-house': '新建房源',
|
||||
'edit-house': '编辑房源',
|
||||
|
||||
...localeSettings,
|
||||
...localeMessageBox,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ const BUSINESS: AppRouteRecordRaw = {
|
|||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "business-space" */
|
||||
'@/views/business/space/house-view.vue'
|
||||
'@/views/business/space/house-view/index.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.business.house-view',
|
||||
|
|
@ -71,7 +71,7 @@ const BUSINESS: AppRouteRecordRaw = {
|
|||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "business-space" */
|
||||
'@/views/business/space/house-list.vue'
|
||||
'@/views/business/space/house-list/index.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.business.house-list',
|
||||
|
|
|
|||
|
|
@ -114,3 +114,20 @@ export interface ProjectInfo extends BaseEntity {
|
|||
export type ProjectDetailRes = HttpResponse<{
|
||||
project: ProjectItem;
|
||||
}>;
|
||||
|
||||
// 创建房源参数
|
||||
export interface CreateHouseParam {
|
||||
id?: number; // 编辑时需要
|
||||
projectId: number; // 项目id
|
||||
buildingId: number; // 楼栋id
|
||||
floorId: number; // 楼层id
|
||||
name: string; // 房源名称
|
||||
rentArea: number | string; // 计租面积
|
||||
price: number | string; // 价格
|
||||
priceType: number; // 价格类型
|
||||
area: number | string; // 面积
|
||||
useArea: number | string; // 使用面积
|
||||
status: number; // 状态
|
||||
objectKeys: string[]; // 图片数组
|
||||
tags: number[]; // 标签数组
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<a-drawer
|
||||
:width="670"
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑条款设置' : '新增条款设置'"
|
||||
:title="isEdit ? $t('edit-project') : $t('add-project')"
|
||||
unmount-on-close
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
|
|
@ -374,7 +374,7 @@
|
|||
emit('update:visible', false);
|
||||
};
|
||||
const handleDragEnd = () => {
|
||||
console.log('handleDragEnd');
|
||||
// 拖拽结束处理
|
||||
};
|
||||
onMounted(() => {
|
||||
getProvinceListData();
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
</a-button>
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<icon-download />
|
||||
<icon-export />
|
||||
</template>
|
||||
{{ t('table.export') }}
|
||||
</a-button>
|
||||
|
|
@ -184,7 +184,11 @@
|
|||
!edit
|
||||
)
|
||||
return;
|
||||
edit ? (isLoadingEdit.value = true) : (isLoadingDetail.value = true);
|
||||
if (edit) {
|
||||
isLoadingEdit.value = true;
|
||||
} else {
|
||||
isLoadingDetail.value = true;
|
||||
}
|
||||
const { code, data } = await getProjectDetail(id);
|
||||
if (code === 200 && data) {
|
||||
currentProjectInfo.value = data.project;
|
||||
|
|
@ -194,7 +198,7 @@
|
|||
};
|
||||
const handleWatchHouse = async (id: number) => {
|
||||
try {
|
||||
console.log(id, 'is id');
|
||||
// TODO: 实现查看房源功能
|
||||
} catch (error) {
|
||||
// 处理错误
|
||||
}
|
||||
|
|
@ -207,7 +211,7 @@
|
|||
getProjectListData();
|
||||
};
|
||||
const handleDel = (id: number) => {
|
||||
console.log(id, 'iiii');
|
||||
// TODO: 实现删除功能
|
||||
};
|
||||
// 分页事件处理
|
||||
const onPageChange = (page: number) => {
|
||||
|
|
|
|||
409
src/views/business/space/drawer/house-form-drawer.vue
Normal file
409
src/views/business/space/drawer/house-form-drawer.vue
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
<template>
|
||||
<a-drawer
|
||||
:width="670"
|
||||
:visible="visible"
|
||||
:title="isEdit ? $t('edit-house') : $t('add-house')"
|
||||
unmount-on-close
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<!-- 房源名称 -->
|
||||
<a-form-item label="房源名称" field="name" required>
|
||||
<a-input
|
||||
v-model="form.name"
|
||||
:placeholder="$t('please.enter', { placeholder: '房源名称' })"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<!-- 房源状态 -->
|
||||
<a-form-item label="房源状态" field="status" required>
|
||||
<a-select v-model="form.status" placeholder="请选择房源状态">
|
||||
<a-option
|
||||
v-for="item in HOUSE_STATUS"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.text }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<!-- 所属项目 -->
|
||||
<a-form-item label="所属项目" field="projectId" required>
|
||||
<a-select
|
||||
v-model="form.projectId"
|
||||
placeholder="请选择所属项目"
|
||||
@change="handleProjectChange"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in allProjetList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<!-- 所属楼栋/楼层 -->
|
||||
<a-form-item label="所属楼栋/楼层" field="buildingId" required>
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="12">
|
||||
<a-select
|
||||
v-model="form.buildingId"
|
||||
placeholder="请选择所属楼栋"
|
||||
:disabled="!form.projectId"
|
||||
@change="handleBuildingChange"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in allBuildingList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-select
|
||||
v-model="form.floorId"
|
||||
placeholder="请选择所属楼层"
|
||||
:disabled="!form.buildingId"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in allFloorList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="计租面积" field="rentArea" required>
|
||||
<a-input-number
|
||||
v-model="form.rentArea"
|
||||
append="㎡"
|
||||
:placeholder="$t('please.enter', { placeholder: '房源计租面积' })"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="招商价格" field="price">
|
||||
<a-input-number
|
||||
v-model="form.price"
|
||||
:placeholder="
|
||||
$t('please.enter', { placeholder: '房源计租招商价格' })
|
||||
"
|
||||
>
|
||||
<template #append>
|
||||
<a-select v-model="form.priceType" style="width: 100px">
|
||||
<a-option
|
||||
v-for="item in PRICE_UNITS"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>{{ item.text }}</a-option
|
||||
>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="建筑面积(可选)" field="area">
|
||||
<a-input
|
||||
v-model="form.area"
|
||||
append="㎡"
|
||||
:placeholder="$t('please.enter', { placeholder: '建筑面积' })"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="使用面积(可选)" field="useArea">
|
||||
<a-input
|
||||
v-model="form.useArea"
|
||||
append="㎡"
|
||||
:placeholder="$t('please.enter', { placeholder: '使用面积' })"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="标签(可选)" field="tags">
|
||||
<a-select v-model="form.tags" multiple placeholder="请选择标签">
|
||||
<a-option
|
||||
v-for="item in roomTagsList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="房源图片(可选)" field="objectKeys">
|
||||
<ImageUpload
|
||||
v-model:object-keys="form.objectKeys"
|
||||
v-model:signed-put-u-r-l="form.signedPutURL"
|
||||
:upload-type="2"
|
||||
:max-count="5"
|
||||
list-type="picture-card"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, reactive, onMounted } from 'vue';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { CreateHouseParam } from '@/types/business';
|
||||
import ImageUpload from '@/components/common/ImageUpload.vue';
|
||||
import { HOUSE_STATUS, PRICE_UNITS } from '@/constants';
|
||||
import {
|
||||
getAllProjectIdNameList,
|
||||
getAllBuildingIdNameList,
|
||||
getAllFloorIdNameList,
|
||||
} from '@/api/common';
|
||||
import { getRoomTagList } from '@/api/setting';
|
||||
import { createRoom, updateRoom } from '@/api/business';
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
isEdit: boolean;
|
||||
record?: any | null;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
(e: 'submitted'): void;
|
||||
}>();
|
||||
const isLoading = ref<boolean>(false);
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const allProjetList = ref<any>([]);
|
||||
const allBuildingList = ref<any>([]);
|
||||
const allFloorList = ref<any>([]);
|
||||
const roomTagsList = ref<any[]>([]);
|
||||
const defaultForm: CreateHouseParam = {
|
||||
projectId: null as any,
|
||||
buildingId: null as any,
|
||||
floorId: null as any,
|
||||
name: '', // 房源名称
|
||||
rentArea: '', // 计租面积
|
||||
price: '', // 价格
|
||||
priceType: 1, // 价格类型
|
||||
area: '', // 面积
|
||||
useArea: '', // 使用面积
|
||||
status: 1, // 状态
|
||||
objectKeys: [], // 图片数组
|
||||
tags: [], // 标签数组
|
||||
};
|
||||
// 扩展表单类型,包含 ImageUpload 需要的 signedPutURL(不提交到后端)
|
||||
const form = reactive<CreateHouseParam & { signedPutURL: string[] }>({
|
||||
...defaultForm,
|
||||
signedPutURL: [],
|
||||
});
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: '请输入房源名称' }],
|
||||
projectId: [{ required: true, message: '请选择项目' }],
|
||||
buildingId: [{ required: true, message: '请选择楼栋' }],
|
||||
floorId: [{ required: true, message: '请选择楼层' }],
|
||||
status: [{ required: true, message: '请选择房源状态' }],
|
||||
});
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
if (props.isEdit && props.record) {
|
||||
const record = props.record;
|
||||
// 回显数据 - 使用 Object.assign 批量赋值,处理特殊字段转换
|
||||
Object.assign(form, {
|
||||
id: record.id,
|
||||
projectId: record.projectId,
|
||||
buildingId: record.buildingId,
|
||||
floorId: record.floorId,
|
||||
name: record.name || '',
|
||||
rentArea: record.rentArea || 0,
|
||||
price: record.price || 0,
|
||||
priceType:
|
||||
PRICE_UNITS.find((i) => i.text === record.priceType)?.value ?? 1,
|
||||
area: record.area || 0,
|
||||
useArea: record.useArea || 0,
|
||||
status: record.status || 1,
|
||||
objectKeys: record.objectKeys || record.imageUrls || [],
|
||||
tags: [
|
||||
...new Set(record?.tags?.map((i) => i.id).filter(Boolean) || []),
|
||||
],
|
||||
signedPutURL: record.signedPutURL || [],
|
||||
});
|
||||
// 编辑模式下,需要加载对应的楼栋和楼层列表
|
||||
if (record.projectId) {
|
||||
await getAllBuildingListData(record.projectId);
|
||||
if (record.buildingId) {
|
||||
await getAllFloorListData(record.buildingId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Object.assign(form, { ...defaultForm, signedPutURL: [] });
|
||||
// 清空楼栋和楼层列表
|
||||
allBuildingList.value = [];
|
||||
allFloorList.value = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
const handleOk = async () => {
|
||||
if (isLoading.value) return;
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const valid = await formRef.value.validate();
|
||||
if (valid) return;
|
||||
// 手动验证楼层字段(因为楼栋和楼层在同一个form-item中)
|
||||
if (!form.floorId) {
|
||||
formRef.value.setFields({
|
||||
floorId: { status: 'error', message: '请选择楼层' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 构建提交参数,排除 signedPutURL(仅用于前端显示)
|
||||
// 将面积、价格相关字段的空值转为0
|
||||
const params: CreateHouseParam = {
|
||||
projectId: form.projectId,
|
||||
buildingId: form.buildingId,
|
||||
floorId: form.floorId,
|
||||
name: form.name,
|
||||
rentArea: form.rentArea || 0,
|
||||
price: form.price || 0,
|
||||
priceType: form.priceType,
|
||||
area: form.area || 0,
|
||||
useArea: form.useArea || 0,
|
||||
status: form.status,
|
||||
objectKeys: form.objectKeys,
|
||||
tags: form.tags,
|
||||
};
|
||||
// 如果是编辑模式,需要包含 id
|
||||
if (props.isEdit && form.id) {
|
||||
params.id = form.id;
|
||||
}
|
||||
// 调用创建/更新房源的 API
|
||||
const res = props.isEdit
|
||||
? await updateRoom(params)
|
||||
: await createRoom(params);
|
||||
if (res?.code === 200) {
|
||||
Message.success(
|
||||
res?.message || (props.isEdit ? '更新成功' : '创建成功')
|
||||
);
|
||||
// 清空表单数据
|
||||
Object.assign(form, { ...defaultForm, signedPutURL: [] });
|
||||
allBuildingList.value = [];
|
||||
allFloorList.value = [];
|
||||
formRef.value?.clearValidate();
|
||||
emit('submitted');
|
||||
emit('update:visible', false);
|
||||
}
|
||||
} catch (err: any) {
|
||||
Message.error(err?.message || (props.isEdit ? '更新失败' : '创建失败'));
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
async function getAllProjectListData() {
|
||||
try {
|
||||
const { code, data } = await getAllProjectIdNameList();
|
||||
if (code === 200) {
|
||||
allProjetList.value = data?.projectsList || [];
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('获取项目列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getAllBuildingListData(projectId: number) {
|
||||
try {
|
||||
const { code, data } = await getAllBuildingIdNameList(projectId);
|
||||
if (code === 200) {
|
||||
allBuildingList.value = data?.buildingList || [];
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('获取楼栋列表失败:', error);
|
||||
allBuildingList.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getAllFloorListData(buildingId: number) {
|
||||
try {
|
||||
const { code, data } = await getAllFloorIdNameList(buildingId);
|
||||
if (code === 200) {
|
||||
allFloorList.value = data?.floorList || [];
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('获取楼层列表失败:', error);
|
||||
allFloorList.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 项目选择变化处理
|
||||
const handleProjectChange = async (projectId: number) => {
|
||||
// 清空楼栋和楼层选择
|
||||
form.buildingId = null as any;
|
||||
form.floorId = null as any;
|
||||
allBuildingList.value = [];
|
||||
allFloorList.value = [];
|
||||
// 如果选择了项目,加载楼栋列表
|
||||
if (projectId) {
|
||||
await getAllBuildingListData(projectId);
|
||||
}
|
||||
};
|
||||
|
||||
// 楼栋选择变化处理
|
||||
const handleBuildingChange = async (buildingId: number) => {
|
||||
// 清空楼层选择
|
||||
form.floorId = null as any;
|
||||
allFloorList.value = [];
|
||||
// 如果选择了楼栋,加载楼层列表
|
||||
if (buildingId) {
|
||||
await getAllFloorListData(buildingId);
|
||||
}
|
||||
};
|
||||
const getAllRoomTagListData = async () => {
|
||||
try {
|
||||
const { code, data } = await getRoomTagList();
|
||||
if (code === 200 && data) {
|
||||
roomTagsList.value = data?.roomTagsList;
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化数据加载
|
||||
getAllProjectListData();
|
||||
getAllRoomTagListData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(label.arco-form-item-label) {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
155
src/views/business/space/drawer/house-info-drawer.vue
Normal file
155
src/views/business/space/drawer/house-info-drawer.vue
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
<template>
|
||||
<a-drawer
|
||||
:width="520"
|
||||
:visible="visible"
|
||||
title="房源详情"
|
||||
:footer="false"
|
||||
unmount-on-close
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p>
|
||||
{{
|
||||
`${detailInfo?.name} -
|
||||
${detailInfo?.projectName}${detailInfo?.buildingName}${detailInfo?.floorName}`
|
||||
}}
|
||||
</p>
|
||||
<div class="mt-[10px]">
|
||||
<a-tag :color="getTagsColor(detailInfo?.status)">{{
|
||||
detailInfo?.statusStr
|
||||
}}</a-tag>
|
||||
<a-tag class="mx-[10px]">{{ detailInfo?.rentArea }}㎡</a-tag>
|
||||
<a-tag>{{ `${detailInfo?.price}${detailInfo?.priceType}` }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <a-button>
|
||||
<template #icon>
|
||||
<icon-edit />
|
||||
</template>
|
||||
{{ $t('edit') }}
|
||||
</a-button> -->
|
||||
</div>
|
||||
<div class="mt-[20px]">
|
||||
<a-tabs v-model:active-key="currentTab" class="">
|
||||
<a-tab-pane key="1">
|
||||
<template #title> <icon-calendar /> 基本信息 </template>
|
||||
<a-alert type="warning">房源空置 xx 天</a-alert>
|
||||
<a-descriptions bordered class="mt-[10px]" :column="2">
|
||||
<a-descriptions-item label="房源编码" :span="1">
|
||||
{{ detailInfo?.code || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="房源名称" :span="1">
|
||||
{{ detailInfo?.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="所属项目" :span="1">
|
||||
{{ detailInfo?.projectName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="所属楼栋/楼层" :span="1">
|
||||
<span>{{ detailInfo?.buildingName || '-' }}</span>
|
||||
<span>*</span>
|
||||
<span>{{ detailInfo?.floorName || '-' }}</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="计租面积" :span="1">
|
||||
{{
|
||||
Number(detailInfo?.rentArea) > 0
|
||||
? `${detailInfo?.rentArea}㎡`
|
||||
: '-'
|
||||
}}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="招商价格" :span="1">
|
||||
{{
|
||||
Number(detailInfo?.price) > 0
|
||||
? `${detailInfo?.rentArea}${detailInfo?.priceType}`
|
||||
: '-'
|
||||
}}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="建筑面积" :span="1">
|
||||
{{ Number(detailInfo?.area) > 0 ? `${detailInfo?.area}㎡` : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="使用面积" :span="1">
|
||||
{{
|
||||
Number(detailInfo?.useArea) > 0
|
||||
? `${detailInfo?.useArea}㎡`
|
||||
: '-'
|
||||
}}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="标签" :span="2">
|
||||
<template v-if="detailInfo?.tags?.length">
|
||||
<a-tag
|
||||
v-for="item in detailInfo?.tags"
|
||||
:key="item.id || item.name"
|
||||
class="mr-[5px] last:mr-0"
|
||||
>
|
||||
{{ item.name }}</a-tag
|
||||
>
|
||||
</template>
|
||||
<template v-else> - </template>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="房源图片" :span="2">
|
||||
<template v-if="detailInfo?.signedPutURL?.length">
|
||||
<img
|
||||
v-for="(item, index) in detailInfo?.signedPutURL"
|
||||
:key="index"
|
||||
:src="item"
|
||||
alt=""
|
||||
class="mt-[5px] last:mr-0 w-[120px] h-[80px] object-contain"
|
||||
/>
|
||||
</template>
|
||||
<template v-else> - </template>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2">
|
||||
<template #title> <icon-clock-circle /> 合同信息 </template>
|
||||
合同信息与租客信息 待开发
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3">
|
||||
<template #title> <icon-user /> 租客信息 </template>
|
||||
合同信息与租客信息 待开发
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted, ref } from 'vue';
|
||||
// import { Message } from '@arco-design/web-vue';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
projectId: number | null;
|
||||
detailInfo: any;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
}>();
|
||||
const currentTab = ref<string>('1');
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (val) => {
|
||||
// 抽屉显示状态变化
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
const getTagsColor = (status) => {
|
||||
const colorMap = {
|
||||
1: 'orangered',
|
||||
2: 'orange',
|
||||
3: 'red',
|
||||
4: 'green',
|
||||
};
|
||||
return colorMap[status];
|
||||
};
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(label.arco-form-item-label) {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
624
src/views/business/space/drawer/house-list-filter-drawer.vue
Normal file
624
src/views/business/space/drawer/house-list-filter-drawer.vue
Normal file
|
|
@ -0,0 +1,624 @@
|
|||
<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="项目">
|
||||
<span class="w-full bg-[#f2f3f5] h-[30px] px-[5px] leading-[30px]">{{
|
||||
projectName
|
||||
}}</span>
|
||||
<!-- <a-input :value="projectName" disabled /> -->
|
||||
</a-form-item>
|
||||
|
||||
<!-- 楼栋(多选) -->
|
||||
<a-form-item label="楼栋">
|
||||
<a-select
|
||||
v-model="filterForm.buildingIds"
|
||||
multiple
|
||||
placeholder="请选择楼栋"
|
||||
:max-tag-count="3"
|
||||
>
|
||||
<a-option :value="ALL_BUILDING_ID">所有楼栋</a-option>
|
||||
<a-option
|
||||
v-for="item in buildingList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 楼层(多选) -->
|
||||
<a-form-item label="楼层">
|
||||
<a-select
|
||||
v-model="filterForm.floorIds"
|
||||
multiple
|
||||
placeholder="请选择楼层"
|
||||
:max-tag-count="3"
|
||||
>
|
||||
<a-option :value="ALL_FLOOR_ID">所有楼层</a-option>
|
||||
<a-option v-for="item in floorList" :key="item.id" :value="item.id">
|
||||
{{ item.name }}
|
||||
</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 HOUSE_STATUS"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.text }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 计租面积(滑块) -->
|
||||
<a-form-item label="计租面积(㎡)">
|
||||
<a-slider
|
||||
v-model="filterForm.rentArea"
|
||||
range
|
||||
:min="rentAreaMin"
|
||||
:max="rentAreaMax"
|
||||
:marks="rentAreaMarks"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 标签(多选) -->
|
||||
<a-form-item label="标签">
|
||||
<a-select
|
||||
v-model="filterForm.tagIds"
|
||||
multiple
|
||||
placeholder="请选择标签"
|
||||
:max-tag-count="3"
|
||||
>
|
||||
<a-option v-for="item in tagList" :key="item.id" :value="item.id">
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, onMounted, nextTick } from 'vue';
|
||||
import { HOUSE_STATUS } from '@/constants';
|
||||
import { getRoomTagList } from '@/api/setting';
|
||||
|
||||
// 特殊值用于表示"所有"选项(使用负数避免与真实ID冲突)
|
||||
const ALL_BUILDING_ID = -1;
|
||||
const ALL_FLOOR_ID = -2;
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
roomList?: any[]; // 房源列表数据,用于提取筛选选项
|
||||
projectName?: string; // 项目名称(仅显示)
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
(
|
||||
e: 'confirm',
|
||||
payload: {
|
||||
buildingIds: number[] | null;
|
||||
floorIds: number[] | null;
|
||||
statusIds: number[] | null;
|
||||
rentArea: number[];
|
||||
tagIds: number[] | null;
|
||||
filteredList: any[]; // 筛选后的列表数据
|
||||
}
|
||||
): void;
|
||||
}>();
|
||||
|
||||
// 保存的筛选状态
|
||||
const savedFilterState = ref<{
|
||||
buildingIds: number[];
|
||||
floorIds: number[];
|
||||
statusIds: number[];
|
||||
rentArea: number[];
|
||||
tagIds: number[];
|
||||
} | null>(null);
|
||||
|
||||
// 筛选表单数据
|
||||
const filterForm = ref({
|
||||
buildingIds: [] as number[],
|
||||
floorIds: [] as number[],
|
||||
statusIds: [] as number[],
|
||||
rentArea: [0, 0] as number[],
|
||||
tagIds: [] as number[],
|
||||
});
|
||||
|
||||
// 楼栋列表(从房源列表中提取唯一值)
|
||||
const buildingList = computed(() => {
|
||||
if (!props.roomList || props.roomList.length === 0) return [];
|
||||
const buildingMap = new Map();
|
||||
props.roomList.forEach((room) => {
|
||||
if (room.buildingId && room.buildingName) {
|
||||
if (!buildingMap.has(room.buildingId)) {
|
||||
buildingMap.set(room.buildingId, {
|
||||
id: room.buildingId,
|
||||
name: room.buildingName,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return Array.from(buildingMap.values());
|
||||
});
|
||||
|
||||
// 楼层列表(从房源列表中提取唯一值)
|
||||
const floorList = computed(() => {
|
||||
if (!props.roomList || props.roomList.length === 0) return [];
|
||||
const floorMap = new Map();
|
||||
props.roomList.forEach((room) => {
|
||||
if (room.floorId && room.floorName) {
|
||||
if (!floorMap.has(room.floorId)) {
|
||||
floorMap.set(room.floorId, {
|
||||
id: room.floorId,
|
||||
name: room.floorName,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return Array.from(floorMap.values());
|
||||
});
|
||||
|
||||
// 计租面积范围
|
||||
const rentAreaMin = computed(() => {
|
||||
if (!props.roomList || props.roomList.length === 0) return 0;
|
||||
const areas = props.roomList
|
||||
.map((room) => Number(room.rentArea))
|
||||
.filter((area) => !isNaN(area) && area > 0);
|
||||
return areas.length > 0 ? Math.min(...areas) : 0;
|
||||
});
|
||||
|
||||
const rentAreaMax = computed(() => {
|
||||
if (!props.roomList || props.roomList.length === 0) return 100;
|
||||
const areas = props.roomList
|
||||
.map((room) => Number(room.rentArea))
|
||||
.filter((area) => !isNaN(area) && area > 0);
|
||||
return areas.length > 0 ? Math.max(...areas) : 100;
|
||||
});
|
||||
|
||||
const rentAreaMarks = computed(() => {
|
||||
return {
|
||||
[rentAreaMin.value]: `${rentAreaMin.value}㎡`,
|
||||
[rentAreaMax.value]: `${rentAreaMax.value}㎡`,
|
||||
};
|
||||
});
|
||||
|
||||
// 标签列表
|
||||
const tagList = ref<any[]>([]);
|
||||
|
||||
// 获取标签列表
|
||||
const getTagListData = async () => {
|
||||
try {
|
||||
const { code, data } = await getRoomTagList();
|
||||
if (code === 200 && data) {
|
||||
tagList.value = data?.roomTagsList || [];
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化筛选表单
|
||||
const initFilterForm = () => {
|
||||
const min = rentAreaMin.value || 0;
|
||||
const max = rentAreaMax.value || 100;
|
||||
|
||||
// 如果有保存的状态,恢复它(但要过滤掉不存在的ID)
|
||||
if (savedFilterState.value) {
|
||||
const allBuildingIds = buildingList.value.map((b) => b.id);
|
||||
const allFloorIds = floorList.value.map((f) => f.id);
|
||||
|
||||
// 如果保存的状态中包含"所有"选项,直接使用
|
||||
let buildingIds = savedFilterState.value.buildingIds;
|
||||
let floorIds = savedFilterState.value.floorIds;
|
||||
|
||||
// 如果保存的状态中不包含"所有"选项,检查是否所有ID都被选中
|
||||
if (!buildingIds.includes(ALL_BUILDING_ID)) {
|
||||
const validBuildingIds = buildingIds.filter((id) =>
|
||||
allBuildingIds.includes(id)
|
||||
);
|
||||
// 如果选中的数量等于总数,则使用"所有"选项
|
||||
if (
|
||||
validBuildingIds.length === allBuildingIds.length &&
|
||||
allBuildingIds.length > 0
|
||||
) {
|
||||
buildingIds = [ALL_BUILDING_ID];
|
||||
} else if (validBuildingIds.length > 0) {
|
||||
buildingIds = validBuildingIds;
|
||||
} else {
|
||||
buildingIds = [ALL_BUILDING_ID];
|
||||
}
|
||||
}
|
||||
|
||||
if (!floorIds.includes(ALL_FLOOR_ID)) {
|
||||
const validFloorIds = floorIds.filter((id) => allFloorIds.includes(id));
|
||||
// 如果选中的数量等于总数,则使用"所有"选项
|
||||
if (
|
||||
validFloorIds.length === allFloorIds.length &&
|
||||
allFloorIds.length > 0
|
||||
) {
|
||||
floorIds = [ALL_FLOOR_ID];
|
||||
} else if (validFloorIds.length > 0) {
|
||||
floorIds = validFloorIds;
|
||||
} else {
|
||||
floorIds = [ALL_FLOOR_ID];
|
||||
}
|
||||
}
|
||||
|
||||
filterForm.value = {
|
||||
buildingIds,
|
||||
floorIds,
|
||||
statusIds: [...savedFilterState.value.statusIds],
|
||||
rentArea: [...savedFilterState.value.rentArea],
|
||||
tagIds: [...savedFilterState.value.tagIds],
|
||||
};
|
||||
} else {
|
||||
// 否则初始化,楼栋和楼层默认选中"所有"
|
||||
filterForm.value = {
|
||||
buildingIds: [ALL_BUILDING_ID],
|
||||
floorIds: [ALL_FLOOR_ID],
|
||||
statusIds: [],
|
||||
rentArea: [min, max],
|
||||
tagIds: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
// 筛选列表数据
|
||||
const filterRoomList = () => {
|
||||
if (!props.roomList || props.roomList.length === 0) return [];
|
||||
|
||||
let filtered = [...props.roomList];
|
||||
|
||||
// 筛选楼栋(如果选择了"所有楼栋",则不过滤)
|
||||
if (
|
||||
filterForm.value.buildingIds.length > 0 &&
|
||||
!filterForm.value.buildingIds.includes(ALL_BUILDING_ID)
|
||||
) {
|
||||
filtered = filtered.filter((room) =>
|
||||
filterForm.value.buildingIds.includes(room.buildingId)
|
||||
);
|
||||
}
|
||||
|
||||
// 筛选楼层(如果选择了"所有楼层",则不过滤)
|
||||
if (
|
||||
filterForm.value.floorIds.length > 0 &&
|
||||
!filterForm.value.floorIds.includes(ALL_FLOOR_ID)
|
||||
) {
|
||||
filtered = filtered.filter((room) =>
|
||||
filterForm.value.floorIds.includes(room.floorId)
|
||||
);
|
||||
}
|
||||
|
||||
// 筛选状态
|
||||
if (filterForm.value.statusIds.length > 0) {
|
||||
filtered = filtered.filter((room) =>
|
||||
filterForm.value.statusIds.includes(room.status)
|
||||
);
|
||||
}
|
||||
|
||||
// 筛选计租面积
|
||||
const [minArea, maxArea] = filterForm.value.rentArea;
|
||||
filtered = filtered.filter((room) => {
|
||||
const area = Number(room.rentArea);
|
||||
return !isNaN(area) && area >= minArea && area <= maxArea;
|
||||
});
|
||||
|
||||
// 筛选标签
|
||||
if (filterForm.value.tagIds.length > 0) {
|
||||
filtered = filtered.filter((room) => {
|
||||
if (!room.tags || !Array.isArray(room.tags)) return false;
|
||||
const roomTagIds = room.tags.map((tag: any) => tag.id || tag);
|
||||
return filterForm.value.tagIds.some((id) => roomTagIds.includes(id));
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
// 处理"所有"选项:如果同时选择了"所有"和具体选项,只保留"所有"
|
||||
let finalBuildingIds = [...filterForm.value.buildingIds];
|
||||
let finalFloorIds = [...filterForm.value.floorIds];
|
||||
|
||||
if (
|
||||
finalBuildingIds.includes(ALL_BUILDING_ID) &&
|
||||
finalBuildingIds.length > 1
|
||||
) {
|
||||
finalBuildingIds = [ALL_BUILDING_ID];
|
||||
}
|
||||
if (finalFloorIds.includes(ALL_FLOOR_ID) && finalFloorIds.length > 1) {
|
||||
finalFloorIds = [ALL_FLOOR_ID];
|
||||
}
|
||||
|
||||
// 保存当前筛选状态(保存处理后的值)
|
||||
savedFilterState.value = {
|
||||
buildingIds: finalBuildingIds,
|
||||
floorIds: finalFloorIds,
|
||||
statusIds: [...filterForm.value.statusIds],
|
||||
rentArea: [...filterForm.value.rentArea],
|
||||
tagIds: [...filterForm.value.tagIds],
|
||||
};
|
||||
|
||||
// 标准化ID数组:如果选择了"所有"选项,则返回null(表示全部)
|
||||
const normalizeIds = (ids: number[], allOptionId: number) => {
|
||||
if (ids.length === 0) return null;
|
||||
// 如果选择了"所有"选项,则返回null
|
||||
if (ids.includes(allOptionId)) {
|
||||
return null;
|
||||
}
|
||||
return ids;
|
||||
};
|
||||
|
||||
// 使用处理后的值进行筛选
|
||||
const originalBuildingIds = filterForm.value.buildingIds;
|
||||
const originalFloorIds = filterForm.value.floorIds;
|
||||
filterForm.value.buildingIds = finalBuildingIds;
|
||||
filterForm.value.floorIds = finalFloorIds;
|
||||
const filteredList = filterRoomList();
|
||||
// 恢复原始值(用于显示)
|
||||
filterForm.value.buildingIds = originalBuildingIds;
|
||||
filterForm.value.floorIds = originalFloorIds;
|
||||
|
||||
emit('confirm', {
|
||||
buildingIds: normalizeIds(finalBuildingIds, ALL_BUILDING_ID),
|
||||
floorIds: normalizeIds(finalFloorIds, ALL_FLOOR_ID),
|
||||
statusIds:
|
||||
filterForm.value.statusIds.length === 0
|
||||
? null
|
||||
: filterForm.value.statusIds,
|
||||
rentArea: filterForm.value.rentArea,
|
||||
tagIds:
|
||||
filterForm.value.tagIds.length === 0 ? null : filterForm.value.tagIds,
|
||||
filteredList, // 筛选后的列表数据
|
||||
});
|
||||
handleCancel();
|
||||
};
|
||||
|
||||
// 用于防止 watch 循环更新的标志
|
||||
const isUpdatingBuilding = ref(false);
|
||||
const isUpdatingFloor = ref(false);
|
||||
|
||||
// 监听楼栋选择变化
|
||||
watch(
|
||||
() => filterForm.value.buildingIds,
|
||||
(newVal, oldVal) => {
|
||||
if (isUpdatingBuilding.value) return;
|
||||
if (!oldVal || oldVal.length === 0) return; // 初始化时跳过
|
||||
|
||||
const isAllSelected = newVal.includes(ALL_BUILDING_ID);
|
||||
const wasAllOnly = oldVal.length === 1 && oldVal[0] === ALL_BUILDING_ID;
|
||||
const hasSpecificSelected = newVal.some((id) => id !== ALL_BUILDING_ID);
|
||||
const specificIds = newVal.filter((id) => id !== ALL_BUILDING_ID);
|
||||
const allBuildingIds = buildingList.value.map((b) => b.id);
|
||||
|
||||
// 情况1:用户选择了"所有"选项(从非"所有"状态切换到包含"所有")
|
||||
// 判断:旧值不包含"所有"或包含多个值,新值包含"所有"且有其他选项
|
||||
if (isAllSelected && hasSpecificSelected && !wasAllOnly) {
|
||||
isUpdatingBuilding.value = true;
|
||||
filterForm.value.buildingIds = [ALL_BUILDING_ID];
|
||||
nextTick(() => {
|
||||
isUpdatingBuilding.value = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况2:用户从"所有"切换到选择具体选项(新值不包含"所有",但旧值只有"所有")
|
||||
if (!isAllSelected && wasAllOnly && hasSpecificSelected) {
|
||||
// v-model已经处理了,不需要额外操作,允许用户选择
|
||||
// 检查是否选中的数量等于总数
|
||||
if (
|
||||
specificIds.length === allBuildingIds.length &&
|
||||
allBuildingIds.every((id) => specificIds.includes(id))
|
||||
) {
|
||||
// 选中的数量等于总数,自动切换为"所有"
|
||||
isUpdatingBuilding.value = true;
|
||||
filterForm.value.buildingIds = [ALL_BUILDING_ID];
|
||||
nextTick(() => {
|
||||
isUpdatingBuilding.value = false;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况3:用户选择了具体选项,但当前值还包含"所有"(可能是从"所有"状态添加了具体选项)
|
||||
if (hasSpecificSelected && isAllSelected && wasAllOnly) {
|
||||
// 移除"所有"选项,只保留具体选项
|
||||
isUpdatingBuilding.value = true;
|
||||
filterForm.value.buildingIds = specificIds;
|
||||
nextTick(() => {
|
||||
isUpdatingBuilding.value = false;
|
||||
// 检查是否选中的数量等于总数
|
||||
if (
|
||||
specificIds.length === allBuildingIds.length &&
|
||||
allBuildingIds.every((id) => specificIds.includes(id))
|
||||
) {
|
||||
isUpdatingBuilding.value = true;
|
||||
filterForm.value.buildingIds = [ALL_BUILDING_ID];
|
||||
nextTick(() => {
|
||||
isUpdatingBuilding.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况4:用户一个个勾选具体选项,当选中数量等于总数时,自动切换为"所有"
|
||||
if (
|
||||
!isAllSelected &&
|
||||
hasSpecificSelected &&
|
||||
specificIds.length === allBuildingIds.length
|
||||
) {
|
||||
// 检查是否所有选项都被选中
|
||||
if (allBuildingIds.every((id) => specificIds.includes(id))) {
|
||||
isUpdatingBuilding.value = true;
|
||||
filterForm.value.buildingIds = [ALL_BUILDING_ID];
|
||||
nextTick(() => {
|
||||
isUpdatingBuilding.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听楼层选择变化
|
||||
watch(
|
||||
() => filterForm.value.floorIds,
|
||||
(newVal, oldVal) => {
|
||||
if (isUpdatingFloor.value) return;
|
||||
if (!oldVal || oldVal.length === 0) return; // 初始化时跳过
|
||||
|
||||
const isAllSelected = newVal.includes(ALL_FLOOR_ID);
|
||||
const wasAllOnly = oldVal.length === 1 && oldVal[0] === ALL_FLOOR_ID;
|
||||
const hasSpecificSelected = newVal.some((id) => id !== ALL_FLOOR_ID);
|
||||
const specificIds = newVal.filter((id) => id !== ALL_FLOOR_ID);
|
||||
const allFloorIds = floorList.value.map((f) => f.id);
|
||||
|
||||
// 情况1:用户选择了"所有"选项(从非"所有"状态切换到包含"所有")
|
||||
// 判断:旧值不包含"所有"或包含多个值,新值包含"所有"且有其他选项
|
||||
if (isAllSelected && hasSpecificSelected && !wasAllOnly) {
|
||||
isUpdatingFloor.value = true;
|
||||
filterForm.value.floorIds = [ALL_FLOOR_ID];
|
||||
nextTick(() => {
|
||||
isUpdatingFloor.value = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况2:用户从"所有"切换到选择具体选项(新值不包含"所有",但旧值只有"所有")
|
||||
if (!isAllSelected && wasAllOnly && hasSpecificSelected) {
|
||||
// v-model已经处理了,不需要额外操作,允许用户选择
|
||||
// 检查是否选中的数量等于总数
|
||||
if (
|
||||
specificIds.length === allFloorIds.length &&
|
||||
allFloorIds.every((id) => specificIds.includes(id))
|
||||
) {
|
||||
// 选中的数量等于总数,自动切换为"所有"
|
||||
isUpdatingFloor.value = true;
|
||||
filterForm.value.floorIds = [ALL_FLOOR_ID];
|
||||
nextTick(() => {
|
||||
isUpdatingFloor.value = false;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况3:用户选择了具体选项,但当前值还包含"所有"(可能是从"所有"状态添加了具体选项)
|
||||
if (hasSpecificSelected && isAllSelected && wasAllOnly) {
|
||||
// 移除"所有"选项,只保留具体选项
|
||||
isUpdatingFloor.value = true;
|
||||
filterForm.value.floorIds = specificIds;
|
||||
nextTick(() => {
|
||||
isUpdatingFloor.value = false;
|
||||
// 检查是否选中的数量等于总数
|
||||
if (
|
||||
specificIds.length === allFloorIds.length &&
|
||||
allFloorIds.every((id) => specificIds.includes(id))
|
||||
) {
|
||||
isUpdatingFloor.value = true;
|
||||
filterForm.value.floorIds = [ALL_FLOOR_ID];
|
||||
nextTick(() => {
|
||||
isUpdatingFloor.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况4:用户一个个勾选具体选项,当选中数量等于总数时,自动切换为"所有"
|
||||
if (
|
||||
!isAllSelected &&
|
||||
hasSpecificSelected &&
|
||||
specificIds.length === allFloorIds.length
|
||||
) {
|
||||
// 检查是否所有选项都被选中
|
||||
if (allFloorIds.every((id) => specificIds.includes(id))) {
|
||||
isUpdatingFloor.value = true;
|
||||
filterForm.value.floorIds = [ALL_FLOOR_ID];
|
||||
nextTick(() => {
|
||||
isUpdatingFloor.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听抽屉打开,初始化数据
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
// 延迟初始化,确保 buildingList 和 floorList 已计算
|
||||
setTimeout(() => {
|
||||
initFilterForm();
|
||||
}, 0);
|
||||
if (tagList.value.length === 0) {
|
||||
getTagListData();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听楼栋和楼层列表变化,如果当前没有保存状态且列表为空,则默认选中"所有"
|
||||
watch(
|
||||
[buildingList, floorList],
|
||||
() => {
|
||||
if (props.visible && !savedFilterState.value) {
|
||||
// 如果楼栋列表为空,默认选中"所有楼栋"
|
||||
if (filterForm.value.buildingIds.length === 0) {
|
||||
filterForm.value.buildingIds = [ALL_BUILDING_ID];
|
||||
}
|
||||
// 如果楼层列表为空,默认选中"所有楼层"
|
||||
if (filterForm.value.floorIds.length === 0) {
|
||||
filterForm.value.floorIds = [ALL_FLOOR_ID];
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
getTagListData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(label.arco-form-item-label) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
color: var(--color-text-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.arco-slider-mark) {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<template>
|
||||
<div>house-list</div>
|
||||
</template>
|
||||
338
src/views/business/space/house-list/index.vue
Normal file
338
src/views/business/space/house-list/index.vue
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
<template>
|
||||
<div class="business_project_wrap">
|
||||
<Breadcrumb :items="['menu.business', 'menu.business.house-list']" />
|
||||
<a-card class="general-card full_height">
|
||||
<template #title>
|
||||
<div class="card-title_between">
|
||||
<p class="title">{{ t('menu.business.house-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"
|
||||
/>
|
||||
<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="handleAddHouse">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
{{ t('add-house') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 使用新的 DataTable 组件 -->
|
||||
<DataTable
|
||||
:data="renderData"
|
||||
:columns="projectColumn"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:current="current"
|
||||
:page-size="pageSize"
|
||||
@page-change="onPageChange"
|
||||
@page-size-change="onPageSizeChange"
|
||||
>
|
||||
<template #tags="{ record }">
|
||||
<template v-if="record.tags?.length">
|
||||
<a-tag
|
||||
v-for="item in record.tags"
|
||||
:key="item.id"
|
||||
class="mr-[5px] first:mr-0"
|
||||
>
|
||||
{{ item.name }}</a-tag
|
||||
>
|
||||
</template>
|
||||
<span v-else> - </span>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getTagsColor(record?.status)">{{
|
||||
record?.statusStr
|
||||
}}</a-tag>
|
||||
</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="handleEditProject(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-space>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
</template>
|
||||
</DataTable>
|
||||
</a-card>
|
||||
<houseFormDrawer
|
||||
v-model:visible="showDrawer"
|
||||
:is-edit="isEdit"
|
||||
:record="currentRecordInfo"
|
||||
@submitted="getHouseListData"
|
||||
/>
|
||||
<houseInfoDrawer
|
||||
v-model:visible="showProjectInfoDrawer"
|
||||
:project-id="currentProjectId"
|
||||
:detail-info="currentRecordInfo"
|
||||
/>
|
||||
<houseListFilterDrawer
|
||||
v-model:visible="showListFilterDrawer"
|
||||
:room-list="projectList"
|
||||
:project-name="currentProjectName"
|
||||
@confirm="handleFilterConfirm"
|
||||
/>
|
||||
</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 { getRoomList, getRoom, delRoom } from '@/api/business';
|
||||
import { ProjectPaginationParams } from '@/types/comm';
|
||||
import houseFormDrawer from '../drawer/house-form-drawer.vue';
|
||||
import houseInfoDrawer from '../drawer/house-info-drawer.vue';
|
||||
import houseListFilterDrawer from '../drawer/house-list-filter-drawer.vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
const { t } = useI18n();
|
||||
const { loading } = useLoading(false);
|
||||
const showDrawer = ref<boolean>(false);
|
||||
const showProjectInfoDrawer = ref<boolean>(false);
|
||||
const showListFilterDrawer = ref<boolean>(false);
|
||||
const currentProjectId = ref<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 projectList = ref<any[]>([]);
|
||||
const currentProjectName = ref<string>(''); // 当前项目名称
|
||||
const {
|
||||
data: renderData,
|
||||
columns: projectColumn,
|
||||
total,
|
||||
current,
|
||||
pageSize,
|
||||
setData,
|
||||
setColumns,
|
||||
setTotal,
|
||||
setCurrent,
|
||||
setPageSize,
|
||||
setLoading,
|
||||
} = useDataTable({
|
||||
initialPage: 1,
|
||||
initialPageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
});
|
||||
|
||||
// 通用空值处理函数
|
||||
const renderEmpty = (value: any) => value ?? '-';
|
||||
|
||||
// 列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '房源名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: ({ record }) => renderEmpty(record.name),
|
||||
},
|
||||
{
|
||||
title: '所属项目',
|
||||
dataIndex: 'projectName',
|
||||
key: 'projectName',
|
||||
render: ({ record }) => renderEmpty(record.projectName),
|
||||
},
|
||||
{
|
||||
title: '所属楼栋',
|
||||
dataIndex: 'buildingName',
|
||||
key: 'buildingName',
|
||||
render: ({ record }) => renderEmpty(record.buildingName),
|
||||
},
|
||||
{
|
||||
title: '楼层',
|
||||
dataIndex: 'floorName',
|
||||
key: 'floorName',
|
||||
render: ({ record }) => renderEmpty(record.floorName),
|
||||
},
|
||||
{
|
||||
title: '计租面积(㎡)',
|
||||
dataIndex: 'rentArea',
|
||||
key: 'rentArea',
|
||||
render: ({ record }) => renderEmpty(record.rentArea),
|
||||
},
|
||||
{
|
||||
title: '租金(实时)',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
render: ({ record }) => (Number(record.price) <= 0 ? '-' : record?.price),
|
||||
},
|
||||
{ title: '标签', dataIndex: 'tags', key: 'tags', slotName: 'tags' },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'statusStr',
|
||||
key: 'statusStr',
|
||||
slotName: 'status',
|
||||
},
|
||||
{
|
||||
title: '合同',
|
||||
dataIndex: 'contractStatusStr',
|
||||
key: 'contractStatusStr',
|
||||
render: ({ record }) => renderEmpty(record.contractStatusStr),
|
||||
},
|
||||
{ title: '操作', key: 'operate', slotName: 'operate', width: 200 },
|
||||
];
|
||||
const queryParams = ref<ProjectPaginationParams>({
|
||||
page: current.value,
|
||||
page_size: pageSize.value,
|
||||
keyword: '',
|
||||
});
|
||||
const getHouseListData = async () => {
|
||||
setLoading(true);
|
||||
const { code, data } = await getRoomList(queryParams.value);
|
||||
if (code === 200 && data) {
|
||||
const { list, page } = data as {
|
||||
list?: any[];
|
||||
page?: { total?: number };
|
||||
};
|
||||
projectList.value = Array.isArray(list) ? list : [];
|
||||
// 提取项目名称(从第一条数据中获取,假设所有房源属于同一项目)
|
||||
if (projectList.value.length > 0 && projectList.value[0].projectName) {
|
||||
currentProjectName.value = projectList.value[0].projectName;
|
||||
}
|
||||
setData(projectList.value);
|
||||
setTotal(page?.total ? page.total : 0);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const getRoomData = async (id: number, edit: boolean) => {
|
||||
if (currentRecordInfo.value && currentRecordInfo.value?.id === id && !edit)
|
||||
return;
|
||||
if (edit) {
|
||||
isLoadingEdit.value = true;
|
||||
} else {
|
||||
isLoadingDetail.value = true;
|
||||
}
|
||||
const { code, data } = await getRoom(id);
|
||||
if (code === 200 && data) {
|
||||
currentRecordInfo.value = (data as { room?: any }).room || null;
|
||||
}
|
||||
isLoadingEdit.value = false;
|
||||
isLoadingDetail.value = false;
|
||||
};
|
||||
const handleFilterConfirm = (payload: {
|
||||
buildingIds: number[] | null;
|
||||
floorIds: number[] | null;
|
||||
statusIds: number[] | null;
|
||||
rentArea: number[];
|
||||
tagIds: number[] | null;
|
||||
filteredList: any[]; // 筛选后的列表数据
|
||||
}) => {
|
||||
// 更新查询参数(用于后端筛选)
|
||||
(queryParams.value as any).building_ids = payload.buildingIds;
|
||||
(queryParams.value as any).floor_ids = payload.floorIds;
|
||||
(queryParams.value as any).status_ids = payload.statusIds;
|
||||
(queryParams.value as any).rent_area_min = payload.rentArea?.[0];
|
||||
(queryParams.value as any).rent_area_max = payload.rentArea?.[1];
|
||||
(queryParams.value as any).tag_ids = payload.tagIds;
|
||||
|
||||
// 使用筛选后的列表数据更新表格
|
||||
if (payload.filteredList) {
|
||||
setData(payload.filteredList);
|
||||
setTotal(payload.filteredList.length);
|
||||
} else {
|
||||
// 如果没有筛选数据,重新加载列表数据
|
||||
getHouseListData();
|
||||
}
|
||||
};
|
||||
const handleDel = async (id: number) => {
|
||||
const { code, message } = await delRoom(id);
|
||||
if (code === 200) {
|
||||
Message.success(message || '删除成功');
|
||||
// 重新加载列表数据
|
||||
await getHouseListData();
|
||||
} else {
|
||||
Message.error(message || '删除失败');
|
||||
}
|
||||
};
|
||||
// 分页事件处理
|
||||
const onPageChange = (page: number) => {
|
||||
setCurrent(page);
|
||||
current.value = page;
|
||||
};
|
||||
const onPageSizeChange = (size: number) => {
|
||||
setPageSize(size);
|
||||
pageSize.value = size;
|
||||
};
|
||||
const handleAddHouse = () => {
|
||||
isEdit.value = false;
|
||||
currentRecordInfo.value = null;
|
||||
showDrawer.value = true;
|
||||
};
|
||||
const handleEditProject = async (id: number) => {
|
||||
if (isLoadingEdit.value) return;
|
||||
isEdit.value = true;
|
||||
await getRoomData(id, true);
|
||||
showDrawer.value = true;
|
||||
};
|
||||
const handleDetail = async (id: number) => {
|
||||
if (isLoadingDetail.value) return;
|
||||
await getRoomData(id, false);
|
||||
showProjectInfoDrawer.value = true;
|
||||
};
|
||||
const handleSearch = () => {
|
||||
getHouseListData();
|
||||
};
|
||||
const handleFilter = () => {
|
||||
showListFilterDrawer.value = true;
|
||||
};
|
||||
const getTagsColor = (status) => {
|
||||
const colorMap = {
|
||||
1: 'orangered',
|
||||
2: 'orange',
|
||||
3: 'red',
|
||||
4: 'green',
|
||||
};
|
||||
return colorMap[status];
|
||||
};
|
||||
onMounted(() => {
|
||||
setColumns(columns);
|
||||
getHouseListData();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -199,20 +199,10 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
const priceUnits = [
|
||||
{ value: 1, text: '元/㎡*月' },
|
||||
{ value: 2, text: '元/㎡*天' },
|
||||
{ value: 3, text: '元/天' },
|
||||
{ value: 4, text: '元/月' },
|
||||
{ value: 5, text: '元/年' },
|
||||
];
|
||||
const priceUnitMap = new Map([
|
||||
[1, '元/㎡*月'],
|
||||
[2, '元/㎡*天'],
|
||||
[3, '元/天'],
|
||||
[4, '元/月'],
|
||||
[5, '元/年'],
|
||||
]);
|
||||
import { PRICE_UNITS, PRICE_UNIT_MAP } from '@/constants';
|
||||
|
||||
const priceUnits = PRICE_UNITS;
|
||||
const priceUnitMap = PRICE_UNIT_MAP;
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
record?: any | null;
|
||||
|
|
@ -259,7 +249,7 @@
|
|||
emit('update:visible', false);
|
||||
emit('submitted', form);
|
||||
} catch {
|
||||
console.warn('❌ 校验未通过');
|
||||
// 校验未通过
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@
|
|||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
// 部门列表
|
||||
|
||||
async function getAllProjectListData() {
|
||||
try {
|
||||
const { code, data } = await getAllProjectIdNameList();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue