Compare commits

..
4 Commits
Author SHA1 Message Date
Zzz1zz adf2a2fa36 feat: api请求按路由分割 2026-02-01 12:04:08 +08:00
Zzz1zz f7a710d6d2 feat: 租客管理功能开发 2026-01-23 17:35:56 +08:00
Zzz1zz f582098dc6 feat: 完成空间管理模块 2026-01-23 01:14:47 +08:00
Zzz1zz a53cd0e8b8 feat: 空间管理 -> 房源列表开发 2026-01-12 00:19:24 +08:00
103 changed files with 8414 additions and 3040 deletions
+14
View File
@@ -0,0 +1,14 @@
{
"tailwindCSS.experimental.configFile": "src/assets/style/index.css",
"tailwindCSS.includeLanguages": {
"vue": "html",
"vue-html": "html"
},
"editor.quickSuggestions": {
"other": true,
"comments": false,
"strings": true
},
"css.validate": true,
"tailwindCSS.emmetCompletions": true
}
BIN
View File
Binary file not shown.
+279
View File
@@ -0,0 +1,279 @@
import axios from 'axios';
import {
SettingContractNumbeRes,
CreateCodeSettingParam,
DisableUserParam,
CreateTermSettingParam,
SettingContractTermListRes,
SettingContractTermInfo,
SettingContractTemplateListRes,
CreateTemplateSettingParam,
ContractWordFormParam,
ContractWordSettingListRes,
SettingContractFileInfoRes,
} from '@/types/setting';
import { PaginationParams } from '@/types/comm';
import { HttpResponse } from './interceptor';
// ==================== 合同设置 API (contractSetting 路由) ====================
// 合同设置-合同编号设置-全部启用列表
export function getAllCodeSettingList(): Promise<SettingContractNumbeRes> {
return axios.get(`contractSetting/getAllCodeSettingList`);
}
// 合同设置-合同编号设置-新增
export function createCodeSetting(
params: CreateCodeSettingParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/createCodeSetting`, params);
}
// 合同设置-合同编号设置-编辑
export function updateCodeSetting(
params: CreateCodeSettingParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/updateCodeSetting`, params);
}
// 合同设置-合同编号设置-删除
export function delCodeSetting(id: number): Promise<HttpResponse> {
return axios.get(`contractSetting/delCodeSetting?id=${id}`);
}
// 合同设置-合同编号设置-单个详情
export function getCodeSettingFromId(
id: number
): Promise<SettingContractNumbeRes> {
return axios.get(`contractSetting/getCodeSetting?id=${id}`);
}
// 合同设置-合同编号设置-启用/禁用
export function disableCodeSetting(
params: DisableUserParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/disableCodeSetting`, params);
}
// 合同设置-合同编号设置-列表(带分页)
export function getCodeSettingList(
params: PaginationParams
): Promise<SettingContractNumbeRes> {
return axios.post(`contractSetting/getCodeSettingList`, params);
}
// 合同设置-合同条款设置-全部启用列表
export function getAllTermsSettingList(): Promise<any> {
return axios.get(`contractSetting/getAllTermsSettingList`);
}
// 合同设置-合同条款设置-新增
export function createTermsSetting(
params: CreateTermSettingParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/createTermsSetting`, params);
}
// 合同设置-合同条款设置-编辑
export function updateTermsSetting(
params: CreateTermSettingParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/updateTermsSetting`, params);
}
// 合同设置-合同条款设置-删除
export function delTermsSetting(id: number): Promise<HttpResponse> {
return axios.get(`contractSetting/delTermsSetting?id=${id}`);
}
// 合同设置-合同条款设置-单个条款配置详情
export function getTermsSettingFromId(
id: number
): Promise<SettingContractTermInfo> {
return axios.get(`contractSetting/getTermsSetting?id=${id}`);
}
// 合同设置-合同条款设置-单个条款数据详情
export function getTermsDataFromId(params: any): Promise<any> {
return axios.post(`contractSetting/getTerm`, params);
}
// 合同设置-合同条款设置-启用/禁用
export function disableTermsSetting(
params: DisableUserParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/disableTermsSetting`, params);
}
// 合同设置-合同条款设置-列表(带分页)
export function getTermsSettingList(
params: PaginationParams
): Promise<SettingContractTermListRes> {
return axios.post(`contractSetting/getTermsSettingList`, params);
}
// 合同设置-合同模板管理-全部启用列表
export function getAllTemplateSettingList(): Promise<SettingContractNumbeRes> {
return axios.get(`contractSetting/getAllTemplateSettingList`);
}
// 合同设置-合同模板管理-新增
export function createTemplateSetting(
params: CreateTemplateSettingParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/createTemplateSetting`, params);
}
// 合同设置-合同模板管理-编辑
export function updateTemplateSetting(params: any): Promise<HttpResponse> {
return axios.post(`contractSetting/saveTemplateSetting`, params);
}
// 合同设置-合同模板管理-删除
export function delTemplateSetting(id: number): Promise<HttpResponse> {
return axios.get(`contractSetting/delTemplateSetting?id=${id}`);
}
// 合同设置-合同模板管理-单个详情
export function getTemplateSettingFromId(id: number): Promise<any> {
return axios.get(`contractSetting/getTemplateSetting?id=${id}`);
}
// 合同设置-合同模板管理-启用/禁用
export function disableTemplateSetting(
params: DisableUserParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/disableTemplateSetting`, params);
}
// 合同设置-合同模板管理-列表(带分页)
export function getTemplateSettingList(
params: PaginationParams
): Promise<SettingContractTemplateListRes> {
return axios.post(`contractSetting/getTemplateSettingList`, params);
}
// 合同设置-合同文档模板管理-全部启用列表
export function getAllWordSettingList(): Promise<ContractWordSettingListRes> {
return axios.get(`contractSetting/getAllWordSettingList`);
}
// 合同设置-合同文档模板管理-新增
export function createWordSetting(
params: ContractWordFormParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/createWordSetting`, params);
}
// 合同设置-合同文档模板管理-编辑(更新)
export function updateWordSetting(
params: ContractWordFormParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/updateWordSetting`, params);
}
// 合同设置-合同文档模板管理-删除
export function delWordSetting(id: number): Promise<HttpResponse> {
return axios.get(`contractSetting/delWordSetting?id=${id}`);
}
// 合同设置-合同文档模板管理-单个详情
export function getWordSettingFromId(
id: number
): Promise<SettingContractFileInfoRes> {
return axios.get(`contractSetting/getWordSetting?id=${id}`);
}
// 合同设置-合同文档模板管理-启用/禁用
export function disableWordSetting(
params: DisableUserParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/disableWordSetting`, params);
}
// 合同设置-合同文档模板管理-列表(带分页)
export function getWordSettingList(
params: PaginationParams
): Promise<ContractWordSettingListRes> {
return axios.post(`contractSetting/getWordSettingList`, params);
}
// 获取合同占位符列表
export function getContractPlaceholderList(params: any): Promise<any> {
// type 类型:1租赁,2物业
return axios.post(`contractSetting/getPlaceholderList`, params);
}
// ==================== 合同管理 API (contracts 路由) ====================
// 合同管理-头部数据列表
export function getContractsListTopData(type: number): Promise<any> {
return axios.post(`contracts/getListTopData`, type);
}
// 合同管理-列表
export interface ContractPaginationParams extends PaginationParams {
type?: number;
}
export function getContractsList(
params: ContractPaginationParams
): Promise<any> {
return axios.post(`contracts/getList`, params);
}
// 合同管理-草稿合同列表
export function getDraftList(type: number): Promise<any> {
return axios.get(`contracts/getDraftList?type=${type}`);
}
// 合同管理-编辑合同详情
export function getEditDetail(id: number): Promise<any> {
return axios.get(`contracts/getEditDetail?id=${id}`);
}
// 合同管理-编辑合同标签详情
export function getEditTagDetail(params: any): Promise<any> {
return axios.post(`contracts/getEditTagDetail`, params);
}
// 合同管理-合同详情
export function getContractDetail(id: number): Promise<any> {
return axios.get(`contracts/getDetail?id=${id}`);
}
// 合同管理-标签详情
export function getTagDetail(params: any): Promise<any> {
return axios.post(`contracts/getTagDetail`, params);
}
// 合同管理-保存合同
export function saveContracts(params: any): Promise<any> {
return axios.post(`contracts/save`, params);
}
// 合同管理-禁用/启用合同
export function disableContracts(params: any): Promise<any> {
return axios.post(`contracts/disable`, params);
}
// 合同管理-删除合同
export function delContract(id: number): Promise<any> {
return axios.get(`contracts/del?id=${id}`);
}
// 合同管理-提交审批
export function approvalContract(id: number): Promise<any> {
return axios.get(`contracts/approval?id=${id}`);
}
// 合同管理-归档
export function filingContract(id: number): Promise<any> {
return axios.get(`contracts/filing?id=${id}`);
}
// 合同管理-生成合同
export function createContracts(params: any): Promise<any> {
return axios.post(`contracts/createWord`, params);
}
// 合同管理-合同文件上传
export function uploadFileContracts(params: any): Promise<any> {
return axios.post(`contracts/uploadFile`, params);
}
// 合同管理-合同文件更新
export function updateFileContracts(params: any): Promise<any> {
return axios.post(`contracts/updateFile`, params);
}
// 合同管理-取消审批
export function cancelApprovalContract(id: number): Promise<any> {
return axios.get(`contracts/cancelApproval?id=${id}`);
}
// 合同归档-列表
export function getFilingContractList(params: any): Promise<any> {
return axios.post(`contracts/getFilingList`, params);
}
-22
View File
@@ -1,22 +0,0 @@
import axios from 'axios';
import type { TableData } from '@arco-design/web-vue/es/table/interface';
export interface ContentDataRecord {
x: string;
y: number;
}
export function queryContentData() {
return axios.get<ContentDataRecord[]>('/api/content-data');
}
export interface PopularRecord {
key: number;
clickNumber: string;
title: string;
increases: number;
}
export function queryPopularList(params: { type: string }) {
return axios.get<TableData[]>('/api/popular/list', { params });
}
+51
View File
@@ -0,0 +1,51 @@
import axios from 'axios';
import {
DepartmentFormParams,
DepartmentFormRes,
DepartmentListRes,
DepartmentDetailRes,
} from '@/types/setting';
import { HttpResponse } from './interceptor';
// ==================== 部门相关 API ====================
export function createDepartment(
params: DepartmentFormParams
): Promise<DepartmentFormRes> {
return axios.post('/department/createDepartment', params);
}
export function updateDepartment(
params: DepartmentFormParams
): Promise<DepartmentFormRes> {
return axios.post('/department/updateDepartment', params);
}
export function getDepartmentList(): Promise<DepartmentListRes> {
return axios.get('/department/getDepartmentList');
}
export function getDepartmentDetail(id: number): Promise<DepartmentDetailRes> {
return axios.get(`/department/getDepartment?id=${id}`);
}
export function departmentSetUp(id: number): Promise<HttpResponse> {
return axios.get(`/department/setUp?id=${id}`);
}
export function departmentSetDown(id: number): Promise<HttpResponse> {
return axios.get(`/department/setDown?id=${id}`);
}
export function delDepartment(id: number): Promise<HttpResponse> {
return axios.get(`/department/delDepartment?id=${id}`);
}
// 保存部门项目数据
export function saveDataPermission(params: any): Promise<HttpResponse> {
return axios.post('/department/saveDataPermission', params);
}
// 获取部门项目数据
export function getDataPermission(id: number): Promise<HttpResponse> {
return axios.get(`/department/getDataPermission?id=${id}`);
}
+221
View File
@@ -0,0 +1,221 @@
import axios from 'axios';
import {
SettingProjectTypeListRes,
SettingProjectTypeRes,
SettingProjectProvinceRes,
DictionaryForm,
CheckProjectParam,
SettingRoomTagsRes,
SettingRoomTagRes,
SettingTenantDictionaryRes,
} from '@/types/setting';
import { TenantDictionaryType } from '@/types/comm';
import { HttpResponse } from './interceptor';
// ==================== 数据字典 API (dictionary 路由) ====================
// 数据字典-项目管理-全部项目类型列表
export function getAllProjectTypeList(): Promise<SettingProjectTypeListRes> {
return axios.get('dictionary/getAllProjectTypeList');
}
// 数据字典-项目管理-单个项目类型信息
export function getProjectTypeById(id: number): Promise<SettingProjectTypeRes> {
return axios.get(`dictionary/getProjectType?id=${id}`);
}
// 数据字典-项目管理-项目地址列表
export function getProvinceList(): Promise<SettingProjectProvinceRes> {
return axios.get(`dictionary/getProvinceList`);
}
// 数据字典-项目管理-创建项目类型
export function createProjectType(
params: DictionaryForm
): Promise<HttpResponse> {
return axios.post(`dictionary/createProjectType`, params);
}
// 数据字典-项目管理-更新项目类型信息
export function updateProjectType(
params: DictionaryForm
): Promise<HttpResponse> {
return axios.post(`dictionary/updateProjectType`, params);
}
// 数据字典-项目管理-删除项目类型
export function delProjectType(id: number): Promise<SettingProjectProvinceRes> {
return axios.get(`dictionary/delProjectType?id=${id}`);
}
// 数据字典-项目管理-启用/禁用项目类型(单个)
export function checkProjectType(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkProjectType`, params);
}
// 数据字典-项目管理-启用/禁用项目类型(全选)
export function checkAllProjectType(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkAllProjectType`, params);
}
// 数据字典-项目管理-启用/禁用省份(单个)
export function checkProvince(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkProvince`, params);
}
// 数据字典-项目管理-启用/禁用省份(全选)
export function checkAllProvince(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkAllProvince`, params);
}
// 数据字典-空间管理-全部房源标签列表
export function getRoomTagList(): Promise<SettingRoomTagsRes> {
return axios.get(`dictionary/getRoomTagList`);
}
// 数据字典-项目管理-单个房源标签信息
export function getRoomTagById(id: number): Promise<SettingRoomTagRes> {
return axios.get(`dictionary/getRoomTag?id=${id}`);
}
// 数据字典-空间管理-创建房源标签
export function createRoomTag(params: DictionaryForm): Promise<HttpResponse> {
return axios.post(`dictionary/createRoomTag`, params);
}
// 数据字典-空间管理-更新房源标签
export function updateRoomTag(params: DictionaryForm): Promise<HttpResponse> {
return axios.post(`dictionary/updateRoomTag`, params);
}
// 数据字典-空间管理-删除房源标签
export function delRoomTag(id: number): Promise<SettingProjectProvinceRes> {
return axios.get(`dictionary/delRoomTag?id=${id}`);
}
// 数据字典-空间管理-禁用/启用 房源标签(单个)
export function checkRoomTag(params: CheckProjectParam): Promise<HttpResponse> {
return axios.post(`dictionary/checkRoomTag`, params);
}
// 数据字典-空间管理-禁用/启用 房源标签(全选)
export function checkAllRoomTag(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkAllRoomTag`, params);
}
// 数据字典-租客管理-全部租客标签列表(包含所有类型标签)
export function getAllTenantDictionaryList(): Promise<SettingTenantDictionaryRes> {
return axios.get(`dictionary/getAllTenantDictionaryList`);
}
// 数据字典-租客管理-单个租客标签信息
export function getTenantDictionary(
id: number,
type: TenantDictionaryType
): Promise<SettingRoomTagsRes> {
return axios.get(`dictionary/getTenantDictionary?id=${id}&type=${type}`);
}
// 数据字典-租客管理-创建租客标签
export function createTenantDictionary(
params: DictionaryForm
): Promise<HttpResponse> {
return axios.post(`dictionary/createTenantDictionary`, params);
}
// 数据字典-租客管理-更新租客标签
export function updateTenantDictionary(
params: DictionaryForm
): Promise<HttpResponse> {
return axios.post(`dictionary/updateTenantDictionary`, params);
}
// 数据字典-租客管理-删除租客标签
export function delTenantDictionary(
id: number,
type: TenantDictionaryType
): Promise<SettingRoomTagsRes> {
return axios.get(`dictionary/delTenantDictionary?id=${id}&type=${type}`);
}
// 数据字典-租客管理-禁用/启用 租客标签(单个)
export function checkTenantDictionary(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkTenantDictionary`, params);
}
// 数据字典-租客管理-禁用/启用 租客标签(全选)
export function checkAllTenantDictionary(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkAllTenantDictionary`, params);
}
// 数据字典-租客管理--已启用列表(type:1租客类型,2租客来源,3租客行业分类,4租客企业规模)
export function getTenantDictionaryListByType(
type: TenantDictionaryType
): Promise<SettingRoomTagsRes> {
return axios.get(`dictionary/getTenantDictionaryList?type=${type}`);
}
// 合同标签列表(已勾选)
export function getContractTagsList(): Promise<any> {
return axios.get(`dictionary/getContractTagsList`);
}
// 数据字典-合同管理-合同标签列表(全部)
export function getAllContractTagsList(): Promise<any> {
return axios.get(`dictionary/getAllContractTagsList`);
}
// 数据字典-合同管理-创建合同标签
export function createContractTags(
params: DictionaryForm
): Promise<HttpResponse> {
return axios.post(`dictionary/createContractTags`, params);
}
// 数据字典-合同管理-更新合同标签
export function updateContractTags(
params: DictionaryForm
): Promise<HttpResponse> {
return axios.post(`dictionary/updateContractTags`, params);
}
// 数据字典-合同管理-删除合同标签
export function delContractTags(
id: number
): Promise<SettingProjectProvinceRes> {
return axios.get(`dictionary/delContractTags?id=${id}`);
}
// 数据字典-合同管理-禁用/启用 合同标签(单个)
export function checkContractTags(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkContractTags`, params);
}
// 数据字典-合同管理-禁用/启用 合同标签(全选)
export function checkAllContractTags(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkAllContractTags`, params);
}
// 数据字典-合同管理-单个合同标签信息
export function getContractTags(
id: number
): Promise<SettingProjectProvinceRes> {
return axios.get(`dictionary/getContractTags?id=${id}`);
}
+55
View File
@@ -0,0 +1,55 @@
import axios from 'axios';
import {
FeeManagementListRes,
CreateFeeParams,
FeeManagementDetailRes,
FeeSelectListParams,
DisableUserParam,
} from '@/types/setting';
import { PaginationParams } from '@/types/comm';
import { HttpResponse } from './interceptor';
// ==================== 财务设置 API (financeSetting 路由) ====================
// 合同设置-财务设置-全部列表(带分页)
export function getFeeList(
params: PaginationParams
): Promise<FeeManagementListRes> {
return axios.post(`financeSetting/getFeeList`, params);
}
// 合同设置-财务设置-新增
export function createFee(params: CreateFeeParams): Promise<HttpResponse> {
return axios.post(`financeSetting/createFee`, params);
}
// 合同设置-财务设置-编辑
export function updateFee(params: CreateFeeParams): Promise<HttpResponse> {
return axios.post(`financeSetting/updateFee`, params);
}
// 合同设置-财务设置-删除
export function delFee(id: number): Promise<HttpResponse> {
return axios.get(`financeSetting/delFee?id=${id}`);
}
// 合同设置-财务设置-单个详情
export function getFeeFromId(id: number): Promise<FeeManagementDetailRes> {
return axios.get(`financeSetting/getFee?id=${id}`);
}
// 合同设置-财务设置-启用/禁用
export function disableFee(params: DisableUserParam): Promise<HttpResponse> {
return axios.post(`financeSetting/disableFee`, params);
}
// 合同设置-财务设置-获取下拉列表
export function getUseFeeList(
params: FeeSelectListParams
): Promise<FeeManagementListRes> {
return axios.post(`financeSetting/getUseFeeList`, params);
}
// 合同设置-流水账户管理--获取下拉列表
export function getUseTurnoverAccountList(params: any): Promise<any> {
return axios.get(`financeSetting/getUseTurnoverAccountList`, params);
}
-21
View File
@@ -1,21 +0,0 @@
import axios from 'axios';
export interface BaseInfoModel {
activityName: string;
channelType: string;
promotionTime: string[];
promoteLink: string;
}
export interface ChannelInfoModel {
advertisingSource: string;
advertisingMedia: string;
keyword: string[];
pushNotify: boolean;
advertisingContent: string;
}
export type UnitChannelModel = BaseInfoModel & ChannelInfoModel;
export function submitChannelForm(data: UnitChannelModel) {
return axios.post('/api/channel-form/submit', { data });
}
-56
View File
@@ -1,56 +0,0 @@
import axios from 'axios';
import qs from 'query-string';
import type { DescData } from '@arco-design/web-vue/es/descriptions/interface';
export interface PolicyRecord {
id: string;
number: number;
name: string;
contentType: 'img' | 'horizontalVideo' | 'verticalVideo';
filterType: 'artificial' | 'rules';
count: number;
status: 'online' | 'offline';
createdTime: string;
}
export interface PolicyParams extends Partial<PolicyRecord> {
current: number;
pageSize: number;
}
export interface PolicyListRes {
list: PolicyRecord[];
total: number;
}
export function queryPolicyList(params: PolicyParams) {
return axios.get<PolicyListRes>('/api/list/policy', {
params,
paramsSerializer: (obj) => {
return qs.stringify(obj);
},
});
}
export interface ServiceRecord {
id: number;
title: string;
description: string;
name?: string;
actionType?: string;
icon?: string;
data?: DescData[];
enable?: boolean;
expires?: boolean;
}
export function queryInspectionList() {
return axios.get('/api/list/quality-inspection');
}
export function queryTheServiceList() {
return axios.get('/api/list/the-service');
}
export function queryRulesPresetList() {
return axios.get('/api/list/rules-preset');
}
-12
View File
@@ -24,15 +24,3 @@ interface MessageStatus {
export function setMessageStatus(data: MessageStatus) { export function setMessageStatus(data: MessageStatus) {
return axios.post<MessageListType>('/api/message/read', data); return axios.post<MessageListType>('/api/message/read', data);
} }
export interface ChatRecord {
id: number;
username: string;
content: string;
time: string;
isCollect: boolean;
}
export function queryChatList() {
return axios.post<ChatRecord[]>('/api/chat/list');
}
-49
View File
@@ -1,49 +0,0 @@
import axios from 'axios';
export interface ProfileBasicRes {
status: number;
video: {
mode: string;
acquisition: {
resolution: string;
frameRate: number;
};
encoding: {
resolution: string;
rate: {
min: number;
max: number;
default: number;
};
frameRate: number;
profile: string;
};
};
audio: {
mode: string;
acquisition: {
channels: number;
};
encoding: {
channels: number;
rate: number;
profile: string;
};
};
}
export function queryProfileBasic() {
return axios.get<ProfileBasicRes>('/api/profile/basic');
}
export type operationLogRes = Array<{
key: string;
contentNumber: string;
updateContent: string;
status: number;
updateTime: string;
}>;
export function queryOperationLog() {
return axios.get<operationLogRes>('/api/operation/log');
}
+5 -2
View File
@@ -7,8 +7,6 @@ import {
} from '@/types/business'; } from '@/types/business';
import { ProjectPaginationParams, CommonCityRes } from '@/types/comm'; import { ProjectPaginationParams, CommonCityRes } from '@/types/comm';
import { HttpResponse } from './interceptor'; import { HttpResponse } from './interceptor';
// import type { RouteRecordNormalized } from 'vue-router';
// import { UserState } from '@/store/modules/user/types';
export interface ProjectRecord { export interface ProjectRecord {
id: string; id: string;
@@ -16,6 +14,7 @@ export interface ProjectRecord {
type: string; type: string;
} }
// ==================== 项目相关 API (project 路由) ====================
// 全部项目列表(包含楼栋) // 全部项目列表(包含楼栋)
export function getAllProjectList(): Promise<AllProjectListRes> { export function getAllProjectList(): Promise<AllProjectListRes> {
return axios.get(`project/getProjectBuildingList`); return axios.get(`project/getProjectBuildingList`);
@@ -46,3 +45,7 @@ export function updateProject(
export function getProjectCityList(): Promise<CommonCityRes> { export function getProjectCityList(): Promise<CommonCityRes> {
return axios.get(`project/getCityList`); return axios.get(`project/getCityList`);
} }
// 删除项目
export function delProject(id: number): Promise<HttpResponse> {
return axios.get(`project/delProject?id=${id}`);
}
+56
View File
@@ -0,0 +1,56 @@
import axios from 'axios';
import {
AllUserListRes,
RoleListRes,
RoleFormParam,
PermissionRes,
RolePermissionRes,
SaveRolePermissionParam,
UpdateUserRoleParam,
} from '@/types/setting';
import { PaginationParams } from '@/types/comm';
import { HttpResponse } from './interceptor';
// ==================== 角色相关 API ====================
export function getRoleList(): Promise<RoleListRes> {
return axios.get('/role/getRoleList');
}
export function createRole(params: RoleFormParam): Promise<HttpResponse> {
return axios.post('/role/createRole', params);
}
export function updateRole(params: RoleFormParam): Promise<HttpResponse> {
return axios.post('/role/updateRole', params);
}
export function delRole(id: number): Promise<HttpResponse> {
return axios.get(`/role/delRole?id=${id}`);
}
export function getPermissionList(): Promise<PermissionRes> {
return axios.get('/role/getPermissionList');
}
export function getRolePermission(id: number): Promise<RolePermissionRes> {
return axios.get(`/role/getRolePermission?id=${id}`);
}
export function saveRolePermission(
params: SaveRolePermissionParam
): Promise<HttpResponse> {
return axios.post('/role/saveRolePermission', params);
}
// 角色下的用户列表
export function getUserListFromRole(
params: PaginationParams
): Promise<AllUserListRes> {
return axios.post('/role/getUserList', params);
}
export function updateUserRole(
params: UpdateUserRoleParam
): Promise<HttpResponse> {
return axios.post('/role/updateUserRole', params);
}
+52
View File
@@ -0,0 +1,52 @@
import axios from 'axios';
import { TopBuildingTagsRes } from '@/types/business';
import { PaginationParams } from '@/types/comm';
import { HttpResponse } from './interceptor';
// ==================== 空间管理 - rooms 路由 ====================
// 创建房源
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);
}
// ==================== 空间管理 - roomOver 路由 ====================
// 房源获取头部楼栋tags
export function getTopBuildingTagsByProjectId(
id: number
): Promise<TopBuildingTagsRes> {
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);
}
// ==================== 空间管理 - roomsGraph 路由 ====================
// 房源图谱列表
export function getRoomsGraphList(params: any): Promise<HttpResponse> {
return axios.post('roomsGraph/getRoomList', params);
}
+4 -543
View File
@@ -1,570 +1,31 @@
import axios from 'axios'; import axios from 'axios';
import { import {
DepartmentFormParams,
DepartmentFormRes,
DepartmentListRes,
DepartmentDetailRes,
AllUserListRes,
FormUserItem,
UserInfoRes,
DisableUserParam,
RoleListRes,
RoleFormParam,
PermissionRes,
RolePermissionRes,
SaveRolePermissionParam,
UpdateUserRoleParam,
SafeInfoRes, SafeInfoRes,
SafeParams, SafeParams,
SettingProjectRes, SettingProjectRes,
SettingProjectParam, SettingProjectParam,
SettingProjectTypeListRes,
SettingProjectTypeRes,
SettingProjectProvinceRes,
DictionaryForm,
CheckProjectParam,
SettingRoomTagsRes,
SettingRoomTagRes,
SettingTenantDictionaryRes,
SettingContractNumbeRes,
CreateCodeSettingParam,
FeeManagementListRes,
CreateFeeParams,
FeeManagementDetailRes,
FeeSelectListParams,
CreateTermSettingParam,
SettingContractTermListRes,
SettingContractTermInfo,
SettingContractTemplateListRes,
CreateTemplateSettingParam,
ContractWordFormParam,
ContractWordSettingListRes,
SettingContractFileInfoRes,
} from '@/types/setting'; } from '@/types/setting';
import { PaginationParams, TenantDictionaryType } from '@/types/comm';
import { HttpResponse } from './interceptor'; import { HttpResponse } from './interceptor';
// ==================== 部门相关 API ==================== // ==================== 系统设置相关 API (setting 路由) ====================
export function createDepartment(
params: DepartmentFormParams
): Promise<DepartmentFormRes> {
return axios.post('/department/createDepartment', params);
}
export function updateDepartment(
params: DepartmentFormParams
): Promise<DepartmentFormRes> {
return axios.post('/department/updateDepartment', params);
}
export function getDepartmentList(): Promise<DepartmentListRes> {
return axios.get('/department/getDepartmentList');
}
export function getDepartmentDetail(id: number): Promise<DepartmentDetailRes> {
return axios.get(`/department/getDepartment?id=${id}`);
}
export function departmentSetUp(id: number): Promise<HttpResponse> {
return axios.get(`/department/setUp?id=${id}`);
}
export function departmentSetDown(id: number): Promise<HttpResponse> {
return axios.get(`/department/setDown?id=${id}`);
}
export function delDepartment(id: number): Promise<HttpResponse> {
return axios.get(`/department/delDepartment?id=${id}`);
}
// 保存部门项目数据
export function saveDataPermission(params: any): Promise<HttpResponse> {
return axios.post('/department/saveDataPermission', params);
}
// 获取部门项目数据
export function getDataPermission(id: number): Promise<HttpResponse> {
return axios.get(`/department/getDataPermission?id=${id}`);
}
// ==================== 用户相关 API ====================
export function getAllUserList(
params: PaginationParams
): Promise<AllUserListRes> {
return axios.post('/user/getAllUserList', params);
}
export function getUserList(params: PaginationParams): Promise<AllUserListRes> {
return axios.post('/user/getUserList', params);
}
export function createUser(params: FormUserItem): Promise<HttpResponse> {
return axios.post('/user/createUser', params);
}
export function updateUser(params: FormUserItem): Promise<HttpResponse> {
return axios.post('/user/updateUser', params);
}
export function getUserInfo(id: number): Promise<UserInfoRes> {
return axios.get(`/user/getUser?id=${id}`);
}
export function delUser(id: number): Promise<HttpResponse> {
return axios.get(`/user/delUser?id=${id}`);
}
export function disableUser(params: DisableUserParam): Promise<HttpResponse> {
return axios.post('/user/disableUser', params);
}
// ==================== 角色相关 API ====================
export function getRoleList(): Promise<RoleListRes> {
return axios.get('/role/getRoleList');
}
export function createRole(params: RoleFormParam): Promise<HttpResponse> {
return axios.post('/role/createRole', params);
}
export function updateRole(params: RoleFormParam): Promise<HttpResponse> {
return axios.post('/role/updateRole', params);
}
export function delRole(id: number): Promise<HttpResponse> {
return axios.get(`/role/delRole?id=${id}`);
}
export function getPermissionList(): Promise<PermissionRes> {
return axios.get('/role/getPermissionList');
}
export function getRolePermission(id: number): Promise<RolePermissionRes> {
return axios.get(`role/getRolePermission?id=${id}`);
}
export function saveRolePermission(
params: SaveRolePermissionParam
): Promise<HttpResponse> {
return axios.post('role/saveRolePermission', params);
}
// 角色下的用户列表
export function getUserListFromRole(
params: PaginationParams
): Promise<AllUserListRes> {
return axios.post('role/getUserList', params);
}
// 角色下的用户列表
export function updateUserRole(
params: UpdateUserRoleParam
): Promise<HttpResponse> {
return axios.post('role/updateUserRole', params);
}
// ==================== 系统设置相关 API ====================
// 通用设置-安全设置信息 // 通用设置-安全设置信息
export function getSafeSettingInfo(): Promise<SafeInfoRes> { export function getSafeSettingInfo(): Promise<SafeInfoRes> {
return axios.get('setting/getSafeSetting'); return axios.get('setting/getSafeSetting');
} }
// 通用设置-安全设置信息保存 // 通用设置-安全设置信息保存
export function saveSafeSetting(params: SafeParams): Promise<HttpResponse> { export function saveSafeSetting(params: SafeParams): Promise<HttpResponse> {
return axios.post('setting/saveSafeSetting', params); return axios.post('setting/saveSafeSetting', params);
} }
// 项目设置信息 // 项目设置信息
export function getProjectSetting(): Promise<SettingProjectRes> { export function getProjectSetting(): Promise<SettingProjectRes> {
return axios.get('setting/getProjectSetting'); return axios.get('setting/getProjectSetting');
} }
// 项目设置信息保存 // 项目设置信息保存
export function saveProjectSetting( export function saveProjectSetting(
params: SettingProjectParam params: SettingProjectParam
): Promise<HttpResponse> { ): Promise<HttpResponse> {
return axios.post('setting/saveProjectSetting', params); return axios.post('setting/saveProjectSetting', params);
} }
// 数据字典-项目管理-全部项目类型列表
export function getAllProjectTypeList(): Promise<SettingProjectTypeListRes> {
return axios.get('dictionary/getAllProjectTypeList');
}
// 数据字典-项目管理-单个项目类型信息
export function getProjectTypeById(id: number): Promise<SettingProjectTypeRes> {
return axios.get(`dictionary/getProjectType?id=${id}`);
}
// 数据字典-项目管理-项目地址列表
export function getProvinceList(): Promise<SettingProjectProvinceRes> {
return axios.get(`dictionary/getProvinceList`);
}
// 数据字典-项目管理-创建项目类型
export function createProjectType(
params: DictionaryForm
): Promise<HttpResponse> {
return axios.post(`dictionary/createProjectType`, params);
}
// 数据字典-项目管理-更新项目类型信息
export function updateProjectType(
params: DictionaryForm
): Promise<HttpResponse> {
return axios.post(`dictionary/updateProjectType`, params);
}
// 数据字典-项目管理-删除项目类型
export function delProjectType(id: number): Promise<SettingProjectProvinceRes> {
return axios.get(`dictionary/delProjectType?id=${id}`);
}
// 数据字典-项目管理-启用/禁用项目类型(单个)
export function checkProjectType(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkProjectType`, params);
}
// 数据字典-项目管理-启用/禁用项目类型(全选)
export function checkAllProjectType(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkAllProjectType`, params);
}
// 数据字典-项目管理-启用/禁用省份(单个)
export function checkProvince(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkProvince`, params);
}
// 数据字典-项目管理-启用/禁用省份(全选)
export function checkAllProvince(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkAllProvince`, params);
}
// 数据字典-空间管理-全部房源标签列表
export function getRoomTagList(): Promise<SettingRoomTagsRes> {
return axios.get(`dictionary/getRoomTagList`);
}
// 数据字典-项目管理-单个房源标签信息
export function getRoomTagById(id: number): Promise<SettingRoomTagRes> {
return axios.get(`dictionary/getRoomTag?id=${id}`);
}
// 数据字典-空间管理-创建房源标签
export function createRoomTag(params: DictionaryForm): Promise<HttpResponse> {
return axios.post(`dictionary/createRoomTag`, params);
}
// 数据字典-空间管理-更新房源标签
export function updateRoomTag(params: DictionaryForm): Promise<HttpResponse> {
return axios.post(`dictionary/updateRoomTag`, params);
}
// 数据字典-空间管理-删除房源标签
export function delRoomTag(id: number): Promise<SettingProjectProvinceRes> {
return axios.get(`dictionary/delRoomTag?id=${id}`);
}
// 数据字典-空间管理-禁用/启用 房源标签(单个)
export function checkRoomTag(params: CheckProjectParam): Promise<HttpResponse> {
return axios.post(`dictionary/checkRoomTag`, params);
}
// 数据字典-空间管理-禁用/启用 房源标签(全选)
export function checkAllRoomTag(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkAllRoomTag`, params);
}
// 数据字典-租客管理-全部租客标签列表(包含所有类型标签)
export function getAllTenantDictionaryList(): Promise<SettingTenantDictionaryRes> {
return axios.get(`dictionary/getAllTenantDictionaryList`);
}
// 数据字典-租客管理-单个租客标签信息
export function getTenantDictionary(
id: number,
type: TenantDictionaryType
): Promise<SettingRoomTagsRes> {
return axios.get(`dictionary/getTenantDictionary?id=${id}&type=${type}`);
}
// 数据字典-租客管理-创建租客标签
export function createTenantDictionary(
params: DictionaryForm
): Promise<HttpResponse> {
return axios.post(`dictionary/createTenantDictionary`, params);
}
// 数据字典-租客管理-更新租客标签
export function updateTenantDictionary(
params: DictionaryForm
): Promise<HttpResponse> {
return axios.post(`dictionary/updateTenantDictionary`, params);
}
// 数据字典-租客管理-删除租客标签
export function delTenantDictionary(
id: number,
type: TenantDictionaryType
): Promise<SettingRoomTagsRes> {
return axios.get(`dictionary/delTenantDictionary?id=${id}&type=${type}`);
}
// 数据字典-租客管理-禁用/启用 租客标签(单个)
export function checkTenantDictionary(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkTenantDictionary`, params);
}
// 数据字典-租客管理-禁用/启用 租客标签(全选)
export function checkAllTenantDictionary(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkAllTenantDictionary`, params);
}
// 数据字典-租客管理--已启用列表(type:1租客类型,2租客来源,3租客行业分类,4租客企业规模)
export function getTenantDictionaryListByType(
type: TenantDictionaryType
): Promise<SettingRoomTagsRes> {
return axios.get(`dictionary/getTenantDictionaryList?type=${type}`);
}
// 合同设置-合同编号设置-全部启用列表
export function getAllCodeSettingList(): Promise<SettingContractNumbeRes> {
return axios.get(`contractSetting/getAllCodeSettingList`);
}
// 合同设置-合同编号设置-新增
export function createCodeSetting(
params: CreateCodeSettingParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/createCodeSetting`, params);
}
// 合同设置-合同编号设置-编辑
export function updateCodeSetting(
params: CreateCodeSettingParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/updateCodeSetting`, params);
}
// 合同设置-合同编号设置-删除
export function delCodeSetting(id: number): Promise<HttpResponse> {
return axios.get(`contractSetting/delCodeSetting?id=${id}`);
}
// 合同设置-合同编号设置-单个详情
export function getCodeSettingFromId(
id: number
): Promise<SettingContractNumbeRes> {
return axios.get(`contractSetting/getCodeSetting?id=${id}`);
}
// 合同设置-合同编号设置-启用/禁用
export function disableCodeSetting(
params: DisableUserParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/disableCodeSetting`, params);
}
// 合同设置-合同编号设置-列表(带分页)
export function getCodeSettingList(
params: PaginationParams
): Promise<SettingContractNumbeRes> {
return axios.post(`contractSetting/getCodeSettingList`, params);
}
// 合同设置-合同条款设置-全部启用列表
export function getAllTermsSettingList(): Promise<any> {
return axios.get(`contractSetting/getAllTermsSettingList`);
}
// 合同设置-合同条款设置-新增
export function createTermsSetting(
params: CreateTermSettingParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/createTermsSetting`, params);
}
// 合同设置-合同条款设置-编辑
export function updateTermsSetting(
params: CreateTermSettingParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/updateTermsSetting`, params);
}
// 合同设置-合同条款设置-删除
export function delTermsSetting(id: number): Promise<HttpResponse> {
return axios.get(`contractSetting/delTermsSetting?id=${id}`);
}
// 合同设置-合同条款设置-单个条款配置详情
export function getTermsSettingFromId(
id: number
): Promise<SettingContractTermInfo> {
return axios.get(`contractSetting/getTermsSetting?id=${id}`);
}
// 合同设置-合同条款设置-单个条款数据详情
export function getTermsDataFromId(params: any): Promise<any> {
return axios.post(`contractSetting/getTerm`, params);
}
// 合同设置-合同条款设置-启用/禁用
export function disableTermsSetting(
params: DisableUserParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/disableTermsSetting`, params);
}
// 合同设置-合同条款设置-列表(带分页)
export function getTermsSettingList(
params: PaginationParams
): Promise<SettingContractTermListRes> {
return axios.post(`contractSetting/getTermsSettingList`, params);
}
// 合同设置-财务设置-全部列表(带分页)
export function getFeeList(
params: PaginationParams
): Promise<FeeManagementListRes> {
return axios.post(`financeSetting/getFeeList`, params);
}
// 合同设置-财务设置-新增
export function createFee(params: CreateFeeParams): Promise<HttpResponse> {
return axios.post(`financeSetting/createFee`, params);
}
// 合同设置-财务设置-编辑
export function updateFee(params: CreateFeeParams): Promise<HttpResponse> {
return axios.post(`financeSetting/updateFee`, params);
}
// 合同设置-财务设置-删除
export function delFee(id: number): Promise<HttpResponse> {
return axios.get(`financeSetting/delFee?id=${id}`);
}
// 合同设置-财务设置-单个详情
export function getFeeFromId(id: number): Promise<FeeManagementDetailRes> {
return axios.get(`financeSetting/getFee?id=${id}`);
}
// 合同设置-财务设置-启用/禁用
export function disableFee(params: DisableUserParam): Promise<HttpResponse> {
return axios.post(`financeSetting/disableFee`, params);
}
// 合同设置-财务设置-获取下拉列表
export function getUseFeeList(
params: FeeSelectListParams
): Promise<FeeManagementListRes> {
return axios.post(`financeSetting/getUseFeeList`, params);
}
// 合同设置-流水账户管理--获取下拉列表
export function getUseTurnoverAccountList(params: any): Promise<any> {
return axios.get(`financeSetting/getUseTurnoverAccountList`, params);
}
// 合同设置-合同模板管理-全部启用列表
export function getAllTemplateSettingList(): Promise<SettingContractNumbeRes> {
return axios.get(`contractSetting/getAllTemplateSettingList`);
}
// 合同设置-合同模板管理-新增
export function createTemplateSetting(
params: CreateTemplateSettingParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/createTemplateSetting`, params);
}
// 合同设置-合同模板管理-编辑
export function updateTemplateSetting(params: any): Promise<HttpResponse> {
return axios.post(`contractSetting/saveTemplateSetting`, params);
}
// 合同设置-合同模板管理-删除
export function delTemplateSetting(id: number): Promise<HttpResponse> {
return axios.get(`contractSetting/delTemplateSetting?id=${id}`);
}
// 合同设置-合同模板管理-单个详情
export function getTemplateSettingFromId(id: number): Promise<any> {
return axios.get(`contractSetting/getTemplateSetting?id=${id}`);
}
// 合同设置-合同模板管理-启用/禁用
export function disableTemplateSetting(
params: DisableUserParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/disableTemplateSetting`, params);
}
// 合同设置-合同模板管理-列表(带分页)
export function getTemplateSettingList(
params: PaginationParams
): Promise<SettingContractTemplateListRes> {
return axios.post(`contractSetting/getTemplateSettingList`, params);
}
// 合同设置-合同文档模板管理-全部启用列表
export function getAllWordSettingList(): Promise<ContractWordSettingListRes> {
return axios.get(`contractSetting/getAllWordSettingList`);
}
// 合同设置-合同文档模板管理-新增
export function createWordSetting(
params: ContractWordFormParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/createWordSetting`, params);
}
// 合同设置-合同文档模板管理-编辑(更新)
export function updateWordSetting(
params: ContractWordFormParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/updateWordSetting`, params);
}
// 合同设置-合同文档模板管理-删除
export function delWordSetting(id: number): Promise<HttpResponse> {
return axios.get(`contractSetting/delWordSetting?id=${id}`);
}
// 合同设置-合同文档模板管理-单个详情
export function getWordSettingFromId(
id: number
): Promise<SettingContractFileInfoRes> {
return axios.get(`contractSetting/getWordSetting?id=${id}`);
}
// 合同设置-合同文档模板管理-启用/禁用
export function disableWordSetting(
params: DisableUserParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/disableWordSetting`, params);
}
// 合同设置-合同文档模板管理-列表(带分页)
export function getWordSettingList(
params: PaginationParams
): Promise<ContractWordSettingListRes> {
return axios.post(`contractSetting/getWordSettingList`, params);
}
// 合同标签列表(已勾选)
export function getContractTagsList(): Promise<any> {
return axios.get(`dictionary/getContractTagsList`);
}
// 数据字典-合同管理-合同标签列表(全部)
export function getAllContractTagsList(): Promise<any> {
return axios.get(`dictionary/getAllContractTagsList`);
}
// 数据字典-合同管理-创建合同标签
export function createContractTags(
params: DictionaryForm
): Promise<HttpResponse> {
return axios.post(`dictionary/createContractTags`, params);
}
// 数据字典-合同管理-更新合同标签
export function updateContractTags(
params: DictionaryForm
): Promise<HttpResponse> {
return axios.post(`dictionary/updateContractTags`, params);
}
// 数据字典-合同管理-删除合同标签
export function delContractTags(
id: number
): Promise<SettingProjectProvinceRes> {
return axios.get(`dictionary/delContractTags?id=${id}`);
}
// 数据字典-合同管理-禁用/启用 合同标签(单个)
export function checkContractTags(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkContractTags`, params);
}
// 数据字典-合同管理-禁用/启用 合同标签(全选)
export function checkAllContractTags(
params: CheckProjectParam
): Promise<HttpResponse> {
return axios.post(`dictionary/checkAllContractTags`, params);
}
// 数据字典-合同管理-单个合同标签信息
export function getContractTags(
id: number
): Promise<SettingProjectProvinceRes> {
return axios.get(`dictionary/getContractTags?id=${id}`);
}
// 系统配置-流程设置-流程设置列表
// 类型:1审批流,2工作流
export function getWorkflowsList(type: number): Promise<any> {
return axios.get(`workflows/getList?type=${type}`);
}
// 创建流程
export function createWorkflows(params: any): Promise<any> {
return axios.post(`workflows/create`, params);
}
// 保存流程
export function saveWorkflows(params: any): Promise<any> {
return axios.post(`workflows/save`, params);
}
// 审批流详情
export function getWorkflowsDetail(id: number): Promise<any> {
return axios.get(`workflows/getDetail?id=${id}`);
}
// 审批流启用禁用
export function toggleWorkflowsDisable(params: any): Promise<any> {
return axios.post('workflows/disable', params);
}
// 删除审批流
export function delWorkflows(id: number): Promise<any> {
return axios.get(`workflows/del?id=${id}`);
}
+41
View File
@@ -0,0 +1,41 @@
import axios from 'axios';
import { PaginationParams } from '@/types/comm';
import { HttpResponse } from './interceptor';
// ==================== 租客管理 API (tenants 路由) ====================
// 创建租客
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);
}
-88
View File
@@ -1,88 +0,0 @@
import axios from 'axios';
export interface MyProjectRecord {
id: number;
name: string;
description: string;
peopleNumber: number;
contributors: {
name: string;
email: string;
avatar: string;
}[];
}
export function queryMyProjectList() {
return axios.post('/api/user/my-project/list');
}
export interface MyTeamRecord {
id: number;
avatar: string;
name: string;
peopleNumber: number;
}
export function queryMyTeamList() {
return axios.post('/api/user/my-team/list');
}
export interface LatestActivity {
id: number;
title: string;
description: string;
avatar: string;
}
export function queryLatestActivity() {
return axios.post<LatestActivity[]>('/api/user/latest-activity');
}
export function saveUserInfo() {
return axios.post('/api/user/save-info');
}
export interface BasicInfoModel {
email: string;
nickname: string;
countryRegion: string;
area: string;
address: string;
profile: string;
}
export interface EnterpriseCertificationModel {
accountType: number;
status: number;
time: string;
legalPerson: string;
certificateType: string;
authenticationNumber: string;
enterpriseName: string;
enterpriseCertificateType: string;
organizationCode: string;
}
export type CertificationRecord = Array<{
certificationType: number;
certificationContent: string;
status: number;
time: string;
}>;
export interface UnitCertification {
enterpriseInfo: EnterpriseCertificationModel;
record: CertificationRecord;
}
export function queryCertification() {
return axios.post<UnitCertification>('/api/user/certification');
}
export function userUploadApi(
data: FormData,
config: {
controller: AbortController;
onUploadProgress?: (progressEvent: any) => void;
}
) {
// const controller = new AbortController();
return axios.post('/api/user/upload', data, config);
}
+47 -3
View File
@@ -1,6 +1,13 @@
import axios from 'axios'; import axios from 'axios';
import type { RouteRecordNormalized } from 'vue-router';
import { UserPermission } from '@/types/api'; import { UserPermission } from '@/types/api';
import {
AllUserListRes,
FormUserItem,
UserInfoRes,
DisableUserParam,
} from '@/types/setting';
import { PaginationParams } from '@/types/comm';
import { HttpResponse } from './interceptor';
export interface LoginData { export interface LoginData {
username: string; username: string;
@@ -14,6 +21,14 @@ export interface UserPermissionList {
permission: UserPermission[]; permission: UserPermission[];
} }
export interface UserProject {
id: number;
name: string;
image?: string;
imageUrl?: string;
[key: string]: any;
}
export interface UserState { export interface UserState {
name?: string; name?: string;
avatar?: string; avatar?: string;
@@ -32,6 +47,8 @@ export interface UserState {
certification?: number; certification?: number;
role: '' | '*' | 'admin' | 'user'; role: '' | '*' | 'admin' | 'user';
permission: UserPermission[]; permission: UserPermission[];
projects?: UserProject[];
currentProjectId?: number;
} }
export async function login(data: LoginData) { export async function login(data: LoginData) {
const result = axios.post<LoginRes>('/auth/login', data); const result = axios.post<LoginRes>('/auth/login', data);
@@ -51,6 +68,33 @@ export function getUserInfo() {
return axios.get<UserState>('/auth/profile'); return axios.get<UserState>('/auth/profile');
} }
export function getMenuList() { // ==================== 用户管理 API (user 路由) ====================
return axios.post<RouteRecordNormalized[]>('/api/user/menu'); export function getAllUserList(
params: PaginationParams
): Promise<AllUserListRes> {
return axios.post('/user/getAllUserList', params);
}
export function getUserList(params: PaginationParams): Promise<AllUserListRes> {
return axios.post('/user/getUserList', params);
}
export function createUser(params: FormUserItem): Promise<HttpResponse> {
return axios.post('/user/createUser', params);
}
export function updateUser(params: FormUserItem): Promise<HttpResponse> {
return axios.post('/user/updateUser', params);
}
export function getUserById(id: number): Promise<UserInfoRes> {
return axios.get(`/user/getUser?id=${id}`);
}
export function delUser(id: number): Promise<HttpResponse> {
return axios.get(`/user/delUser?id=${id}`);
}
export function disableUser(params: DisableUserParam): Promise<HttpResponse> {
return axios.post('/user/disableUser', params);
} }
-73
View File
@@ -1,73 +0,0 @@
import axios from 'axios';
import { GeneralChart } from '@/types/global';
export interface ChartDataRecord {
x: string;
y: number;
name: string;
}
export interface DataChainGrowth {
quota: string;
}
export interface DataChainGrowthRes {
count: number;
growth: number;
chartData: {
xAxis: string[];
data: { name: string; value: number[] };
};
}
export function queryDataChainGrowth(data: DataChainGrowth) {
return axios.post<DataChainGrowthRes>('/api/data-chain-growth', data);
}
export interface PopularAuthorRes {
list: {
ranking: number;
author: string;
contentCount: number;
clickCount: number;
}[];
}
export function queryPopularAuthor() {
return axios.get<PopularAuthorRes>('/api/popular-author/list');
}
export interface ContentPublishRecord {
x: string[];
y: number[];
name: string;
}
export function queryContentPublish() {
return axios.get<ContentPublishRecord[]>('/api/content-publish');
}
export function queryContentPeriodAnalysis() {
return axios.post<GeneralChart>('/api/content-period-analysis');
}
export interface PublicOpinionAnalysis {
quota: string;
}
export interface PublicOpinionAnalysisRes {
count: number;
growth: number;
chartData: ChartDataRecord[];
}
export function queryPublicOpinionAnalysis(data: DataChainGrowth) {
return axios.post<PublicOpinionAnalysisRes>(
'/api/public-opinion-analysis',
data
);
}
export interface DataOverviewRes {
xAxis: string[];
data: Array<{ name: string; value: number[]; count: number }>;
}
export function queryDataOverview() {
return axios.post<DataOverviewRes>('/api/data-overview');
}
+33
View File
@@ -0,0 +1,33 @@
import axios from 'axios';
// ==================== 流程设置 API (workflows 路由) ====================
// 系统配置-流程设置-流程设置列表
// 类型:1审批流,2工作流
export function getWorkflowsList(type: number): Promise<any> {
return axios.get(`workflows/getList?type=${type}`);
}
// 创建流程
export function createWorkflows(params: any): Promise<any> {
return axios.post(`workflows/create`, params);
}
// 保存流程
export function saveWorkflows(params: any): Promise<any> {
return axios.post(`workflows/save`, params);
}
// 审批流详情
export function getWorkflowsDetail(id: number): Promise<any> {
return axios.get(`workflows/getDetail?id=${id}`);
}
// 审批流启用禁用
export function toggleWorkflowsDisable(params: any): Promise<any> {
return axios.post('workflows/disable', params);
}
// 删除审批流
export function delWorkflows(id: number): Promise<any> {
return axios.get(`workflows/del?id=${id}`);
}
-236
View File
@@ -1,236 +0,0 @@
# DataTable 组件使用指南
## 概述
`DataTable` 是一个基于 Arco Design Vue 的 `a-table` 组件封装,提供了统一的分页配置和事件处理,可以在多个页面中复用。
## 特性
- 🚀 统一的分页配置
- 📊 支持自定义列配置
- 🔄 内置加载状态管理
- 🎯 支持排序和筛选
- 📱 响应式设计
- 🎨 可自定义样式
## 基础用法
### 1. 使用 DataTable 组件
```vue
<template>
<DataTable
:data="tableData"
:columns="columns"
:loading="loading"
:total="total"
:current="current"
:page-size="pageSize"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<!-- 自定义列插槽 -->
<template #status="{ record }">
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
{{ record.status }}
</a-tag>
</template>
<template #operate="{ record }">
<a-space>
<a-button size="small">查看</a-button>
<a-button size="small" type="primary">编辑</a-button>
</a-space>
</template>
</DataTable>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import type { TableColumnData, TableData } from '@arco-design/web-vue';
import useDataTable from '@/composables/useDataTable';
// 使用 DataTable composable
const {
data: tableData,
columns,
loading,
total,
current,
pageSize,
setData,
setColumns,
setLoading,
setTotal,
setCurrent,
setPageSize,
} = useDataTable({
initialPage: 1,
initialPageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
});
// 列配置
const columnConfig: TableColumnData[] = [
{ title: '姓名', dataIndex: 'name', key: 'name' },
{ title: '年龄', dataIndex: 'age', key: 'age' },
{ title: '状态', dataIndex: 'status', key: 'status', slotName: 'status' },
{ title: '操作', key: 'operate', slotName: 'operate', width: 200 },
];
// 模拟数据
const mockData: TableData[] = [
{ id: 1, name: '张三', age: 25, status: 'active' },
{ id: 2, name: '李四', age: 30, status: 'inactive' },
];
onMounted(() => {
setColumns(columnConfig);
setData(mockData);
setTotal(mockData.length);
});
// 分页事件处理
const handlePageChange = (page: number) => {
setCurrent(page);
// 调用 API 获取数据
fetchData(page, pageSize.value);
};
const handlePageSizeChange = (pageSize: number) => {
setPageSize(pageSize);
// 调用 API 获取数据
fetchData(1, pageSize);
};
const fetchData = async (page: number, size: number) => {
setLoading(true);
try {
// 调用 API
// const { data, total } = await api.getList({ page, size });
// setData(data);
// setTotal(total);
} finally {
setLoading(false);
}
};
</script>
```
### 2. 高级配置
```vue
<template>
<DataTable
:data="tableData"
:columns="columns"
:loading="loading"
:total="total"
:current="current"
:page-size="pageSize"
:bordered="true"
:stripe="true"
:size="'small'"
:scroll="{ x: 1200, y: 400 }"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
@selection-change="handleSelectionChange"
@sorter-change="handleSorterChange"
@filter-change="handleFilterChange"
>
<!-- 插槽内容 -->
</DataTable>
</template>
```
## Props 说明
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| data | TableData[] | [] | 表格数据 |
| columns | TableColumnData[] | [] | 列配置 |
| loading | boolean | false | 加载状态 |
| rowKey | string | 'id' | 行键 |
| pagination | boolean \| PaginationProps | true | 分页配置 |
| bordered | boolean | false | 是否显示边框 |
| size | 'mini' \| 'small' \| 'medium' \| 'large' | 'medium' | 表格大小 |
| stripe | boolean | false | 是否显示斑马纹 |
| tableLayout | 'auto' \| 'fixed' | 'auto' | 表格布局 |
| scroll | object | - | 滚动配置 |
| current | number | 1 | 当前页码 |
| pageSize | number | 10 | 每页条数 |
| total | number | 0 | 总条数 |
| showTotal | boolean | true | 是否显示总数 |
| pageSizeOptions | number[] | [10, 20, 50, 100] | 每页条数选项 |
| showJumper | boolean | true | 是否显示快速跳转 |
| showPageSize | boolean | true | 是否显示每页条数选择器 |
## Events 说明
| 事件名 | 参数 | 说明 |
|--------|------|------|
| page-change | (page: number) | 页码变化 |
| page-size-change | (pageSize: number) | 每页条数变化 |
| selection-change | (selection: TableData[]) | 选择变化 |
| sorter-change | (sorter: any) | 排序变化 |
| filter-change | (filters: any) | 筛选变化 |
## useDataTable Composable
`useDataTable` 是一个用于管理表格状态的 composable,提供了以下功能:
### 返回值
| 属性 | 类型 | 说明 |
|------|------|------|
| data | TableData[] | 表格数据 |
| columns | TableColumnData[] | 列配置 |
| loading | boolean | 加载状态 |
| pagination | PaginationProps | 分页配置 |
| current | number | 当前页码 |
| pageSize | number | 每页条数 |
| total | number | 总条数 |
| setData | (data: TableData[]) => void | 设置数据 |
| setColumns | (columns: TableColumnData[]) => void | 设置列配置 |
| setLoading | (loading: boolean) => void | 设置加载状态 |
| setTotal | (total: number) => void | 设置总条数 |
| setCurrent | (current: number) => void | 设置当前页码 |
| setPageSize | (pageSize: number) => void | 设置每页条数 |
| resetPagination | () => void | 重置分页 |
### 配置选项
```typescript
interface UseDataTableOptions {
initialPage?: number; // 初始页码
initialPageSize?: number; // 初始每页条数
pageSizeOptions?: number[]; // 每页条数选项
showTotal?: boolean; // 是否显示总数
showJumper?: boolean; // 是否显示快速跳转
showPageSize?: boolean; // 是否显示每页条数选择器
rowKey?: string; // 行键
bordered?: boolean; // 是否显示边框
size?: 'mini' | 'small' | 'medium' | 'large'; // 表格大小
stripe?: boolean; // 是否显示斑马纹
tableLayout?: 'auto' | 'fixed'; // 表格布局
}
```
## 最佳实践
1. **统一使用 DataTable 组件**:在项目中统一使用 DataTable 组件,避免重复配置分页逻辑。
2. **使用 useDataTable composable**:利用 composable 管理表格状态,提高代码复用性。
3. **合理配置列插槽**:对于需要自定义渲染的列,使用插槽进行自定义。
4. **处理加载状态**:在数据请求时正确设置 loading 状态。
5. **分页事件处理**:在分页事件中调用 API 获取对应页的数据。
## 注意事项
1. 确保在 `main.ts` 中注册了 `DataTable` 组件。
2. 列配置中的 `slotName` 要与模板中的插槽名称一致。
3. 数据源需要包含 `rowKey` 指定的字段。
4. 分页事件处理中应该调用实际的 API 接口。
+39 -16
View File
@@ -1,20 +1,29 @@
<template> <template>
<!-- Arco a-upload 只认 #upload-button 插槽作为触发区,需把自定义内容传入该插槽 -->
<a-upload <a-upload
draggable draggable
:accept="accept" :accept="accept"
:disabled="disabled"
:custom-request="customRequest" :custom-request="customRequest"
:multiple="maxCount > 1" :multiple="maxCount > 1"
:limit="maxCount" :limit="maxCount"
:show-file-list="showFileList"
:show-upload-button="showUploadButtonConfig"
@change="handleChange" @change="handleChange"
> >
<div style="padding: 20px; text-align: center"> <template #upload-button>
<icon-upload style="font-size: 32px; color: #666" /> <!-- 插槽 trigger:可传入自定义触发内容(如按钮),不传则使用下方默认拖拽区 -->
<div style="margin-top: 8px"> <slot name="trigger">
<p>点击或拖拽文件到此处上传</p> <div style="padding: 20px; text-align: center">
<p v-if="accept">支持的格式:{{ accept }}</p> <icon-upload style="font-size: 32px; color: #666" />
<p>最大支持 {{ maxSizeText }}</p> <div style="margin-top: 8px">
</div> <p>点击或拖拽文件到此处上传</p>
</div> <p v-if="accept">支持的格式:{{ accept }}</p>
<p>最大支持 {{ maxSizeText }}</p>
</div>
</div>
</slot>
</template>
</a-upload> </a-upload>
</template> </template>
@@ -30,14 +39,24 @@
objectKey: string; objectKey: string;
} }
const props = defineProps<{ const props = withDefaults(
objectKeys: string[]; defineProps<{
signedPutURL: string[]; objectKeys?: string[];
uploadType: UploadType; signedPutURL?: string[];
maxSize?: number; // 单位字节 uploadType: UploadType;
maxCount?: number; maxSize?: number; // 单位字节
accept?: string; // 限制类型,例如 "application/pdf" 或 ".doc,.docx,.xls" 或 "application/pdf,.docx" maxCount?: number;
}>(); accept?: string; // 限制类型,例如 "application/pdf" 或 ".doc,.docx,.xls" 或 "application/pdf,.docx"
showFileList?: boolean;
disabled?: boolean;
}>(),
{
objectKeys: () => [],
signedPutURL: () => [],
showFileList: true,
disabled: false,
}
);
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:objectKeys', value: string[]): void; (e: 'update:objectKeys', value: string[]): void;
@@ -51,6 +70,10 @@
const maxCount = props.maxCount ?? 1; const maxCount = props.maxCount ?? 1;
const accept = props.accept ?? '*/*'; // 默认支持所有文件 const accept = props.accept ?? '*/*'; // 默认支持所有文件
// 达到 limit 后仍显示上传按钮,方便替换文件(Arco 默认会隐藏)
const showUploadButtonConfig = computed(() =>
maxCount > 0 ? { showOnExceedLimit: true } : true
);
const maxSizeText = computed(() => { const maxSizeText = computed(() => {
const size = props.maxSize || 10 * 1024 * 1024; const size = props.maxSize || 10 * 1024 * 1024;
return `${Math.round(size / 1024 / 1024)}MB`; return `${Math.round(size / 1024 / 1024)}MB`;
-116
View File
@@ -1,116 +0,0 @@
# ImageUpload 图片上传组件
一个可复用的图片上传组件,支持自定义上传类型、文件大小限制等功能。
## 功能特性
- 支持图片格式:JPG、PNG、GIF、WEBP
- 可自定义文件大小限制
- 支持上传进度显示
- 支持图片预览
- 支持多文件上传
- 上传成功后自动回传 URL
## Props
| 参数 | 类型 | 默认值 | 说明 |
| ---------- | ------------ | ---------------- | ---------------------------- |
| modelValue | UploadFile[] | [] | 文件列表,支持 v-model |
| uploadType | UploadType | - | 上传类型:1 合同模版,2 图片 |
| maxSize | number | 10 _ 1024 _ 1024 | 最大文件大小(字节) |
| maxCount | number | - | 最大上传数量 |
## Events
| 事件名 | 参数 | 说明 |
| ----------------- | -------------------------------------------- | ------------------ |
| update:modelValue | (value: UploadFile[]) | 文件列表更新时触发 |
| upload-success | (file: UploadFile, response: UploadResponse) | 上传成功时触发 |
| upload-error | (error: Error) | 上传失败时触发 |
## 类型定义
```typescript
interface UploadFile {
uid: string;
name: string;
url?: string;
status: 'done' | 'uploading' | 'error';
percent?: number;
}
interface UploadResponse {
filename: string;
size: number;
signedPutURL: string;
objectKey?: string;
}
type UploadType = 1 | 2; // 1合同模版,2图片
```
## 使用示例
### 基础用法
```vue
<template>
<ImageUpload
v-model="images"
:upload-type="2"
@upload-success="handleUploadSuccess"
@upload-error="handleUploadError"
/>
</template>
<script setup>
import { ref } from 'vue';
import ImageUpload from '@/components/common/ImageUpload.vue';
const images = ref([]);
const handleUploadSuccess = (file, response) => {
console.log('上传成功:', response.url);
};
const handleUploadError = (error) => {
console.error('上传失败:', error.message);
};
</script>
```
### 自定义文件大小限制
```vue
<template>
<ImageUpload
v-model="images"
:upload-type="2"
:max-size="5 * 1024 * 1024"
@upload-success="handleUploadSuccess"
/>
</template>
```
### 在表单中使用
```vue
<template>
<a-form-item label="项目图片" field="images">
<ImageUpload
v-model="form.images"
:upload-type="2"
:max-size="10 * 1024 * 1024"
@upload-success="handleUploadSuccess"
@upload-error="handleUploadError"
/>
</a-form-item>
</template>
```
## 注意事项
1. 组件会自动处理文件验证,包括文件大小和格式检查
2. 上传成功后,文件对象会自动更新 URL 和状态
3. 支持 v-model 双向绑定,父组件可以直接操作文件列表
4. 上传类型必须从父组件传入,用于区分不同的上传场景
-12
View File
@@ -1,12 +0,0 @@
## 组件说明
该组件非官方最终设计规范,以单独组件存在。
同时仅仅提供最基本的功能,后续进行优化及更改。
## Component description
The component unofficial final design specification exists as a separate component.
At the same time, only the most basic functions are provided, and subsequent optimizations and changes will be made.
+168 -101
View File
@@ -1,135 +1,202 @@
<template> <template>
<div class="top-menu"> <div class="top-menu">
<a-select :style="{ width: '200px' }" placeholder="请选择" :bordered="false"> <a-select
<template #label="{ data }"> v-model="selectedProjectId"
<div class="current-building"> :style="{ width: '200px' }"
<img class="img" src="@/assets/images/building1.png" alt=""> placeholder="请选择"
<p class="building-name">星河商务中心</p> :bordered="false"
</div> @change="handleProjectChange"
</template> >
<a-option> <template #label>
<p class="building-name">星河商务中心</p> <div v-if="currentProject" class="current-building">
</a-option> <img
</a-select> class="img"
<a-menu :style="{ width: '100%' }" mode="horizontal" :selected-keys="[selectedKey]" :src="
@menu-item-click="onMenuClick" style="width: 100%"> currentProject.imageUrl || currentProject.image || defaultImage
<a-menu-item v-for="item in topLevelMenus" :key="item.path"> "
{{ t(item.meta?.locale || '') }} :alt="currentProject.name"
</a-menu-item> />
</a-menu> <p class="building-name">{{ currentProject.name }}</p>
</div> </div>
<div v-else class="current-building">
<p class="building-name">请选择项目</p>
</div>
</template>
<a-option
v-for="project in projectList"
:key="project.id"
:value="project.id"
>
<div>
<div class="flex items-center">
<img
class="w-[30px] h-[30px] object-contain"
:src="project.imageUrl || project.image || defaultImage"
:alt="project.name"
/>
<p class="ml-[10px]">{{ project.name }}</p>
</div>
</div>
</a-option>
</a-select>
<a-menu
:style="{ width: '100%' }"
mode="horizontal"
:selected-keys="[selectedKey]"
style="width: 100%"
@menu-item-click="onMenuClick"
>
<a-menu-item v-for="item in topLevelMenus" :key="item.path">
{{ t(item.meta?.locale || '') }}
</a-menu-item>
</a-menu>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed, watch, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import type { RouteRecordNormalized } from 'vue-router'; import type { RouteRecordNormalized } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useUserStore } from '@/store';
import type { UserProject } from '@/store/modules/user/types';
const router = useRouter(); // 默认图片路径
const { t } = useI18n(); const defaultImage = new URL('@/assets/images/building1.png', import.meta.url)
.href;
// 获取一级路由(顶级菜单) const router = useRouter();
const topLevelMenus = computed(() => { const { t } = useI18n();
return router.getRoutes() const userStore = useUserStore();
.filter(route => {
// 过滤出一级路由:有 meta 且有 children
return route.meta &&
route.children?.length > 0 &&
route.meta.isFirst &&
route.meta.locale;
})
.map(route => ({
path: route.path,
name: route.name,
meta: route.meta
}));
});
const buildingList = ref([ // 获取一级路由(顶级菜单)
{ logo: new URL('@/assets/images/building1.png', import.meta.url).href, title: '星河商务中心', id: 1 }, const topLevelMenus = computed(() => {
{ logo: new URL('@/assets/images/building1.png', import.meta.url).href, title: '南山科学科学园', id: 2 }, return router
{ logo: new URL('@/assets/images/building1.png', import.meta.url).href, title: '金融科技大厦', id: 3 }, .getRoutes()
{ logo: new URL('@/assets/images/building1.png', import.meta.url).href, title: '福田平安大厦', id: 4 }, .filter((route) => {
]); // 过滤出一级路由:有 meta 且有 children
return (
route.meta &&
route.children?.length > 0 &&
route.meta.isFirst &&
route.meta.locale
);
})
.map((route) => ({
path: route.path,
name: route.name,
meta: route.meta,
}));
});
const currentBuildingId = ref(1); // 项目列表
const projectList = computed<UserProject[]>(() => {
return userStore.projects || [];
});
const getCurrentBuilding = (id: number) => { // 选中的项目ID,优先使用 store 中保存的值
return buildingList.value.find(building => building.id === id); const selectedProjectId = ref<number | undefined>(userStore.currentProjectId);
};
defineProps<{ // 当前选中的项目
const currentProject = computed(() => {
if (!selectedProjectId.value) return null;
return projectList.value.find(
(project) => project.id === selectedProjectId.value
);
});
// 项目切换处理
const handleProjectChange = (projectId: number) => {
selectedProjectId.value = projectId;
// 保存到 store
userStore.setInfo({ currentProjectId: projectId });
};
// 监听 projects 变化,设置默认选中第一个
watch(
() => userStore.projects,
(projects) => {
if (projects && projects.length > 0) {
// 优先使用 store 中保存的项目ID,否则使用第一个项目
const projectId = userStore.currentProjectId || projects[0].id;
selectedProjectId.value = projectId;
// 如果 store 中没有保存,则保存第一个项目ID
if (!userStore.currentProjectId) {
userStore.setInfo({ currentProjectId: projectId });
}
}
},
{ immediate: true }
);
// 组件挂载时,如果已有项目数据,设置默认选中
onMounted(() => {
if (projectList.value.length > 0) {
// 优先使用 store 中保存的项目ID,否则使用第一个项目
const projectId = userStore.currentProjectId || projectList.value[0].id;
selectedProjectId.value = projectId;
// 如果 store 中没有保存,则保存第一个项目ID
if (!userStore.currentProjectId) {
userStore.setInfo({ currentProjectId: projectId });
}
}
});
defineProps<{
selectedKey: string; selectedKey: string;
}>(); }>();
const emit = defineEmits(['menu-click']); const emit = defineEmits(['menu-click']);
const onMenuClick = (key: string) => { const onMenuClick = (key: string) => {
// 查找对应的路由 // 查找对应的路由
const targetRoute = topLevelMenus.value.find(item => item.path === key); const targetRoute = topLevelMenus.value.find((item) => item.path === key);
if (targetRoute) { if (targetRoute) {
// 跳转到对应路由 // 跳转到对应路由
router.push(targetRoute.path); router.push(targetRoute.path);
} }
// 触发父组件事件 // 触发父组件事件
emit('menu-click', key); emit('menu-click', key);
}; };
</script> </script>
<style lang="less"> <style lang="less">
.top-menu { .top-menu {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
.current-building, .current-building,
.building { .building {
display: flex;
align-items: center;
.img {
width: 30px;
height: 30px;
}
.building-name {
margin-left: 10px;
}
}
:deep(.arco-select-option-content) {
display: flex !important;
align-items: center !important;
.current-building,
.building {
display: flex; display: flex;
align-items: center; align-items: center;
.img { .img {
width: 30px; width: 30px;
height: 30px; height: 30px;
} }
.building-name { .building-name {
margin-left: 10px; margin-left: 10px;
} }
}
} }
}
.building-item { </style>
.building {
display: flex;
align-items: center;
.img {
width: 30px;
height: 30px;
}
.building-name {
margin-left: 10px;
}
}
}
:deep(.arco-select-option-content) {
display: flex !important;
align-items: center !important;
.current-building,
.building {
display: flex;
align-items: center;
.img {
width: 30px;
height: 30px;
}
.building-name {
margin-left: 10px;
}
}
}
}
</style>
-196
View File
@@ -1,196 +0,0 @@
# 分页处理 Composable 使用指南
## 概述
本项目提供了两个用于处理表格分页的 composable:
1. `usePagination` - 基础分页事件处理
2. `useTableWithPagination` - 完整的分页和数据获取集成
## usePagination
### 基础用法
```typescript
import usePagination from '@/composables/usePagination';
const { handlePageChange, handlePageSizeChange } = usePagination({
onPageChange: () => getUserListData(),
onPageSizeChange: () => getUserListData(),
resetToFirstPage: true,
});
// 在分页事件中使用
const onPageChange = (page: number) => {
handlePageChange(page, setCurrent);
};
const onPageSizeChange = (pageSize: number) => {
handlePageSizeChange(pageSize, setPageSize, setCurrent);
};
```
### 配置选项
| 选项 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| onPageChange | Function | - | 页码变化时的回调函数 |
| onPageSizeChange | Function | - | 每页条数变化时的回调函数 |
| resetToFirstPage | boolean | true | 切换每页条数时是否重置到第一页 |
## useTableWithPagination
### 完整用法
```typescript
import useTableWithPagination from '@/composables/useTableWithPagination';
const {
data: renderData,
columns: projectColumn,
loading,
total,
current,
pageSize,
setColumns,
onPageChange,
onPageSizeChange,
refresh,
updateParams,
} = useTableWithPagination({
// 数据获取函数
fetchData: async (params) => {
const { code, data } = await getUserList({
dept_id: selectedKeys.value[0],
keyword: searchKeyWord.value,
page: params.page,
page_size: params.pageSize,
});
if (code === 200) {
const { page, user } = data;
return {
data: Array.isArray(user) ? user : [],
total: typeof page.total === 'number' ? page.total : 0,
};
}
return { data: [], total: 0 };
},
initialPage: 1,
initialPageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
autoFetch: false, // 不自动获取,等选中部门后再获取
});
```
### 配置选项
| 选项 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| fetchData | Function | - | 数据获取函数,必须返回 { data, total } |
| initialPage | number | 1 | 初始页码 |
| initialPageSize | number | 10 | 初始每页条数 |
| pageSizeOptions | number[] | [10, 20, 50, 100] | 每页条数选项 |
| extraParams | object | {} | 额外的查询参数 |
| autoFetch | boolean | true | 是否自动获取数据 |
| resetToFirstPage | boolean | true | 切换每页条数时是否重置到第一页 |
### 返回值
| 属性 | 类型 | 说明 |
|------|------|------|
| data | Ref<TableData[]> | 表格数据 |
| columns | Ref<TableColumnData[]> | 列配置 |
| loading | Ref<boolean> | 加载状态 |
| total | Ref<number> | 总条数 |
| current | Ref<number> | 当前页码 |
| pageSize | Ref<number> | 每页条数 |
| setColumns | Function | 设置列配置 |
| onPageChange | Function | 页码变化处理 |
| onPageSizeChange | Function | 每页条数变化处理 |
| refresh | Function | 刷新数据 |
| updateParams | Function | 更新查询参数 |
## 最佳实践
### 1. 使用 usePagination(推荐用于简单场景)
```typescript
// 在现有的 useDataTable 基础上添加分页处理
const { handlePageChange, handlePageSizeChange } = usePagination({
onPageChange: () => getUserListData(),
onPageSizeChange: () => getUserListData(),
});
const onPageChange = (page: number) => {
handlePageChange(page, setCurrent);
};
const onPageSizeChange = (pageSize: number) => {
handlePageSizeChange(pageSize, setPageSize, setCurrent);
};
```
### 2. 使用 useTableWithPagination(推荐用于新页面)
```typescript
// 完整的分页和数据获取集成
const {
data: renderData,
columns: projectColumn,
loading,
total,
current,
pageSize,
onPageChange,
onPageSizeChange,
refresh,
} = useTableWithPagination({
fetchData: async (params) => {
// 你的数据获取逻辑
return { data: [], total: 0 };
},
autoFetch: false,
});
// 搜索时刷新数据
const handleSearch = () => {
refresh();
};
// 删除后刷新数据
const handleDelete = async (id: number) => {
await deleteItem(id);
refresh();
};
```
### 3. 处理搜索和筛选
```typescript
// 使用 updateParams 更新查询参数
const handleSearch = () => {
updateParams({ keyword: searchKeyWord.value });
};
// 或者使用 refresh 直接刷新
const handleSearch = () => {
refresh();
};
```
## 优势
1. **代码复用**:避免在每个页面重复编写分页逻辑
2. **统一处理**:确保所有页面的分页行为一致
3. **类型安全**:完整的 TypeScript 类型支持
4. **易于维护**:分页逻辑集中管理,便于修改和扩展
5. **自动优化**:自动处理页码重置、加载状态等细节
## 注意事项
1. `fetchData` 函数必须返回 `{ data, total }` 格式
2. 使用 `useTableWithPagination` 时,确保 `fetchData` 函数能正确处理错误
3. 如果需要自定义分页行为,可以使用 `usePagination` 进行更精细的控制
4. `autoFetch: false` 适用于需要等待用户操作后再获取数据的场景
+4 -5
View File
@@ -1,5 +1,5 @@
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { getDepartmentList, getDepartmentDetail } from '@/api/setting'; import { getDepartmentList, getDepartmentDetail } from '@/api/department';
import { DepartmentItem } from '@/types/setting'; import { DepartmentItem } from '@/types/setting';
export default function useDepartmentList(autoFetch = true) { export default function useDepartmentList(autoFetch = true) {
@@ -40,8 +40,8 @@ export default function useDepartmentList(autoFetch = true) {
); );
departmentListData.value = fetchDepartmentList; departmentListData.value = fetchDepartmentList;
} }
} catch (error) { } catch {
console.error('获取部门列表失败:', error); // 获取部门列表失败,静默处理
} }
}; };
@@ -52,8 +52,7 @@ export default function useDepartmentList(autoFetch = true) {
return data; return data;
} }
return null; return null;
} catch (error) { } catch {
console.error('获取部门详情失败:', error);
return null; return null;
} }
}; };
+154
View File
@@ -42,3 +42,157 @@ export const VALIDATION_RULES = {
ID_CARD: 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]$/, /^[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; } 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, '元/年'],
]);
// 租客来源
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, '其他原因'],
]);
-7
View File
@@ -1,10 +1,6 @@
import localeMessageBox from '@/components/message-box/locale/en-US'; import localeMessageBox from '@/components/message-box/locale/en-US';
import localeLogin from '@/views/login/locale/en-US'; import localeLogin from '@/views/login/locale/en-US';
import localeWorkplace from '@/views/dashboard/workplace/locale/en-US';
import localeMonitor from '@/views/dashboard/monitor/locale/en-US';
import locale403 from '@/views/exception/403/locale/en-US'; import locale403 from '@/views/exception/403/locale/en-US';
import locale404 from '@/views/exception/404/locale/en-US'; import locale404 from '@/views/exception/404/locale/en-US';
import locale500 from '@/views/exception/500/locale/en-US'; import locale500 from '@/views/exception/500/locale/en-US';
@@ -30,9 +26,6 @@ export default {
...localeSettings, ...localeSettings,
...localeMessageBox, ...localeMessageBox,
...localeLogin, ...localeLogin,
...localeWorkplace,
...localeMonitor,
...locale403, ...locale403,
...locale404, ...locale404,
...locale500, ...locale500,
+8 -8
View File
@@ -1,10 +1,6 @@
import localeMessageBox from '@/components/message-box/locale/zh-CN'; import localeMessageBox from '@/components/message-box/locale/zh-CN';
import localeLogin from '@/views/login/locale/zh-CN'; import localeLogin from '@/views/login/locale/zh-CN';
import localeWorkplace from '@/views/dashboard/workplace/locale/zh-CN';
import localeMonitor from '@/views/dashboard/monitor/locale/zh-CN';
import locale403 from '@/views/exception/403/locale/zh-CN'; import locale403 from '@/views/exception/403/locale/zh-CN';
import locale404 from '@/views/exception/404/locale/zh-CN'; import locale404 from '@/views/exception/404/locale/zh-CN';
import locale500 from '@/views/exception/500/locale/zh-CN'; import locale500 from '@/views/exception/500/locale/zh-CN';
@@ -41,19 +37,22 @@ export default {
'content': '内容', 'content': '内容',
'table.filter': '筛选', 'table.filter': '筛选',
'table.export': '导出', 'table.export': '导出',
'table.import': '导入',
'watch-detail': '查看详情', 'watch-detail': '查看详情',
'menu.business.project': '项目管理', 'menu.business.project': '项目管理',
'menu.business.space': '空间管理', 'menu.business.space': '空间管理',
'menu.business.house-view': '房源剖视', 'menu.business.house-view': '房源剖视',
'menu.business.house-list': '房源列表', 'menu.business.house-list': '房源列表',
'menu.business.house-atlas': '房源图谱',
'menu.business.contract': '合同管理', 'menu.business.contract': '合同管理',
'menu.business.contract-list': '合同列表', 'menu.business.contract-list': '合同列表',
'menu.business.contract-file': '合同归档', 'menu.business.contract-file': '合同归档',
'menu.business.contract-template': '合同模板', 'menu.business.contract-template': '合同模板',
'menu.business.tenant': '租客管理', 'menu.business.tenant': '租客管理',
'menu.business.tenant-list': '租客列表', 'menu.business.tenant-list': '租客列表',
'menu.business.tenant.contacts': '租客联系人', 'menu.business.tenant-file': '租客归档',
'add-project': '新建项目', 'add-project': '新建项目',
'edit-project': '编辑项目',
'menu.setting.user': '用户设置', 'menu.setting.user': '用户设置',
'menu.setting.user-list': '用户列表', 'menu.setting.user-list': '用户列表',
'menu.setting.role': '角色权限', 'menu.setting.role': '角色权限',
@@ -123,13 +122,14 @@ export default {
'setting.contract': '合同设置', 'setting.contract': '合同设置',
'setting.finance': '财务设置', 'setting.finance': '财务设置',
'setting.data-dictionary': '数据字典', 'setting.data-dictionary': '数据字典',
'add-house': '新建房源',
'edit-house': '编辑房源',
'add-tenant': '新建租客',
'edit-tenant': '编辑租客',
...localeSettings, ...localeSettings,
...localeMessageBox, ...localeMessageBox,
...localeLogin, ...localeLogin,
...localeWorkplace,
...localeMonitor,
...locale403, ...locale403,
...locale404, ...locale404,
...locale500, ...locale500,
@@ -1,40 +0,0 @@
import { DEFAULT_LAYOUT } from '../base';
import { AppRouteRecordRaw } from '../types';
const RESULT: AppRouteRecordRaw = {
path: '/result',
name: 'result',
component: DEFAULT_LAYOUT,
redirect: '/result/success',
meta: {
locale: 'menu.result',
icon: 'icon-check-circle',
requiresAuth: true,
order: 5,
isFirst: true,
},
children: [
{
path: 'success',
name: 'Success',
component: () => import('@/views/result/success/index.vue'),
meta: {
locale: 'menu.result.success',
requiresAuth: true,
roles: ['admin'],
},
},
{
path: 'error',
name: 'Error',
component: () => import('@/views/result/error/index.vue'),
meta: {
locale: 'menu.result.error',
requiresAuth: true,
roles: ['admin'],
},
},
],
};
export default RESULT;
@@ -1,41 +0,0 @@
import { DEFAULT_LAYOUT } from '../base';
import { AppRouteRecordRaw } from '../types';
const VISUALIZATION: AppRouteRecordRaw = {
path: '/visualization',
name: 'visualization',
component: DEFAULT_LAYOUT,
redirect: '/visualization/data-analysis',
meta: {
locale: 'menu.visualization',
requiresAuth: true,
icon: 'icon-apps',
order: 1,
isFirst: true,
},
children: [
{
path: 'data-analysis',
name: 'DataAnalysis',
component: () => import('@/views/visualization/data-analysis/index.vue'),
meta: {
locale: 'menu.visualization.dataAnalysis',
requiresAuth: true,
roles: ['admin'],
},
},
{
path: 'multi-dimension-data-analysis',
name: 'MultiDimensionDataAnalysis',
component: () =>
import('@/views/visualization/multi-dimension-data-analysis/index.vue'),
meta: {
locale: 'menu.visualization.multiDimensionDataAnalysis',
requiresAuth: true,
roles: ['admin'],
},
},
],
};
export default VISUALIZATION;
+21 -10
View File
@@ -55,7 +55,7 @@ const BUSINESS: AppRouteRecordRaw = {
component: () => component: () =>
import( import(
/* webpackChunkName: "business-space" */ /* webpackChunkName: "business-space" */
'@/views/business/space/house-view.vue' '@/views/business/space/house-view/index.vue'
), ),
meta: { meta: {
locale: 'menu.business.house-view', locale: 'menu.business.house-view',
@@ -69,10 +69,7 @@ const BUSINESS: AppRouteRecordRaw = {
path: 'house-list', path: 'house-list',
name: 'business-space-house-list', name: 'business-space-house-list',
component: () => component: () =>
import( import('@/views/business/space/house-list/index.vue'),
/* webpackChunkName: "business-space" */
'@/views/business/space/house-list.vue'
),
meta: { meta: {
locale: 'menu.business.house-list', locale: 'menu.business.house-list',
requiresAuth: true, requiresAuth: true,
@@ -81,6 +78,20 @@ const BUSINESS: AppRouteRecordRaw = {
permissionKey: 'businessHouseList', permissionKey: 'businessHouseList',
}, },
}, },
// 房源图谱
{
path: 'house-atlas',
name: 'business-space-house-atlas',
component: () =>
import('@/views/business/space/house-atlas/index.vue'),
meta: {
locale: 'menu.business.house-atlas',
requiresAuth: true,
roles: ['*'],
icon: 'icon-brush',
permissionKey: 'businessHouseAtlas',
},
},
], ],
}, },
// 合同管理 // 合同管理
@@ -171,7 +182,7 @@ const BUSINESS: AppRouteRecordRaw = {
component: () => component: () =>
import( import(
/* webpackChunkName: "business-tenant" */ /* webpackChunkName: "business-tenant" */
'@/views/business/tenant/list.vue' '@/views/business/tenant/list/index.vue'
), ),
meta: { meta: {
locale: 'menu.business.tenant-list', locale: 'menu.business.tenant-list',
@@ -183,15 +194,15 @@ const BUSINESS: AppRouteRecordRaw = {
}, },
// 租客联系人 // 租客联系人
{ {
path: 'tenant-contacts', path: 'tenant-file',
name: 'business-tenant-contacts', name: 'business-tenant-file',
component: () => component: () =>
import( import(
/* webpackChunkName: "business-tenant" */ /* webpackChunkName: "business-tenant" */
'@/views/business/tenant/contacts.vue' '@/views/business/tenant/file/index.vue'
), ),
meta: { meta: {
locale: 'menu.business.tenant.contacts', locale: 'menu.business.tenant-file',
requiresAuth: true, requiresAuth: true,
roles: ['*'], roles: ['*'],
icon: 'icon-font-colors', icon: 'icon-font-colors',
-73
View File
@@ -1,73 +0,0 @@
import PageLayout from '@/layout/page-layout.vue';
import { DEFAULT_LAYOUT } from '../base';
import { AppRouteRecordRaw } from '../types';
const DASHBOARD: AppRouteRecordRaw = {
path: '/dashboard',
name: 'dashboard',
component: DEFAULT_LAYOUT,
redirect: '/dashboard/workplace',
meta: {
locale: 'menu.dashboard.workplace',
requiresAuth: true,
icon: 'icon-dashboard',
order: 1,
isFirst: true,
permissionKey: 'dashboard',
},
children: [
{
path: 'workplace',
name: 'Workplace',
component: PageLayout,
redirect: '/dashboard/workplace/step',
meta: {
locale: 'menu.dashboard.workplace',
requiresAuth: true,
roles: ['*'],
icon: 'icon-dashboard',
permissionKey: 'workplace',
// activeMenu: 'dashboard',
},
children: [
{
path: 'step',
name: 'Step',
component: () =>
import(
/* webpackChunkName: "dashboard-workplace" */
/* webpackPreload: true */
'@/views/dashboard/workplace/index.vue'
),
meta: {
locale: 'menu.form.step',
requiresAuth: true,
roles: ['*'],
icon: 'icon-exclamation-circle',
permissionKey: 'step',
},
},
],
},
{
path: 'monitor',
name: 'Monitor',
component: () =>
import(
/* webpackChunkName: "dashboard-monitor" */
'@/views/dashboard/monitor/index.vue'
),
meta: {
locale: 'menu.dashboard.monitor',
requiresAuth: true,
roles: ['*'],
icon: 'icon-settings',
permissionKey: 'monitor',
// activeMenu: 'dashboard',
},
},
],
};
export default DASHBOARD;
+1 -28
View File
@@ -1,9 +1,6 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { Notification } from '@arco-design/web-vue';
import type { NotificationReturn } from '@arco-design/web-vue/es/notification/interface';
import type { RouteRecordRaw } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router';
import defaultSettings from '@/config/settings.json'; import defaultSettings from '@/config/settings.json';
import { getMenuList } from '@/api/user';
import { AppState } from './types'; import { AppState } from './types';
const useAppStore = defineStore('app', { const useAppStore = defineStore('app', {
@@ -42,7 +39,7 @@ const useAppStore = defineStore('app', {
actions: { actions: {
// Update app settings // Update app settings
updateSettings(partial: Partial<AppState>) { updateSettings(partial: Partial<AppState>) {
// @ts-ignore-next-line // @ts-expect-error pinia $patch typing with partial
this.$patch(partial); this.$patch(partial);
}, },
@@ -62,30 +59,6 @@ const useAppStore = defineStore('app', {
toggleMenu(value: boolean) { toggleMenu(value: boolean) {
this.hideMenu = value; this.hideMenu = value;
}, },
async fetchServerMenuConfig() {
let notifyInstance: NotificationReturn | null = null;
try {
notifyInstance = Notification.info({
id: 'menuNotice', // Keep the instance id the same
content: 'loading',
closable: true,
});
const { data } = await getMenuList();
this.serverMenu = data;
notifyInstance = Notification.success({
id: 'menuNotice',
content: 'success',
closable: true,
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
notifyInstance = Notification.error({
id: 'menuNotice',
content: 'error',
closable: true,
});
}
},
clearServerMenu() { clearServerMenu() {
this.serverMenu = []; this.serverMenu = [];
}, },
+12 -1
View File
@@ -30,6 +30,8 @@ const useUserStore = defineStore('user', {
certification: undefined, certification: undefined,
role: '', role: '',
permission: [], permission: [],
projects: undefined,
currentProjectId: undefined,
}), }),
// 配置持久化 // 配置持久化
@@ -37,7 +39,16 @@ const useUserStore = defineStore('user', {
key: 'user-store', key: 'user-store',
storage: localStorage, storage: localStorage,
// 只持久化关键的用户信息,排除敏感信息 // 只持久化关键的用户信息,排除敏感信息
pick: ['name', 'avatar', 'role', 'permission', 'accountId', 'email'], pick: [
'name',
'avatar',
'role',
'permission',
'accountId',
'email',
'projects',
'currentProjectId',
],
}, },
getters: { getters: {
+11
View File
@@ -1,6 +1,15 @@
import { UserPermission } from '@/types/api'; import { UserPermission } from '@/types/api';
export type RoleType = '' | '*' | 'admin' | 'user'; export type RoleType = '' | '*' | 'admin' | 'user';
export interface UserProject {
id: number;
name: string;
image?: string;
imageUrl?: string;
[key: string]: any;
}
export interface UserState { export interface UserState {
name?: string; name?: string;
avatar?: string; avatar?: string;
@@ -19,4 +28,6 @@ export interface UserState {
certification?: number; certification?: number;
role: RoleType; role: RoleType;
permission: UserPermission[]; permission: UserPermission[];
projects?: UserProject[];
currentProjectId?: number;
} }
+29
View File
@@ -51,6 +51,7 @@ export interface ProjectItem extends BaseEntity {
roomArea: number; roomArea: number;
rentArea: number; rentArea: number;
useArea: number; useArea: number;
keepArea: number;
rentRate: number; rentRate: number;
RoomAreaStr: string; // 列表显示的计租面积 RoomAreaStr: string; // 列表显示的计租面积
RentAreaStr: string; // 列表显示的招商面积 RentAreaStr: string; // 列表显示的招商面积
@@ -114,3 +115,31 @@ export interface ProjectInfo extends BaseEntity {
export type ProjectDetailRes = HttpResponse<{ export type ProjectDetailRes = HttpResponse<{
project: ProjectItem; project: ProjectItem;
}>; }>;
// 楼栋标签响应类型
export interface BuildingTag {
buildingId: number;
projectName: string;
buildingName: string;
}
export type TopBuildingTagsRes = HttpResponse<{
tags: BuildingTag[];
}>;
// 创建房源参数
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[]; // 标签数组
}
+1 -7
View File
@@ -15,12 +15,6 @@ function preloadRouteComponent(routeName: string): void {
'@/views/business/project/index.vue' '@/views/business/project/index.vue'
); );
break; break;
case 'dashboard-workplace':
import(
/* webpackChunkName: "dashboard-workplace" */
'@/views/dashboard/workplace/index.vue'
);
break;
default: default:
// 未知路由,跳过预加载 // 未知路由,跳过预加载
break; break;
@@ -63,7 +57,7 @@ export function smartPreload(
userRole: string, userRole: string,
currentRoute?: RouteLocationNormalized currentRoute?: RouteLocationNormalized
): void { ): void {
const commonRoutes = ['dashboard-workplace']; const commonRoutes: string[] = [];
// 根据用户角色预加载不同的路由 // 根据用户角色预加载不同的路由
const roleBasedRoutes: Record<string, string[]> = { const roleBasedRoutes: Record<string, string[]> = {
@@ -2,7 +2,7 @@
<a-drawer <a-drawer
:width="670" :width="670"
:visible="visible" :visible="visible"
:title="isEdit ? '编辑条款设置' : '新增条款设置'" :title="isEdit ? $t('edit-project') : $t('add-project')"
unmount-on-close unmount-on-close
@ok="handleOk" @ok="handleOk"
@cancel="handleCancel" @cancel="handleCancel"
@@ -185,11 +185,11 @@
getCityListByProvinceId, getCityListByProvinceId,
getDistrictListByCityId, getDistrictListByCityId,
} from '@/api/common'; } from '@/api/common';
import { createProject, updateProject } from '@/api/business'; import { createProject, updateProject } from '@/api/project';
import { CreateProjectParam } from '@/types/business'; import { CreateProjectParam } from '@/types/business';
import { projectTypeInfo } from '@/types/setting'; import { projectTypeInfo } from '@/types/setting';
import ImageUpload from '@/components/common/ImageUpload.vue'; import ImageUpload from '@/components/common/ImageUpload.vue';
import { getAllProjectTypeList } from '@/api/setting'; import { getAllProjectTypeList } from '@/api/dictionary';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
import { generateRandomId } from '@/utils'; import { generateRandomId } from '@/utils';
@@ -374,7 +374,7 @@
emit('update:visible', false); emit('update:visible', false);
}; };
const handleDragEnd = () => { const handleDragEnd = () => {
console.log('handleDragEnd'); // 拖拽结束处理
}; };
onMounted(() => { onMounted(() => {
getProvinceListData(); getProvinceListData();
@@ -7,20 +7,70 @@
unmount-on-close unmount-on-close
@cancel="handleCancel" @cancel="handleCancel"
> >
项目详情 <div class="flex items-center justify-between">
<div>
<p>{{ detailInfo?.name }}</p>
<p class="mt-[10px]">
<a-tag :color="getTypeColor(detailInfo?.type)">{{
detailInfo?.typeName
}}</a-tag>
</p>
</div>
<div class="flex items-center gap-[10px]">
<p>{{ detailInfo?.updatedAt }}</p>
<div>
<a-button @click="handleEdit">
<template #icon>
<icon-edit />
</template>
{{ $t('edit') }}
</a-button>
</div>
<!-- <a-button type="primary" @click="handleDelete">删除</a-button> -->
</div>
</div>
<div class="mt-[10px] grid grid-cols-3 gap-[10px]">
<a-card title="面积信息">
<div class="ring-chart-container">
<Chart :options="ringChartOption" height="200px" />
</div>
</a-card>
<a-card title="房源统计">
<div class="grid grid-cols-3 gap-[10px] text-center">
<div>
<p class="text-[18px] font-bold">{{ detailInfo?.roomNum }}</p>
<p>房源总数</p>
</div>
<div>
<p class="text-[18px] font-bold">{{ detailInfo?.roomsRentNum }}</p>
<p>已出租</p>
</div>
<div>
<p class="text-[18px] font-bold">{{
detailInfo?.roomsUnRentNum
}}</p>
<p>空置</p>
</div>
</div>
</a-card>
<a-card title="租客信息"></a-card>
</div>
</a-drawer> </a-drawer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch, onMounted } from 'vue'; import { watch, onMounted, computed } from 'vue';
// import { Message } from '@arco-design/web-vue'; // import { Message } from '@arco-design/web-vue';
// import { projectTypeList } from '@/utils/constant'; // import { projectTypeList } from '@/utils/constant';
import { ProjectItem } from '@/types/business'; import { ProjectItem } from '@/types/business';
import Chart from '@/components/chart/index.vue';
import type { EChartsOption } from 'echarts';
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean;
projectId: number | null; projectId: number | null;
detailInfo: ProjectItem | null; detailInfo: ProjectItem | null;
projectTypeList: any[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:visible', val: boolean): void; (e: 'update:visible', val: boolean): void;
@@ -36,6 +86,158 @@
const handleCancel = () => { const handleCancel = () => {
emit('update:visible', false); emit('update:visible', false);
}; };
const handleEdit = () => {
emit('update:visible', false);
};
const getTypeColor = (type: number | undefined) => {
if (!type) return '';
const color = props.projectTypeList.find((item) => item.id === type)?.color;
return color || '';
};
// 格式化面积数值,保留2位小数
const formatArea = (value: number): number => {
return Math.round(value * 100) / 100;
};
// 格式化面积显示,添加千分位分隔符
const formatAreaDisplay = (value: number): string => {
if (!value && value !== 0) return '0';
return formatArea(value).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
};
// 计算环形图数据
const ringChartData = computed(() => {
if (!props.detailInfo) {
return [];
}
const { roomArea = 0, rentArea = 0, keepArea = 0 } = props.detailInfo;
// 确保数值有效
const totalArea = Number(roomArea) || 0;
const rentedArea = Number(rentArea) || 0;
const reservedArea = Number(keepArea) || 0;
// 可租面积 = 计租面积 - 在租面积 - 保留面积
const availableArea = Math.max(0, totalArea - rentedArea - reservedArea);
// 构建图表数据,如果总面积大于0,显示所有三个部分(即使某些为0)
const data: Array<{ name: string; value: number }> = [];
// 如果总面积大于0,显示所有分类
if (totalArea > 0) {
// 在租面积
data.push({
name: '在租面积',
value: formatArea(rentedArea),
});
// 保留面积
data.push({
name: '保留面积',
value: formatArea(reservedArea),
});
// 可租面积
data.push({
name: '可租面积',
value: formatArea(availableArea),
});
} else {
// 如果所有数据都为0,显示占位数据
data.push({
name: '计租面积',
value: 0,
});
}
// 调试信息(开发时使用)
console.log('环形图数据:', {
roomArea: totalArea,
rentArea: rentedArea,
keepArea: reservedArea,
availableArea,
chartData: data,
});
return data;
});
// 环形图配置
const ringChartOption = computed<EChartsOption>(() => {
const data = ringChartData.value;
if (data.length === 0) {
return {
title: {
text: '暂无数据',
left: 'center',
top: 'middle',
textStyle: {
color: '#999',
fontSize: 14,
},
},
};
}
return {
tooltip: {
trigger: 'item',
formatter: (params: any) => {
const { name, value, percent } = params;
return `${name}<br/>${formatAreaDisplay(value)}㎡ (${percent}%)`;
},
},
legend: {
orient: 'vertical',
left: 'left',
top: 'middle',
itemWidth: 12,
itemHeight: 12,
textStyle: {
fontSize: 12,
},
},
series: [
{
name: '面积信息',
type: 'pie',
radius: ['40%', '70%'], // 环形图:内半径40%,外半径70%
center: ['60%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: true,
formatter: '{b}\n{d}%',
fontSize: 12,
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold',
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
data: data,
},
],
};
});
onMounted(() => {}); onMounted(() => {});
</script> </script>
@@ -43,4 +245,33 @@
:deep(label.arco-form-item-label) { :deep(label.arco-form-item-label) {
width: 100% !important; width: 100% !important;
} }
.ring-chart-container {
position: relative;
min-height: 300px;
.area-summary {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--color-border-2);
.summary-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.summary-label {
color: var(--color-text-2);
font-size: 14px;
}
.summary-value {
color: var(--color-text-1);
font-size: 16px;
font-weight: 500;
}
}
}
}
</style> </style>
@@ -41,8 +41,8 @@
<div class="slider"> <div class="slider">
<a-slider <a-slider
v-model="rentRate" v-model="rentRate"
:min="0" :min="rentRateMin"
:max="100" :max="rentRateMax"
range range
:marks="marks" :marks="marks"
></a-slider> ></a-slider>
@@ -51,12 +51,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch, onMounted } from 'vue'; import { ref, watch, onMounted, computed } from 'vue';
import { getProjectCityList } from '@/api/business'; import { getAllProjectTypeList } from '@/api/dictionary';
import { CommonCityItem } from '@/types/comm';
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean;
projectList?: any[]; // 项目列表数据,用于提取筛选选项
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:visible', val: boolean): void; (e: 'update:visible', val: boolean): void;
@@ -66,30 +66,120 @@
typeIds: number[] | null; typeIds: number[] | null;
cityIds: number[] | null; cityIds: number[] | null;
rentRate: number | number[]; rentRate: number | number[];
filteredList: any[]; // 筛选后的列表数据
} }
): void; ): void;
}>(); }>();
// 保存的筛选状态
const savedFilterState = ref<{
projectTypeChecked: number[];
projectCityChecked: number[];
rentRate: number | number[];
} | null>(null);
const projectTypeChecked = ref<number[]>([0]); const projectTypeChecked = ref<number[]>([0]);
const projectCityChecked = ref<number[]>([0]); const projectCityChecked = ref<number[]>([0]);
const rentRate = ref<number | number[]>(0); const rentRate = ref<number | number[]>([0, 100]);
const projectTypeList = [
{ text: '全部', value: 0 }, // 所有项目类型列表(从API获取)
{ text: '写字楼', value: 1 }, const allProjectTypeList = ref<any[]>([]);
{ text: '园区', value: 2 },
{ text: '商业综合体', value: 3 }, // 获取项目类型列表
]; const getAllProjectTypeListData = async () => {
const marks = { try {
0: '0%', const { code, data } = await getAllProjectTypeList();
100: '100%', if (code === 200 && data) {
}; allProjectTypeList.value = data?.projectTypesList || [];
const projectCityList = ref<CommonCityItem[]>([]); }
const getProjectCityListData = async () => { } catch (error) {
const { code, data } = await getProjectCityList(); // console.error(error);
if (code === 200 && data) {
projectCityList.value = Array.isArray(data.cityList) ? data.cityList : [];
projectCityList.value.unshift({ id: 0, code: '0', name: '全部' });
} }
}; };
// 项目类型列表(从项目列表中提取实际存在的类型)
const projectTypeList = computed(() => {
const result = [{ text: '全部', value: 0 }];
// 如果项目列表为空,返回所有项目类型
if (!props.projectList || props.projectList.length === 0) {
if (allProjectTypeList.value.length > 0) {
allProjectTypeList.value.forEach((type) => {
result.push({
text: type.name,
value: type.id,
});
});
}
return result;
}
// 从项目列表中提取实际存在的类型ID
const existingTypeIds = new Set<number>();
props.projectList.forEach((project) => {
if (project.type) {
existingTypeIds.add(project.type);
}
});
// 从所有项目类型中筛选出实际存在的类型
allProjectTypeList.value.forEach((type) => {
if (existingTypeIds.has(type.id)) {
result.push({
text: type.name,
value: type.id,
});
}
});
return result;
});
// 城市列表(从项目列表中提取唯一值)
const projectCityList = computed(() => {
if (!props.projectList || props.projectList.length === 0) {
return [{ id: 0, code: '0', name: '全部' }];
}
const cityMap = new Map();
// 添加"全部"选项
cityMap.set(0, { id: 0, code: '0', name: '全部' });
// 从项目列表中提取城市
props.projectList.forEach((project) => {
if (project.cityId && project.city) {
if (!cityMap.has(project.cityId)) {
cityMap.set(project.cityId, {
id: project.cityId,
code: String(project.cityId),
name: project.city,
});
}
}
});
return Array.from(cityMap.values());
});
// 出租率范围
const rentRateMin = computed(() => {
if (!props.projectList || props.projectList.length === 0) return 0;
const rates = props.projectList
.map((project) => Number(project.rentRate))
.filter((rate) => !isNaN(rate) && rate >= 0);
return rates.length > 0 ? Math.min(...rates) : 0;
});
const rentRateMax = computed(() => {
if (!props.projectList || props.projectList.length === 0) return 100;
const rates = props.projectList
.map((project) => Number(project.rentRate))
.filter((rate) => !isNaN(rate) && rate >= 0);
return rates.length > 0 ? Math.max(...rates) : 100;
});
const marks = computed(() => {
return {
[rentRateMin.value]: `${rentRateMin.value}%`,
[rentRateMax.value]: `${rentRateMax.value}%`,
};
});
const handleCheckTypeIds = (id: number) => { const handleCheckTypeIds = (id: number) => {
if (typeof id !== 'number') return; if (typeof id !== 'number') return;
if (id === 0) { if (id === 0) {
@@ -129,26 +219,114 @@
const handleCancel = () => { const handleCancel = () => {
emit('update:visible', false); emit('update:visible', false);
}; };
// 筛选项目列表数据
const filterProjectList = () => {
if (!props.projectList || props.projectList.length === 0) return [];
let filtered = [...props.projectList];
// 筛选项目类型
const normalizedTypeIds = normalizeIds(projectTypeChecked.value);
if (normalizedTypeIds && normalizedTypeIds.length > 0) {
filtered = filtered.filter((project) =>
normalizedTypeIds.includes(project.type)
);
}
// 筛选城市
const normalizedCityIds = normalizeIds(projectCityChecked.value);
if (normalizedCityIds && normalizedCityIds.length > 0) {
filtered = filtered.filter((project) =>
normalizedCityIds.includes(project.cityId)
);
}
// 筛选出租率
const [minRate, maxRate] = Array.isArray(rentRate.value)
? rentRate.value
: [0, 100];
filtered = filtered.filter((project) => {
const rate = Number(project.rentRate);
return !isNaN(rate) && rate >= minRate && rate <= maxRate;
});
return filtered;
};
const normalizeIds = (ids: number[]) => const normalizeIds = (ids: number[]) =>
ids.length === 1 && ids[0] === 0 ? null : ids; ids.length === 1 && ids[0] === 0 ? null : ids;
const handleOk = () => { const handleOk = () => {
// 保存当前筛选状态
savedFilterState.value = {
projectTypeChecked: [...projectTypeChecked.value],
projectCityChecked: [...projectCityChecked.value],
rentRate: Array.isArray(rentRate.value)
? [...rentRate.value]
: rentRate.value,
};
const filteredList = filterProjectList();
emit('confirm', { emit('confirm', {
typeIds: normalizeIds(projectTypeChecked.value), typeIds: normalizeIds(projectTypeChecked.value),
cityIds: normalizeIds(projectCityChecked.value), cityIds: normalizeIds(projectCityChecked.value),
rentRate: rentRate.value, rentRate: rentRate.value,
filteredList, // 筛选后的列表数据
}); });
handleCancel(); handleCancel();
}; };
// 初始化筛选表单
const initFilterForm = () => {
// 如果有保存的状态,恢复它
if (savedFilterState.value) {
projectTypeChecked.value = [...savedFilterState.value.projectTypeChecked];
projectCityChecked.value = [...savedFilterState.value.projectCityChecked];
rentRate.value = Array.isArray(savedFilterState.value.rentRate)
? [...savedFilterState.value.rentRate]
: savedFilterState.value.rentRate;
} else {
// 否则使用默认值
projectTypeChecked.value = [0];
projectCityChecked.value = [0];
const min = rentRateMin.value || 0;
const max = rentRateMax.value || 100;
rentRate.value = [min, max];
}
};
// 监听项目列表变化,更新出租率范围
watch(
() => props.projectList,
() => {
if (props.visible) {
const min = rentRateMin.value || 0;
const max = rentRateMax.value || 100;
// 如果当前出租率范围不在新的范围内,则更新
const currentRate = Array.isArray(rentRate.value)
? rentRate.value
: [0, 100];
if (currentRate[0] < min || currentRate[1] > max) {
rentRate.value = [min, max];
}
}
}
);
watch( watch(
() => props.visible, () => props.visible,
async (val) => { (val) => {
if (val && projectCityList.value.length === 0) { if (val) {
getProjectCityListData(); initFilterForm();
if (allProjectTypeList.value.length === 0) {
getAllProjectTypeListData();
}
} }
}, },
{ immediate: true } { immediate: true }
); );
onMounted(() => {});
onMounted(() => {
getAllProjectTypeListData();
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
+78 -21
View File
@@ -21,7 +21,7 @@
</a-button> </a-button>
<a-button> <a-button>
<template #icon> <template #icon>
<icon-download /> <icon-export />
</template> </template>
{{ t('table.export') }} {{ t('table.export') }}
</a-button> </a-button>
@@ -47,7 +47,7 @@
@page-size-change="onPageSizeChange" @page-size-change="onPageSizeChange"
> >
<template #type="{ record }"> <template #type="{ record }">
<a-tag checkable :color="record.color"> <a-tag checkable :color="getTypeColor(record.type)">
{{ record.typeName }} {{ record.typeName }}
</a-tag> </a-tag>
</template> </template>
@@ -71,9 +71,20 @@
<a-button @click="handleWatchHouse(record.id)"> <a-button @click="handleWatchHouse(record.id)">
查看房源 查看房源
</a-button> </a-button>
<a-button status="danger" @click="handleDel(record.id)"> <a-popconfirm
删除项目 :ok-text="$t('cancel')"
</a-button> :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> </a-space>
</template> </template>
</a-popover> </a-popover>
@@ -91,9 +102,11 @@
v-model:visible="showProjectInfoDrawer" v-model:visible="showProjectInfoDrawer"
:project-id="currentProjectId" :project-id="currentProjectId"
:detail-info="currentProjectInfo" :detail-info="currentProjectInfo"
:project-type-list="projectTypeList"
/> />
<projectListFilterDrawer <projectListFilterDrawer
v-model:visible="showListFilterDrawer" v-model:visible="showListFilterDrawer"
:project-list="projectList"
@confirm="handleFilterConfirm" @confirm="handleFilterConfirm"
/> />
</div> </div>
@@ -106,12 +119,13 @@
import type { TableColumnData } from '@arco-design/web-vue'; import type { TableColumnData } from '@arco-design/web-vue';
import useDataTable from '@/composables/useDataTable'; import useDataTable from '@/composables/useDataTable';
import { ProjectItem } from '@/types/business'; import { ProjectItem } from '@/types/business';
import { getProjectList, getProjectDetail } from '@/api/business'; import { getProjectList, getProjectDetail, delProject } from '@/api/project';
import { getAllProjectTypeList } from '@/api/dictionary';
import { ProjectPaginationParams } from '@/types/comm'; import { ProjectPaginationParams } from '@/types/comm';
import projectFormDrawer from './drawer/project-form-drawer.vue'; import projectFormDrawer from './drawer/project-form-drawer.vue';
import projectInfoDrawer from './drawer/project-info-drawer.vue'; import projectInfoDrawer from './drawer/project-info-drawer.vue';
import projectListFilterDrawer from './drawer/project-list-filter-drawer.vue'; import projectListFilterDrawer from './drawer/project-list-filter-drawer.vue';
import { Message } from '@arco-design/web-vue';
const { t } = useI18n(); const { t } = useI18n();
const { loading } = useLoading(false); const { loading } = useLoading(false);
const showDrawer = ref<boolean>(false); const showDrawer = ref<boolean>(false);
@@ -155,7 +169,13 @@
}, },
{ title: '房源数量', dataIndex: 'roomNum', key: 'roomNum' }, { title: '房源数量', dataIndex: 'roomNum', key: 'roomNum' },
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt' }, { title: '创建时间', dataIndex: 'createdAt', key: 'createdAt' },
{ title: '操作', key: 'operate', slotName: 'operate', width: 200 }, {
title: '操作',
key: 'operate',
align: 'center',
slotName: 'operate',
width: 300,
},
]; ];
const queryParams = ref<ProjectPaginationParams>({ const queryParams = ref<ProjectPaginationParams>({
page: current.value, page: current.value,
@@ -184,7 +204,11 @@
!edit !edit
) )
return; return;
edit ? (isLoadingEdit.value = true) : (isLoadingDetail.value = true); if (edit) {
isLoadingEdit.value = true;
} else {
isLoadingDetail.value = true;
}
const { code, data } = await getProjectDetail(id); const { code, data } = await getProjectDetail(id);
if (code === 200 && data) { if (code === 200 && data) {
currentProjectInfo.value = data.project; currentProjectInfo.value = data.project;
@@ -194,20 +218,41 @@
}; };
const handleWatchHouse = async (id: number) => { const handleWatchHouse = async (id: number) => {
try { try {
console.log(id, 'is id'); // TODO: 实现查看房源功能
} catch (error) { } catch (error) {
// 处理错误 // 处理错误
} }
}; };
const handleFilterConfirm = ({ typeIds, cityIds, rentRate }) => { const handleFilterConfirm = (payload: {
queryParams.value.type = typeIds; typeIds: number[] | null;
queryParams.value.province_ids = cityIds; cityIds: number[] | null;
queryParams.value.rent_rate_min = rentRate[0]; rentRate: number | number[];
queryParams.value.rent_rate_max = rentRate[1]; filteredList: any[]; // 筛选后的列表数据
getProjectListData(); }) => {
// 更新查询参数(用于后端筛选)
queryParams.value.type = payload.typeIds;
queryParams.value.province_ids = payload.cityIds;
const rentRateArray = Array.isArray(payload.rentRate)
? payload.rentRate
: [0, 100];
queryParams.value.rent_rate_min = rentRateArray[0];
queryParams.value.rent_rate_max = rentRateArray[1];
// 使用筛选后的列表数据更新表格
if (payload.filteredList) {
setData(payload.filteredList);
setTotal(payload.filteredList.length);
} else {
// 如果没有筛选数据,重新加载列表数据
getProjectListData();
}
}; };
const handleDel = (id: number) => { const handleDel = async (id: number) => {
console.log(id, 'iiii'); const { code, message } = await delProject(id);
if (code === 200) {
Message.success(message);
getProjectListData();
}
}; };
// 分页事件处理 // 分页事件处理
const onPageChange = (page: number) => { const onPageChange = (page: number) => {
@@ -240,8 +285,20 @@
const handleFilter = () => { const handleFilter = () => {
showListFilterDrawer.value = true; showListFilterDrawer.value = true;
}; };
onMounted(() => { const projectTypeList = ref<any[]>([]);
setColumns(columns); const getAllProjectTypeListData = async () => {
getProjectListData(); const { code, data } = await getAllProjectTypeList();
if (code === 200 && data) {
projectTypeList.value = data?.projectTypesList;
}
};
const getTypeColor = (type: number) => {
const color = projectTypeList.value.find((item) => item.id === type)?.color;
return color || '';
};
onMounted(async () => {
await setColumns(columns);
await getAllProjectTypeListData();
await getProjectListData();
}); });
</script> </script>
@@ -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/dictionary';
import { createRoom, updateRoom } from '@/api/room';
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>
@@ -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">
<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>
@@ -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/dictionary';
// 特殊值用于表示"所有"选项(使用负数避免与真实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>
@@ -0,0 +1,272 @@
<template>
<div
class="cross-year-timeline-container"
:style="{
position: 'absolute',
left: '0',
width: `${yearRange.length * 200}px`,
height: '40px',
zIndex: 2,
}"
>
<!-- 年份分隔线 -->
<template v-for="(year, index) in yearRange" :key="`divider-${year}`">
<div
v-if="index < yearRange.length - 1"
class="year-divider"
:style="{
position: 'absolute',
left: `${(index + 1) * 200}px`,
top: '0',
bottom: '0',
width: '1px',
backgroundColor: '#e5e6eb',
zIndex: 1,
}"
></div>
</template>
<!-- 连续时间线(跨列显示) -->
<div
class="timeline-bar"
:style="{
position: 'absolute',
left: '0',
right: '0',
top: '0',
bottom: '0',
}"
>
<!-- 空置状态 -->
<div
v-for="(segment, index) in allSegments"
:key="`segment-${index}`"
class="timeline-segment"
:class="segment.type"
:style="{
position: 'absolute',
left: `${segment.startPercentage}%`,
width: `${segment.percentage}%`,
height: '100%',
backgroundColor:
segment.type === 'vacant'
? '#f2f3f5'
: segment.type === 'lease'
? '#00b42a'
: 'transparent',
backgroundImage:
segment.type === 'unarranged'
? 'repeating-linear-gradient(45deg, #f2f3f5, #f2f3f5 4px, #d9d9d9 4px, #d9d9d9 8px)'
: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 2,
}"
>
<span
v-if="segment.label && segment.percentage > 5"
class="segment-label"
:style="{
fontSize: '12px',
color: segment.type === 'lease' ? '#fff' : '#86909c',
fontWeight: segment.type === 'lease' ? '500' : 'normal',
padding: '0 4px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}"
>
{{ segment.label }}
</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
interface YearData {
year: string;
allDays: number;
allData: Array<{
companyName: string;
days: number;
type: number; // 1: 在租, 其他可能是空置
}>;
}
interface Props {
yearsData?: YearData[];
yearRange: number[];
}
const props = withDefaults(defineProps<Props>(), {
yearsData: () => [],
yearRange: () => [],
});
// 计算总天数(5年)
const totalDays = computed(() => {
// 如果数据为空,使用默认的5年天数
if (!props.yearsData || props.yearsData.length === 0) {
return 365 * props.yearRange.length;
}
// 确保所有年份都有数据,缺失的年份使用365天
let total = 0;
props.yearRange.forEach((year) => {
const yearData = props.yearsData?.find((d) => d.year === String(year));
total += yearData?.allDays || 365;
});
return total;
});
// 合并所有年份的数据,创建连续的时间段
interface TimelineSegment {
type: 'vacant' | 'lease' | 'unarranged';
percentage: number;
startPercentage: number; // 在整个时间线中的起始位置百分比
days: number;
label?: string;
startDay: number; // 在整个5年时间线中的起始天数
endDay: number; // 在整个5年时间线中的结束天数
}
const allSegments = computed(() => {
if (!props.yearsData || props.yearsData.length === 0) {
// 如果没有数据,全部显示为未安排
return [
{
type: 'unarranged' as const,
percentage: 100,
startPercentage: 0,
days: totalDays.value,
startDay: 0,
endDay: totalDays.value,
},
];
}
const segments: TimelineSegment[] = [];
let globalOffset = 0; // 在整个5年时间线中的累计天数
// 按年份顺序处理数据,确保年份顺序正确
const sortedYearsData = [...props.yearsData].sort((a, b) => {
return Number(a.year) - Number(b.year);
});
// 创建年份数据映射
const yearDataMap = new Map<string, YearData>();
sortedYearsData.forEach((data) => {
yearDataMap.set(data.year, data);
});
// 按年份范围顺序处理
props.yearRange.forEach((year) => {
const yearStr = String(year);
const yearData = yearDataMap.get(yearStr);
const yearDays = yearData?.allDays || 365;
const yearStartDay = globalOffset;
const yearEndDay = globalOffset + yearDays;
if (!yearData || !yearData.allData || yearData.allData.length === 0) {
// 如果该年份没有数据,全部显示为未安排
segments.push({
type: 'unarranged',
percentage: (yearDays / totalDays.value) * 100,
startPercentage: (yearStartDay / totalDays.value) * 100,
days: yearDays,
startDay: yearStartDay,
endDay: yearEndDay,
});
globalOffset = yearEndDay;
return;
}
// 收集该年份的所有时间段,按顺序排列
const yearSegments: Array<{
type: 'vacant' | 'lease';
days: number;
label: string;
}> = [];
yearData.allData.forEach((item) => {
if (item.type === 1 && item.days > 0) {
// 在租
yearSegments.push({
type: 'lease',
days: item.days,
label: item.companyName || '',
});
} else if (item.type !== 1 && item.days > 0) {
// 空置
yearSegments.push({
type: 'vacant',
days: item.days,
label: `空置${item.days}天`,
});
}
});
// 计算已安排的天数
const arrangedDays = yearSegments.reduce((sum, seg) => sum + seg.days, 0);
const unarrangedDays = yearDays - arrangedDays;
// 添加已安排的时间段(连续显示)
let offset = 0;
yearSegments.forEach((seg) => {
const startDay = yearStartDay + offset;
const endDay = startDay + seg.days;
segments.push({
type: seg.type,
percentage: (seg.days / totalDays.value) * 100,
startPercentage: (startDay / totalDays.value) * 100,
days: seg.days,
label: seg.label,
startDay,
endDay,
});
offset += seg.days;
});
// 添加未安排的时间段
if (unarrangedDays > 0) {
const startDay = yearStartDay + offset;
segments.push({
type: 'unarranged',
percentage: (unarrangedDays / totalDays.value) * 100,
startPercentage: (startDay / totalDays.value) * 100,
days: unarrangedDays,
startDay,
endDay: yearEndDay,
});
}
globalOffset = yearEndDay;
});
return segments;
});
</script>
<style scoped>
.cross-year-timeline-container {
position: relative;
overflow: visible;
}
.timeline-bar {
border-radius: 2px;
overflow: visible;
}
.timeline-segment {
transition: all 0.3s;
min-width: 0;
}
.segment-label {
user-select: none;
}
</style>
@@ -0,0 +1,216 @@
<template>
<div
class="year-timeline-container"
:style="{ position: 'relative', width: '100%', height: '40px' }"
>
<div
class="timeline-bar"
:style="{
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
}"
>
<!-- 空置状态 -->
<div
v-for="(vacant, index) in vacantSegments"
:key="`vacant-${index}`"
class="timeline-segment vacant-segment"
:style="{
width: `${vacant.percentage}%`,
height: '100%',
backgroundColor: '#f2f3f5',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
marginRight: '2px',
}"
>
<span
v-if="vacant.days > 0"
class="segment-label"
:style="{ fontSize: '12px', color: '#86909c' }"
>
空置{{ vacant.days }}天
</span>
</div>
<!-- 在租状态 -->
<div
v-for="(lease, index) in leaseSegments"
:key="`lease-${index}`"
class="timeline-segment lease-segment"
:style="{
width: `${lease.percentage}%`,
height: '100%',
backgroundColor: '#00b42a',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
marginRight: '2px',
}"
>
<span
v-if="lease.companyName"
class="segment-label"
:style="{
fontSize: '12px',
color: '#fff',
fontWeight: '500',
padding: '0 4px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}"
>
{{ lease.companyName }}
</span>
</div>
<!-- 未安排状态(斜线) -->
<div
v-if="unarrangedPercentage > 0"
class="timeline-segment unarranged-segment"
:style="{
width: `${unarrangedPercentage}%`,
height: '100%',
backgroundImage:
'repeating-linear-gradient(45deg, #f2f3f5, #f2f3f5 4px, #d9d9d9 4px, #d9d9d9 8px)',
position: 'relative',
marginRight: '2px',
}"
></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
interface YearData {
year: string;
allDays: number;
allData: Array<{
companyName: string;
days: number;
type: number; // 1: 在租, 其他可能是空置
}>;
}
interface Props {
yearData?: YearData;
}
const props = withDefaults(defineProps<Props>(), {
yearData: undefined,
});
// 计算各个状态的百分比和显示信息
// 根据需求:进度条仅显示空置、在租(有合同),未有安排的用斜线代替
const vacantSegments = computed(() => {
if (
!props.yearData ||
!props.yearData.allData ||
props.yearData.allData.length === 0
) {
// 如果没有数据,不显示空置段,全部显示为未安排(斜线)
return [];
}
const segments: Array<{ percentage: number; days: number }> = [];
const totalDays = props.yearData.allDays || 365;
// 计算在租天数
const leaseDays = props.yearData.allData
.filter((item) => item.type === 1)
.reduce((sum, item) => sum + (item.days || 0), 0);
// 计算其他已安排天数(可能是空置记录,type !== 1)
const otherArrangedDays = props.yearData.allData
.filter((item) => item.type !== 1)
.reduce((sum, item) => sum + (item.days || 0), 0);
// 总已安排天数
const totalArrangedDays = leaseDays + otherArrangedDays;
// 如果总已安排天数小于总天数,剩余部分可能是空置(如果是在租期间的空置)
// 但根据需求,只有明确标记为空置的才显示,否则显示为未安排
if (otherArrangedDays > 0) {
// 如果有其他类型的数据(非在租),可能是空置记录
segments.push({
percentage: (otherArrangedDays / totalDays) * 100,
days: otherArrangedDays,
});
} else if (totalArrangedDays < totalDays && leaseDays > 0) {
// 如果有在租数据,但总天数大于已安排天数,剩余部分可能是空置
// 这里需要根据实际业务逻辑判断,暂时不显示为空置,显示为未安排
}
return segments;
});
const leaseSegments = computed(() => {
if (!props.yearData || !props.yearData.allData) return [];
const totalDays = props.yearData.allDays || 365;
const segments: Array<{
percentage: number;
companyName: string;
days: number;
}> = [];
// type === 1 表示在租(有合同)
props.yearData.allData?.forEach((item) => {
if (item.type === 1 && item.days > 0) {
segments.push({
percentage: (item.days / totalDays) * 100,
companyName: item.companyName || '',
days: item.days,
});
}
});
return segments;
});
const unarrangedPercentage = computed(() => {
if (!props.yearData) return 100; // 如果没有数据,全部显示为未安排
const totalDays = props.yearData.allDays || 365;
// 只计算已安排的天数(空置 + 在租)
let arrangedDays = 0;
if (props.yearData.allData && props.yearData.allData.length > 0) {
props.yearData.allData.forEach((item) => {
arrangedDays += item.days || 0;
});
}
const unarrangedDays = totalDays - arrangedDays;
return unarrangedDays > 0 ? (unarrangedDays / totalDays) * 100 : 0;
});
</script>
<style scoped>
.year-timeline-container {
min-width: 200px;
}
.timeline-bar {
border-radius: 2px;
overflow: hidden;
}
.timeline-segment {
transition: all 0.3s;
}
.segment-label {
user-select: none;
}
</style>
@@ -0,0 +1,413 @@
<template>
<div class="business_project_wrap">
<Breadcrumb :items="['menu.business', 'menu.business.house-atlas']" />
<a-card class="general-card full_height">
<template #title>
<div class="card-title_between">
<p class="title">{{ t('menu.business.house-atlas') }}</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-space>
</div>
</div>
</template>
<!-- 使用新的 DataTable 组件 -->
<div class="table-container" :style="{ position: 'relative' }">
<DataTable
:data="renderData"
:columns="projectColumn"
: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
v-for="(year, index) in yearRange"
:key="year"
#[`year-${year}`]="{ record }"
>
<div class="year-cell" :class="{ 'first-year': index === 0 }">
<!-- 时间线只在第一个列中渲染,使用绝对定位跨越所有列 -->
<div
v-if="index === 0"
class="timeline-row-wrapper"
:style="{ width: `${timelineWidth}px` }"
>
<CrossYearTimeline
:years-data="getAllYearsData(record)"
:year-range="yearRange"
/>
</div>
</div>
</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>
</div>
</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, computed } 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 { getRoomsGraphList, getRoom, delRoom } from '@/api/room';
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';
import CrossYearTimeline from './components/cross-year-timeline.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],
});
// 计算5年年份范围(当前年的前1年到后3年)
const currentYear = new Date().getFullYear();
const yearRange = computed(() => {
const years: number[] = [];
for (let i = -1; i <= 3; i++) {
years.push(currentYear + i);
}
return years;
});
// 计算时间线总宽度
const timelineWidth = computed(() => {
return yearRange.value.length * 200; // 每个年份列200px
});
// 通用空值处理函数
const renderEmpty = (value: any) => value ?? '-';
// 获取所有年份的数据
const getAllYearsData = (record: any) => {
if (!record?.yearsData || !Array.isArray(record.yearsData)) {
return [];
}
// 只返回在年份范围内的数据
return record.yearsData.filter((item: any) => {
const year = Number(item.year);
return yearRange.value.includes(year);
});
};
// 动态生成列配置
const generateColumns = (): TableColumnData[] => {
const baseColumns: TableColumnData[] = [
{
title: '房源名称',
dataIndex: 'name',
key: 'name',
width: 150,
render: ({ record }) => renderEmpty(record.name),
},
{
title: '所属',
dataIndex: 'projectName',
key: 'projectName',
width: 200,
render: ({ record }) => {
const buildingName = record.buildingName
? ` ${record.buildingName}座`
: '';
const floorName = record.floorName ? `・${record.floorName}层` : '';
return renderEmpty(record.projectName) + buildingName + floorName;
},
},
{
title: '面积(㎡)',
dataIndex: 'rentArea',
key: 'rentArea',
width: 100,
render: ({ record }) => renderEmpty(record.rentArea),
},
];
// 添加年份列(每个年份独立一列,各自显示标题)
const yearColumns: TableColumnData[] = yearRange.value.map((year) => ({
title: String(year),
key: `year-${year}`,
slotName: `year-${year}`,
width: 200,
align: 'center' as const,
}));
// 添加状态和操作列
const otherColumns: TableColumnData[] = [
{
title: '当前状态',
key: 'statusStr',
slotName: 'status',
width: 120,
},
{
title: '操作',
key: 'operate',
slotName: 'operate',
width: 200,
},
];
return [...baseColumns, ...yearColumns, ...otherColumns];
};
// 列配置
const columns = ref<TableColumnData[]>(generateColumns());
const queryParams = ref<ProjectPaginationParams>({
page: current.value,
page_size: pageSize.value,
keyword: '',
});
const getHouseListData = async () => {
setLoading(true);
const { code, data } = await getRoomsGraphList(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 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.value);
getHouseListData();
});
</script>
<style scoped lang="less">
.table-container {
position: relative;
}
.year-cell {
position: relative;
padding: 8px;
overflow: visible;
height: 56px;
min-height: 56px;
&.first-year {
overflow: visible;
}
}
.timeline-row-wrapper {
position: absolute;
left: 0;
top: 0;
height: 100%;
z-index: 2;
pointer-events: none;
padding: 8px;
box-sizing: border-box;
}
// 确保表格行可以包含绝对定位的元素
:deep(.arco-table-tr) {
position: relative;
}
:deep(.arco-table-td) {
position: relative;
overflow: visible;
}
</style>
-3
View File
@@ -1,3 +0,0 @@
<template>
<div>house-list</div>
</template>
@@ -0,0 +1,343 @@
<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/room';
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';
import { PRICE_UNIT_MAP } from '@/constants';
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 }) => {
if (Number(record.price) <= 0) return '-';
const unit = PRICE_UNIT_MAP.get(record?.priceType);
return `${record?.price} ${unit}`;
},
},
{ 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>
-3
View File
@@ -1,3 +0,0 @@
<template>
<div>house-view</div>
</template>
@@ -0,0 +1,595 @@
<template>
<div class="house-view-content px-[24px]">
<!-- 统计数据区域 -->
<div class="flex items-center justify-between bg-white pb-[5px] w-full">
<div class="flex items-center bg-[#f2f3f5] flex-1">
<div class="flex items-center justify-center flex-1">
<div class="flex flex-col items-center justify-center flex-1">
<span class="text-sm text-[#86909c]">房源</span>
<span class="text-[28px] text-[#121111] mt-[8px]"
>{{ props.statisticsData.roomNum || 0 }}套</span
>
</div>
<div class="flex flex-col items-center justify-center flex-1">
<span class="text-sm text-[#86909c]">出租率</span>
<span class="text-[28px] text-[#121111] mt-[8px]"
>{{ props.statisticsData.rentRateStr || '0.00' }}%</span
>
</div>
<div class="flex flex-col items-center justify-center flex-1">
<span class="text-sm text-[#86909c]">累计出租率</span>
<span class="text-[28px] text-[#121111] mt-[8px]"
>{{ props.statisticsData.totalRentRateStr || '0.00' }}%</span
>
</div>
<div class="flex flex-col items-center justify-center flex-1">
<span class="text-sm text-[#86909c]">计租面积</span>
<span class="text-[28px] text-[#121111] mt-[8px]">{{
props.statisticsData.rentAreaStr || '0.00㎡'
}}</span>
</div>
</div>
<div class="w-[300px] h-[150px] ml-[50px] flex-shrink-0 relative">
<Chart :options="chartOption" />
</div>
</div>
</div>
<!-- 筛选区域 -->
<div class="filter-section mt-[24px]">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<!-- 状态筛选 -->
<div class="flex items-center">
<div
v-for="status in statusOptions"
:key="status.id"
class="flex items-center mr-[20px] last:mr-0 rounded cursor-pointer transition-all border border-transparent hover:bg-[#f2f3f5]"
:class="{
'bg-[#e8f3ff] border-[#165dff]': selectedStatus.includes(
status.id
),
}"
@click="toggleStatus(status.id)"
>
<span
class="w-[20px] h-[20px] rounded-full"
:style="{ backgroundColor: status.color }"
></span>
<span class="text-[14px] ml-[8px] text-[#333]">{{
status.text
}}</span>
</div>
</div>
</div>
<div class="flex items-center">
<a-input-search
v-model="searchKeyword"
placeholder="请输入内容"
style="width: 200px"
@search="handleSearch"
/>
<a-button class="mx-[8px]" @click="showFilterDrawer = true">
<template #icon>
<icon-filter />
</template>
筛选
</a-button>
<a-button-group>
<a-button
:type="viewMode === 'grid' ? 'primary' : 'outline'"
@click="viewMode = 'grid'"
>
<template #icon>
<icon-apps />
</template>
</a-button>
<a-button
:type="viewMode === 'list' ? 'primary' : 'outline'"
@click="viewMode = 'list'"
>
<template #icon>
<icon-list />
</template>
</a-button>
</a-button-group>
<a-button type="primary" class="ml-[8px]" @click="handleAddHouse">
<template #icon>
<icon-plus />
</template>
新建房源
</a-button>
</div>
</div>
</div>
<!-- 房源网格区域 -->
<div class="room-grid-section">
<div v-for="floor in floorList" :key="floor.floorId">
<div class="flex items-center my-[10px]">
<span class="text-base font-semibold text-[#1d2129]"
>{{ floor.floorName }}·</span
>
<span class="text-sm text-[#86909c]">{{ floor.totalArea }}m²</span>
</div>
<div>
<div
v-for="(row, rowIndex) in floor.rows"
:key="rowIndex"
class="grid grid-cols-8 gap-x-[8px] mb-2"
>
<div
v-for="(cell, cellIndex) in row"
:key="cellIndex"
class="h-[74px] border-2 rounded p-2 relative cursor-pointer transition-all hover:shadow-md p-[5px]"
:class="getRoomCellClass(cell)"
:style="getRoomCellStyle(cell)"
@click="handleRoomClick(cell.room)"
>
<template v-if="cell.room">
<div class="h-full flex flex-col">
<div
class="text-sm font-semibold text-[#1d2129] overflow-hidden text-ellipsis whitespace-nowrap"
>
{{ cell.room.name || cell.room.roomName }}
</div>
<div class="text-xs text-[#86909c]"
>{{ cell.room.rentArea || cell.room.area }}m²</div
>
<div class="text-xs text-[#4e5969] flex-1">{{
getRoomInfo(cell.room)
}}</div>
<div
v-if="cell.room.status === 4"
class="absolute top-1 right-1 px-1.5 py-0.5 bg-[#00b42a] text-white rounded text-[10px]"
>
{{ cell.room.statusStr || '在租' }}
</div>
<div
v-if="cell.room.isExpired"
class="absolute top-1 right-1 px-1.5 py-0.5 bg-[#f53f3f] text-white rounded text-[10px]"
>
逾
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
<!-- 筛选弹窗 -->
<HouseViewFilterDrawer
v-model:visible="showFilterDrawer"
:room-list="props.roomList"
:project-name="props.projectName"
@confirm="handleFilterConfirm"
/>
<!-- 房源详情弹窗 -->
<HouseInfoDrawer
v-model:visible="showRoomDetailDrawer"
:project-id="currentProjectId"
:detail-info="roomDetailInfo"
/>
<!-- 新建/编辑房源弹窗 -->
<HouseFormDrawer
v-model:visible="showHouseFormDrawer"
:is-edit="false"
@submitted="handleHouseFormSubmitted"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { Message } from '@arco-design/web-vue';
import Chart from '@/components/chart/index.vue';
import HouseViewFilterDrawer from './house-view-filter-drawer.vue';
import HouseInfoDrawer from '@/views/business/space/drawer/house-info-drawer.vue';
import HouseFormDrawer from '@/views/business/space/drawer/house-form-drawer.vue';
import { getRoom } from '@/api/room';
import { useUserStore } from '@/store';
interface Props {
statisticsData: any;
roomList: any[];
loading?: boolean;
projectName?: string;
}
const props = withDefaults(defineProps<Props>(), {
statisticsData: () => ({}),
roomList: () => [],
loading: false,
projectName: '',
});
const userStore = useUserStore();
const showRoomDetailDrawer = ref(false);
const roomDetailInfo = ref<any>(null);
const showHouseFormDrawer = ref(false);
const currentProjectId = computed(() => userStore.currentProjectId ?? null);
const emit = defineEmits<{
search: [keyword: string];
filterChange: [
filters: {
buildingIds?: number[] | null;
floorIds?: number[] | null;
status?: number[];
rentArea?: number[];
vacantDays?: number[];
contractStatuses?: string[] | null;
}
];
refresh: [];
}>();
const searchKeyword = ref('');
const selectedStatus = ref<number[]>([]);
const viewMode = ref<'grid' | 'list'>('grid');
const showFilterDrawer = ref(false);
// 房源状态选项(6个状态)
const statusOptions = [
{ id: 4, text: '在租', color: '#00b42a' },
{ id: 1, text: '招商', color: '#165dff' },
{ id: 2, text: '保留', color: '#ff7d00' },
{ id: 5, text: '即将到期', color: '#f53f3f' },
{ id: 6, text: '到期', color: '#f53f3f' },
{ id: 3, text: '下架', color: '#86909c' },
];
// 切换状态筛选
const toggleStatus = (statusId: number) => {
const index = selectedStatus.value.indexOf(statusId);
if (index > -1) {
selectedStatus.value.splice(index, 1);
} else {
selectedStatus.value.push(statusId);
}
emit('filterChange', { status: selectedStatus.value });
};
// 环形图配置
const chartOption = computed(() => {
const data = [
{
name: '下架面积',
value: parseFloat(
props.statisticsData.removalAreaStr?.replace('㎡', '') || '0'
),
},
{
name: '保留面积',
value: parseFloat(
props.statisticsData.keepAreaStr?.replace('㎡', '') || '0'
),
},
{
name: '可租面积',
value: parseFloat(
props.statisticsData.rentAreaStr?.replace('㎡', '') || '0'
),
},
{
name: '已租面积',
value: parseFloat(
props.statisticsData.useAreaStr?.replace('㎡', '') || '0'
),
},
].filter((item) => item.value > 0);
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}㎡ ({d}%)',
confine: true, // 将 tooltip 限制在图表区域内
appendToBody: true, // 将 tooltip 追加到 body,避免被遮挡
extraCssText: 'z-index: 9999;', // 确保 tooltip 在最上层
},
legend: {
orient: 'vertical',
right: 0,
top: 'middle',
itemWidth: 10,
itemHeight: 10,
itemGap: 8,
textStyle: {
fontSize: 11,
color: '#4e5969',
},
formatter: (name: string) => {
// 限制图例文本长度,避免过长
return name.length > 6 ? name.substring(0, 6) + '...' : name;
},
},
grid: {
left: 0,
right: '25%', // 为图例留出空间
top: 10,
bottom: 10,
},
series: [
{
name: '面积分布',
type: 'pie',
radius: ['35%', '65%'], // 稍微缩小半径,为图例留出空间
center: ['40%', '50%'], // 将图表向左移动
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: false,
},
labelLine: {
show: false,
},
emphasis: {
label: {
show: true,
fontSize: 12,
fontWeight: 'bold',
},
},
data: data,
},
],
};
});
// 计算房源占用的格子数
const getRoomCells = (area: number): number => {
if (area <= 250) return 1;
if (area <= 500) return 2;
if (area <= 750) return 3;
if (area <= 1000) return 4;
if (area <= 1250) return 5;
if (area <= 1500) return 6;
if (area <= 1750) return 7;
return 8;
};
// 按楼层组织房源数据(数据已经是按楼层分组的结构)
const floorList = computed(() => {
// 数据已经是按楼层分组的结构:list 中每个元素是一个楼层,包含 rooms 数组
return props.roomList
.map((floorData: any) => {
const floorId = floorData.id || floorData.floorId || 0;
const floorName = floorData.floorName || `楼层${floorId}`;
const rooms = floorData.rooms || [];
// 计算该楼层所有房间的总面积(使用 rentArea 累加)
const totalArea = rooms.reduce((sum: number, room: any) => {
return sum + parseFloat(room.rentArea || room.area || 0);
}, 0);
// 对每个楼层进行网格布局
const rows: any[][] = [];
let currentRow: any[] = [];
if (viewMode.value === 'list') {
// 列表模式:每个房源固定占用1格,一行最多8个
rooms.forEach((room: any) => {
// 如果当前行已满8个,换行
if (currentRow.length >= 8) {
rows.push(currentRow);
currentRow = [];
}
// 添加房源到当前行,固定占用1格
currentRow.push({ room, cells: 1 });
});
// 添加最后一行
if (currentRow.length > 0) {
rows.push(currentRow);
}
// 填充空单元格到8格
rows.forEach((row) => {
while (row.length < 8) {
row.push({ room: null, cells: 1 });
}
});
} else {
// 网格模式:按面积计算格子数
let currentRowArea = 0;
let currentRowTotalCells = 0; // 当前行的总格子数
rooms.forEach((room: any) => {
const area = parseFloat(room.rentArea || room.area || 0);
const cells = getRoomCells(area);
// 如果当前行加上这个房源会超过8格或2000平,则先处理当前行,然后换行
if (
currentRowTotalCells + cells > 8 ||
currentRowArea + area > 2000
) {
if (currentRow.length > 0) {
// 如果当前行的总格子数小于8,按比例放大到8格
if (currentRowTotalCells < 8) {
const scale = 8 / currentRowTotalCells;
currentRow.forEach((cell) => {
cell.cells = Math.round(cell.cells * scale);
});
}
rows.push(currentRow);
}
currentRow = [];
currentRowArea = 0;
currentRowTotalCells = 0;
}
// 添加房源到当前行
currentRow.push({ room, cells });
currentRowArea += area;
currentRowTotalCells += cells;
// 如果当前行刚好8格,处理并换行
if (currentRowTotalCells === 8) {
rows.push(currentRow);
currentRow = [];
currentRowArea = 0;
currentRowTotalCells = 0;
} else if (currentRowTotalCells > 8) {
// 如果超过8格,需要将最后一个房源移除,处理当前行,然后重新添加
const lastCell = currentRow.pop();
currentRowTotalCells -= lastCell.cells;
currentRowArea -= parseFloat(
lastCell.room.rentArea || lastCell.room.area || 0
);
// 如果当前行的总格子数小于8,按比例放大到8格
if (currentRowTotalCells < 8) {
const scale = 8 / currentRowTotalCells;
currentRow.forEach((cell) => {
cell.cells = Math.round(cell.cells * scale);
});
}
rows.push(currentRow);
// 重新开始一行,添加刚才移除的房源
currentRow = [lastCell];
currentRowArea = parseFloat(
lastCell.room.rentArea || lastCell.room.area || 0
);
currentRowTotalCells = lastCell.cells;
}
});
// 添加最后一行
if (currentRow.length > 0) {
// 如果最后一行总格子数小于8,按比例放大到8格
if (currentRowTotalCells < 8) {
const scale = 8 / currentRowTotalCells;
currentRow.forEach((cell) => {
cell.cells = Math.round(cell.cells * scale);
});
}
rows.push(currentRow);
}
}
return {
floorId,
floorName,
totalArea,
rows,
};
})
.sort((a: any, b: any) => b.floorId - a.floorId); // 按楼层ID排序(降序)
});
// 获取房源单元格样式
const getRoomCellStyle = (cell: any) => {
if (!cell.room) return {};
const cells = cell.cells || 1;
return {
gridColumn: `span ${cells}`,
};
};
// 获取房源单元格类名
const getRoomCellClass = (cell: any) => {
if (!cell.room) return 'bg-[#f7f8fa] border-[#e5e6eb]';
const status = cell.room.status;
const statusMap: Record<number, string> = {
1: 'border-[#165dff] bg-[#f0f5ff]', // 招商
2: 'border-[#ff7d00] bg-[#fff7e6]', // 保留
3: 'border-[#86909c] bg-[#f2f3f5]', // 下架
4: 'border-[#00b42a] bg-[#e8f8e8]', // 在租
5: 'border-[#f53f3f] bg-[#ffece8]', // 即将到期
6: 'border-[#f53f3f] bg-[#ffece8]', // 到期
};
return statusMap[status] || 'border-[#e5e6eb]';
};
// 获取房源信息文本
const getRoomInfo = (room: any) => {
if (room.status === 4) {
// 在租状态
if (room.dateStr) {
return room.dateStr;
}
if (room.days !== undefined && room.days > 0) {
return `剩余${room.days}天`;
}
if (room.days !== undefined && room.days < 0) {
return `到期${Math.abs(room.days)}天`;
}
return '租物';
} else if (room.status === 1) {
// 招商状态
if (room.days !== undefined) {
return `空置 ${room.days}天`;
}
return '空置';
} else if (room.status === 2) {
// 保留状态
return '客户已下订,保留中';
} else if (room.status === 3) {
// 下架状态
return '暂时下架,不出租了';
}
return '';
};
// 搜索处理
const handleSearch = () => {
emit('search', searchKeyword.value);
};
// 筛选确认处理
const handleFilterConfirm = (filters: {
buildingIds: number[] | null;
floorIds: number[] | null;
statusIds: number[] | null;
rentArea: number[];
vacantDays: number[];
contractStatuses: string[] | null;
}) => {
emit('filterChange', {
buildingIds: filters.buildingIds,
floorIds: filters.floorIds,
status: filters.statusIds || undefined,
rentArea: filters.rentArea,
vacantDays: filters.vacantDays,
contractStatuses: filters.contractStatuses,
});
};
// 点击房源查看详情
const handleRoomClick = async (room: any) => {
if (!room || !room.id) {
Message.warning('房源信息不完整');
return;
}
try {
// 调用 API 获取房源详情
const response = await getRoom(room.id);
if (response.code === 200 && response.data) {
roomDetailInfo.value = response.data;
showRoomDetailDrawer.value = true;
} else {
Message.error(response.message || '获取房源详情失败');
}
} catch (error) {
console.error('获取房源详情失败:', error);
Message.error('获取房源详情失败');
}
};
// 新建房源
const handleAddHouse = () => {
showHouseFormDrawer.value = true;
};
// 房源表单提交成功后的回调
const handleHouseFormSubmitted = () => {
// 触发父组件刷新数据
emit('refresh');
};
</script>
@@ -0,0 +1,630 @@
<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-[32px] px-[8px] leading-[32px] block rounded"
>
{{ props.projectName || '-' }}
</span>
</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-slider
v-model="filterForm.vacantDays"
range
:min="vacantDaysMin"
:max="vacantDaysMax"
:marks="vacantDaysMarks"
/>
</a-form-item>
<!-- 房源合同(多选) -->
<a-form-item label="房源合同">
<a-select
v-model="filterForm.contractStatuses"
multiple
placeholder="请选择房源合同"
:max-tag-count="3"
>
<a-option
v-for="item in contractStatusList"
:key="item"
:value="item"
>
{{ item }}
</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/dictionary';
// 特殊值用于表示"所有"选项(使用负数避免与真实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[];
vacantDays: number[];
contractStatuses: string[] | null;
}
): void;
}>();
// 保存的筛选状态
const savedFilterState = ref<{
buildingIds: number[];
floorIds: number[];
statusIds: number[];
rentArea: number[];
vacantDays: number[];
contractStatuses: string[];
} | null>(null);
// 筛选表单数据
const filterForm = ref({
buildingIds: [] as number[],
floorIds: [] as number[],
statusIds: [] as number[],
rentArea: [0, 0] as number[],
vacantDays: [0, 0] as number[],
contractStatuses: [] as string[],
});
// 楼栋列表(从房源列表中提取唯一值)
const buildingList = computed(() => {
if (!props.roomList || props.roomList.length === 0) return [];
const buildingMap = new Map();
props.roomList.forEach((floor) => {
if (floor.rooms && Array.isArray(floor.rooms)) {
floor.rooms.forEach((room: any) => {
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((floor) => {
if (floor.id !== undefined && floor.floorName) {
if (!floorMap.has(floor.id)) {
floorMap.set(floor.id, {
id: floor.id,
name: floor.floorName,
});
}
}
});
return Array.from(floorMap.values());
});
// 房源合同状态列表(从房源列表中提取唯一值)
const contractStatusList = computed(() => {
if (!props.roomList || props.roomList.length === 0) return [];
const statusSet = new Set<string>();
props.roomList.forEach((floor) => {
if (floor.rooms && Array.isArray(floor.rooms)) {
floor.rooms.forEach((room: any) => {
if (room.contractStatusStr && room.contractStatusStr.trim()) {
statusSet.add(room.contractStatusStr);
}
});
}
});
return Array.from(statusSet).sort();
});
// 计租面积范围(从所有房源中计算)
const rentAreaMin = computed(() => {
if (!props.roomList || props.roomList.length === 0) return 0;
const areas: number[] = [];
props.roomList.forEach((floor) => {
if (floor.rooms && Array.isArray(floor.rooms)) {
floor.rooms.forEach((room: any) => {
const area = parseFloat(room.rentArea || 0);
if (!isNaN(area) && area > 0) {
areas.push(area);
}
});
}
});
return areas.length > 0 ? Math.min(...areas) : 0;
});
const rentAreaMax = computed(() => {
if (!props.roomList || props.roomList.length === 0) return 100;
const areas: number[] = [];
props.roomList.forEach((floor) => {
if (floor.rooms && Array.isArray(floor.rooms)) {
floor.rooms.forEach((room: any) => {
const area = parseFloat(room.rentArea || 0);
if (!isNaN(area) && area > 0) {
areas.push(area);
}
});
}
});
return areas.length > 0 ? Math.max(...areas) : 100;
});
const rentAreaMarks = computed(() => {
return {
[rentAreaMin.value]: `${rentAreaMin.value}㎡`,
[rentAreaMax.value]: `${rentAreaMax.value}㎡`,
};
});
// 空置天数范围(只从招商状态的房源中计算)
const vacantDaysMin = computed(() => {
if (!props.roomList || props.roomList.length === 0) return 0;
const days: number[] = [];
props.roomList.forEach((floor) => {
if (floor.rooms && Array.isArray(floor.rooms)) {
floor.rooms.forEach((room: any) => {
// 只计算招商状态(status === 1)的房源
if (
room.status === 1 &&
room.days !== undefined &&
room.days !== null
) {
const day = Number(room.days);
if (!isNaN(day) && day >= 0) {
days.push(day);
}
}
});
}
});
return days.length > 0 ? Math.min(...days) : 0;
});
const vacantDaysMax = computed(() => {
if (!props.roomList || props.roomList.length === 0) return 100;
const days: number[] = [];
props.roomList.forEach((floor) => {
if (floor.rooms && Array.isArray(floor.rooms)) {
floor.rooms.forEach((room: any) => {
// 只计算招商状态(status === 1)的房源
if (
room.status === 1 &&
room.days !== undefined &&
room.days !== null
) {
const day = Number(room.days);
if (!isNaN(day) && day >= 0) {
days.push(day);
}
}
});
}
});
return days.length > 0 ? Math.max(...days) : 100;
});
const vacantDaysMarks = computed(() => {
return {
[vacantDaysMin.value]: `${vacantDaysMin.value}天`,
[vacantDaysMax.value]: `${vacantDaysMax.value}天`,
};
});
// 用于防止 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) {
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) {
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;
});
}
}
}
);
// 初始化筛选表单
const initFilterForm = () => {
const minArea = rentAreaMin.value || 0;
const maxArea = rentAreaMax.value || 100;
const minDays = vacantDaysMin.value || 0;
const maxDays = vacantDaysMax.value || 100;
// 如果有保存的状态,恢复它(但要过滤掉不存在的ID)
if (savedFilterState.value) {
const allBuildingIds = buildingList.value.map((b) => b.id);
const allFloorIds = floorList.value.map((f) => f.id);
// 处理楼栋ID
let buildingIds = savedFilterState.value.buildingIds;
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];
}
}
// 处理楼层ID
let floorIds = savedFilterState.value.floorIds;
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],
vacantDays: [...savedFilterState.value.vacantDays],
contractStatuses: [...savedFilterState.value.contractStatuses],
};
} else {
// 否则初始化,楼栋和楼层默认选中"所有"
filterForm.value = {
buildingIds: [ALL_BUILDING_ID],
floorIds: [ALL_FLOOR_ID],
statusIds: [],
rentArea: [minArea, maxArea],
vacantDays: [minDays, maxDays],
contractStatuses: [],
};
}
};
const handleCancel = () => {
emit('update:visible', false);
};
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],
vacantDays: [...filterForm.value.vacantDays],
contractStatuses: [...filterForm.value.contractStatuses],
};
// 标准化ID数组:如果选择了"所有"选项,则返回null(表示全部)
const normalizeIds = (ids: number[], allOptionId: number) => {
if (ids.length === 0) return null;
// 如果选择了"所有"选项,则返回null
if (ids.includes(allOptionId)) {
return null;
}
return ids;
};
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,
vacantDays: filterForm.value.vacantDays,
contractStatuses:
filterForm.value.contractStatuses.length === 0
? null
: filterForm.value.contractStatuses,
});
handleCancel();
};
// 监听抽屉打开,初始化数据
watch(
() => props.visible,
(val) => {
if (val) {
// 延迟初始化,确保 roomList 已更新
setTimeout(() => {
initFilterForm();
}, 0);
}
},
{ 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 }
);
</script>
<style lang="less" scoped>
:deep(label.arco-form-item-label) {
width: 100% !important;
}
:deep(.arco-slider-mark) {
text-wrap: nowrap;
}
</style>
@@ -0,0 +1,351 @@
<template>
<div class="house_view_wrap">
<Breadcrumb :items="['menu.business', 'menu.business.house-view']" />
<div class="bg-[#fff] flex flex-col">
<a-tabs
v-if="topBuildList && topBuildList.length > 0"
v-model="activeBuildId"
type="card"
class="flex-shrink-0"
@change="handleTabChange"
>
<a-tab-pane
v-for="item in topBuildList"
:key="item.buildingId"
:title="item.buildingName"
>
<div class="house-view-scroll-container">
<HouseViewContent
v-if="
currentProjectId &&
activeBuildId &&
Number(activeBuildId) === Number(item.buildingId)
"
:key="`building-${activeBuildId}-project-${currentProjectId}`"
:statistics-data="statisticsData"
:room-list="roomList"
:loading="contentLoading"
:project-name="currentProjectName"
@search="handleSearch"
@filter-change="handleFilterChange"
@refresh="handleRefresh"
/>
</div>
</a-tab-pane>
</a-tabs>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted, computed } from 'vue';
import {
getTopBuildingTagsByProjectId,
getStatisticsData,
getRoomOverList,
} from '@/api/room';
import { useUserStore } from '@/store';
import { Message } from '@arco-design/web-vue';
import type { BuildingTag } from '@/types/business';
import HouseViewContent from './components/house-view-content.vue';
const userStore = useUserStore();
const topBuildList = ref<BuildingTag[]>([]);
const loading = ref(false);
const activeBuildId = ref<number | null>(null);
const statisticsData = ref<any>({});
const roomList = ref<any[]>([]);
const contentLoading = ref(false);
const searchKeyword = ref('');
const selectedStatus = ref<number[]>([]);
const selectedBuildingIds = ref<number[] | null>(null);
const selectedFloorIds = ref<number[] | null>(null);
const rentAreaRange = ref<number[]>([0, 0]);
const vacantDaysRange = ref<number[]>([0, 0]);
const selectedContractStatuses = ref<string[] | null>(null);
const isInitialized = ref(false); // 标记是否已经初始化加载过数据
// 计算当前项目ID
const currentProjectId = computed(() => userStore.currentProjectId);
const currentProjectName = computed(() => {
const id = userStore.currentProjectId;
const list = userStore.projects || [];
const found = list.find((p: any) => p.id === id);
return found?.name || '';
});
// 加载统计数据
const loadStatisticsData = async () => {
if (!currentProjectId.value || !activeBuildId.value) return;
try {
const response = await getStatisticsData({
keyword: searchKeyword.value,
projectId: currentProjectId.value,
buildingId: activeBuildId.value,
floorId: 0,
rentAreaMin: rentAreaRange.value[0] || 0,
rentAreaMax: rentAreaRange.value[1] || 0,
status: selectedStatus.value.length > 0 ? selectedStatus.value : [],
});
if (response.code === 200 && response.data) {
const data = response.data as { statisticsData?: any };
if (data.statisticsData) {
// statisticsData.value = data.statisticsData
statisticsData.value = {
roomNum: 25,
roomAreaStr: '1000.㎡',
rentAreaStr: '1380.00㎡',
keepAreaStr: '600.00㎡',
useAreaStr: '809.00㎡',
removalAreaStr: '320.00㎡',
rentRateStr: '67.80',
totalRentRateStr: '79.10',
};
}
}
} catch (error) {
console.error('获取统计数据失败:', error);
}
};
// 加载房源列表数据
const loadRoomData = async () => {
if (!currentProjectId.value || !activeBuildId.value) return;
contentLoading.value = true;
try {
const response = await getRoomOverList({
keyword: searchKeyword.value,
projectId: currentProjectId.value,
buildingId: activeBuildId.value,
floorId: 0,
rentAreaMin: rentAreaRange.value[0] || 0,
rentAreaMax: rentAreaRange.value[1] || 0,
status: selectedStatus.value.length > 0 ? selectedStatus.value : [],
// 注意:如果接口支持空置天数和标签筛选,可以在这里添加
// vacantDaysMin: vacantDaysRange.value[0] || 0,
// vacantDaysMax: vacantDaysRange.value[1] || 0,
// tagIds: selectedTagIds.value.length > 0 ? selectedTagIds.value : [],
});
if (response.code === 200 && response.data) {
const data = response.data as { list?: any[] };
// 数据已经是按楼层分组的结构,直接使用
roomList.value = Array.isArray(data.list) ? data.list : [];
}
} catch (error) {
console.error('获取房源列表失败:', error);
Message.error('获取房源列表失败');
} finally {
contentLoading.value = false;
}
};
// 加载所有数据
const loadAllData = async () => {
if (!currentProjectId.value || !activeBuildId.value) return;
await Promise.all([loadStatisticsData(), loadRoomData()]);
};
// 搜索处理
const handleSearch = (keyword: string) => {
searchKeyword.value = keyword;
loadAllData();
};
// 筛选变化处理
const handleFilterChange = (filters: {
buildingIds?: number[] | null;
floorIds?: number[] | null;
status?: number[];
rentArea?: number[];
vacantDays?: number[];
contractStatuses?: string[] | null;
}) => {
if (filters.buildingIds !== undefined) {
selectedBuildingIds.value = filters.buildingIds;
}
if (filters.floorIds !== undefined) {
selectedFloorIds.value = filters.floorIds;
}
if (filters.status !== undefined) {
selectedStatus.value = filters.status;
}
if (filters.rentArea !== undefined) {
rentAreaRange.value = filters.rentArea;
}
if (filters.vacantDays !== undefined) {
vacantDaysRange.value = filters.vacantDays;
}
if (filters.contractStatuses !== undefined) {
selectedContractStatuses.value = filters.contractStatuses;
}
loadAllData();
};
// 刷新数据(新建房源成功后调用)
const handleRefresh = () => {
loadAllData();
};
// Tab 切换处理
const handleTabChange = (activeKey: string | number) => {
console.log('handleTabChange 被调用', {
activeKey,
activeKeyType: typeof activeKey,
activeBuildId: activeBuildId.value,
activeBuildIdType: typeof activeBuildId.value,
currentProjectId: currentProjectId.value,
});
// 确保 activeBuildId 被正确设置(转换为数字)
const newBuildingId =
typeof activeKey === 'string' ? Number(activeKey) : activeKey;
// 确保 activeBuildId 也是数字类型
if (newBuildingId && !isNaN(newBuildingId)) {
activeBuildId.value = newBuildingId;
console.log('Tab 切换,设置 activeBuildId 并加载数据', {
newBuildingId,
currentBuildId: activeBuildId.value,
});
// activeBuildId 变化会触发 watch,这里不需要再次调用 loadAllData
}
};
async function getTopBuildingTagsData() {
const projectId = userStore.currentProjectId;
if (!projectId) {
Message.warning('请先选择项目');
return;
}
loading.value = true;
try {
const response = await getTopBuildingTagsByProjectId(projectId);
if (response.code === 200 && response.data?.tags) {
topBuildList.value = Array.isArray(response.data.tags)
? response.data.tags
: [];
if (topBuildList.value.length > 0) {
// 如果当前选中的楼栋不在新列表中,或者没有选中楼栋,则选择第一个
const currentBuilding = topBuildList.value.find(
(item) => item.buildingId === activeBuildId.value
);
if (!currentBuilding) {
activeBuildId.value = topBuildList.value[0]?.buildingId;
// activeBuildId 变化会触发 watch,不需要手动调用
}
// 如果楼栋ID没有变化(可能是同一个项目重新加载),但需要初始化加载数据
// 这种情况下 watch 不会触发,需要手动调用一次
else if (
activeBuildId.value &&
currentProjectId.value &&
!isInitialized.value
) {
isInitialized.value = true;
loadAllData();
}
} else {
activeBuildId.value = null;
}
} else {
topBuildList.value = [];
activeBuildId.value = null;
Message.error(response.message || '获取楼栋列表失败');
}
} catch (error) {
console.error('获取楼栋列表失败:', error);
topBuildList.value = [];
activeBuildId.value = null;
Message.error('获取楼栋列表失败');
} finally {
loading.value = false;
}
}
// 监听 activeBuildId 变化,重新加载数据
watch(
activeBuildId,
(newBuildingId, oldBuildingId) => {
console.log('watch activeBuildId 触发', {
newBuildingId,
oldBuildingId,
currentProjectId: currentProjectId.value,
isEqual: newBuildingId === oldBuildingId,
});
// 如果 buildingId 有值且项目ID也有值
if (newBuildingId && currentProjectId.value) {
// 如果值变化了(从 null 变为有值,或者从一个值变为另一个值),就加载数据
if (newBuildingId !== oldBuildingId) {
console.log('activeBuildId 变化,准备加载数据', {
newBuildingId,
oldBuildingId,
});
isInitialized.value = true;
loadAllData();
} else {
console.log('activeBuildId 没有变化,跳过加载', {
newBuildingId,
oldBuildingId,
});
}
} else {
console.log('条件不满足,清空数据', {
newBuildingId,
currentProjectId: currentProjectId.value,
});
statisticsData.value = {};
roomList.value = [];
isInitialized.value = false;
}
},
{ immediate: false }
);
// 监听项目ID变化,重新获取数据
watch(
() => userStore.currentProjectId,
(newProjectId) => {
if (newProjectId) {
isInitialized.value = false; // 重置初始化标志
getTopBuildingTagsData();
} else {
topBuildList.value = [];
statisticsData.value = {};
roomList.value = [];
isInitialized.value = false;
}
}
);
// 组件挂载时获取数据
onMounted(() => {
if (userStore.currentProjectId) {
getTopBuildingTagsData();
} else {
// 如果没有选中的项目,尝试使用第一个项目
if (userStore.projects && userStore.projects.length > 0) {
userStore.setInfo({ currentProjectId: userStore.projects[0].id });
getTopBuildingTagsData();
} else {
Message.warning('请先选择项目');
}
}
});
</script>
<style scoped lang="less">
.house_view_wrap {
display: flex;
flex-direction: column;
height: 100%;
}
.house-view-scroll-container {
max-height: calc(100vh - 56px - 58px - 40px);
padding-bottom: 20px;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch; // iOS 平滑滚动
}
</style>
-3
View File
@@ -1,3 +0,0 @@
<template>
<div>tenant-contacts</div>
</template>
@@ -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>
@@ -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/tenants';
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>
@@ -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>
@@ -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>
+343
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?.filingName || '-' }}
</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/tenants';
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: 'filingName',
key: 'filingName',
slotName: 'filingType',
},
{
title: '信用评分',
dataIndex: 'creditScore',
key: 'creditScore',
slotName: 'creditScore',
},
{ title: '操作', key: 'operate', slotName: 'operate', width: 120 },
];
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>
-3
View File
@@ -1,3 +0,0 @@
<template>
<div>tenant-list</div>
</template>
+526
View File
@@ -0,0 +1,526 @@
<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 :model="filingForm" layout="vertical">
<a-form-item label="请选择归档原因" field="filingType" required>
<a-select
v-model="filingForm.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/tenants';
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 filingForm = ref<{ filingType?: number }>({ filingType: 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;
filingForm.value.filingType = undefined;
showFilingModal.value = true;
};
// 确认归档
const handleFilingConfirm = async () => {
if (!filingForm.value.filingType) {
Message.warning('请选择归档原因');
return;
}
if (!currentTenantId.value) {
Message.error('租客ID不存在');
return;
}
try {
const { code, message } = await filingTenant({
id: currentTenantId.value,
filingType: filingForm.value.filingType,
});
if (code === 200) {
Message.success(message || '归档成功');
showFilingModal.value = false;
filingForm.value.filingType = undefined;
currentTenantId.value = null;
// 重新加载列表数据
await getTenantListData();
} else {
Message.error(message || '归档失败');
}
} catch (error: any) {
Message.error(error.message || '归档失败');
}
};
// 取消归档
const handleFilingCancel = () => {
showFilingModal.value = false;
filingForm.value.filingType = 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>
-3
View File
@@ -1,3 +0,0 @@
<template>
<div>monitor</div>
</template>
@@ -1,48 +0,0 @@
export default {
'menu.dashboard.monitor': 'Real-time Monitor',
'monitor.title.chatPanel': 'Chat Window',
'monitor.title.quickOperation': 'Quick Operation',
'monitor.title.studioInfo': 'Studio Information',
'monitor.title.studioPreview': 'Studio Preview',
'monitor.chat.options.all': 'All',
'monitor.chat.placeholder.searchCategory': 'Search Category',
'monitor.chat.update': 'Update',
'monitor.list.title.order': 'Order',
'monitor.list.title.cover': 'Cover',
'monitor.list.title.name': 'Name',
'monitor.list.title.duration': 'Duration',
'monitor.list.title.id': 'ID',
'monitor.list.tip.rotations': 'Rotations ',
'monitor.list.tip.rest': ', The program list is not visible to viewers',
'monitor.list.tag.auditFailed': 'Audit Failed',
'monitor.tab.title.liveMethod': 'Live Method',
'monitor.tab.title.onlinePopulation': 'Online Population',
'monitor.liveMethod.normal': 'Normal Live',
'monitor.liveMethod.flowControl': 'Flow Control Live',
'monitor.liveMethod.video': 'Video Live',
'monitor.liveMethod.web': 'Web Live',
'monitor.editCarousel': 'Edit',
'monitor.startCarousel': 'Start',
'monitor.quickOperation.changeClarity': 'Change the Clarity',
'monitor.quickOperation.switchStream': 'Switch Stream',
'monitor.quickOperation.removeClarity': 'Remove the Clarity',
'monitor.quickOperation.pushFlowGasket': 'Push Flow Gasket',
'monitor.studioInfo.label.studioTitle': 'Studio Title',
'monitor.studioInfo.label.onlineNotification': 'Online Notification',
'monitor.studioInfo.label.studioCategory': 'Studio Category',
'monitor.studioInfo.placeholder.studioTitle': "'s Studio",
'monitor.studioInfo.btn.fresh': 'Fresh',
'monitor.studioStatus.title.studioStatus': 'Studio Status',
'monitor.studioStatus.title.pictureInfo': 'Picture Information',
'monitor.studioStatus.smooth': 'Smooth',
'monitor.studioStatus.frameRate': 'Frame',
'monitor.studioStatus.bitRate': 'Bit',
'monitor.studioStatus.mainstream': 'Main',
'monitor.studioStatus.hotStandby': 'Hot',
'monitor.studioStatus.coldStandby': 'Cold',
'monitor.studioStatus.line': 'Line',
'monitor.studioStatus.play': 'Format',
'monitor.studioStatus.pictureQuality': 'Quality',
'monitor.studioPreview.studio': 'Studio',
'monitor.studioPreview.watching': 'watching',
};
@@ -1,48 +0,0 @@
export default {
'menu.dashboard.monitor': '实时监控',
'monitor.title.chatPanel': '聊天窗口',
'monitor.title.quickOperation': '快捷操作',
'monitor.title.studioInfo': '直播信息',
'monitor.title.studioPreview': '直播预览',
'monitor.chat.options.all': '全部',
'monitor.chat.placeholder.searchCategory': '搜索类目',
'monitor.chat.update': '更新',
'monitor.list.title.order': '序号',
'monitor.list.title.cover': '封面',
'monitor.list.title.name': '名称',
'monitor.list.title.duration': '视频时长',
'monitor.list.title.id': '视频Id',
'monitor.list.tip.rotations': '轮播次数',
'monitor.list.tip.rest': ',节目单观众不可见',
'monitor.list.tag.auditFailed': '审核未通过',
'monitor.tab.title.liveMethod': '直播方式',
'monitor.tab.title.onlinePopulation': '在线人数',
'monitor.liveMethod.normal': '普通直播',
'monitor.liveMethod.flowControl': '控流直播',
'monitor.liveMethod.video': '视频直播',
'monitor.liveMethod.web': '网页开播',
'monitor.editCarousel': '编辑轮播',
'monitor.startCarousel': '开始轮播',
'monitor.quickOperation.changeClarity': '切换清晰度',
'monitor.quickOperation.switchStream': '主备流切换',
'monitor.quickOperation.removeClarity': '摘除清晰度',
'monitor.quickOperation.pushFlowGasket': '推流垫片',
'monitor.studioInfo.label.studioTitle': '直播标题',
'monitor.studioInfo.label.onlineNotification': '上线通知',
'monitor.studioInfo.label.studioCategory': '直播类目',
'monitor.studioInfo.placeholder.studioTitle': '的直播间',
'monitor.studioInfo.btn.fresh': '更新',
'monitor.studioStatus.title.studioStatus': '直播状态',
'monitor.studioStatus.title.pictureInfo': '画面信息',
'monitor.studioStatus.smooth': '流畅',
'monitor.studioStatus.frameRate': '帧率',
'monitor.studioStatus.bitRate': '码率',
'monitor.studioStatus.mainstream': '主流',
'monitor.studioStatus.hotStandby': '热备',
'monitor.studioStatus.coldStandby': '冷备',
'monitor.studioStatus.line': '线路',
'monitor.studioStatus.play': '播放格式',
'monitor.studioStatus.pictureQuality': '画质',
'monitor.studioPreview.studio': '直播间',
'monitor.studioPreview.watching': '在看',
};
-3
View File
@@ -1,3 +0,0 @@
<template>
<div>workplace</div>
</template>
@@ -1,38 +0,0 @@
export default {
'menu.dashboard.workplace': 'Workplace',
'workplace.welcome': 'Welcome!',
'workplace.balance': 'Balance (CNY)',
'workplace.order.pending': 'Pending',
'workplace.order.pendingRenewal': 'Renewal Order',
'workplace.onlineContent': 'Online Content',
'workplace.putIn': 'Put In',
'workplace.newDay': 'Daily Additional Comments',
'workplace.newFromYesterday': 'New From Yesterday',
'workplace.minute': 'Min',
'workplace.docs': 'Documents',
'workplace.docs.productOverview': 'Product Overview',
'workplace.docs.userGuide': 'User Guide',
'workplace.docs.workflow': 'Workflow',
'workplace.docs.interfaceDocs': 'Interface Docs',
//
'workplace.contentManagement': 'Content Management',
'workplace.contentStatistical': 'Content Statistical',
'workplace.advanced': 'Advanced',
'workplace.onlinePromotion': 'Online Promotion',
'workplace.contentPutIn': 'Put In',
'workplace.announcement': 'Announcement',
'workplace.recently.visited': 'Recently Visited',
'workplace.record.nodata': 'No data',
'workplace.quick.operation': 'Quick Operation',
'workplace.quickOperation.setup': 'Setup',
'workplace.allProject': 'All',
'workplace.loadMore': 'More',
'workplace.viewMore': 'More',
'workplace.contentData': 'Content Data',
'workplace.popularContent': 'Popular Content',
'workplace.popularContent.text': 'text',
'workplace.popularContent.image': 'image',
'workplace.popularContent.video': 'video',
'workplace.categoriesPercent': 'Categories Percent',
'workplace.pecs': 'pecs',
};
@@ -1,37 +0,0 @@
export default {
'menu.dashboard.workplace': '工作台',
'workplace.welcome': '欢迎回来!',
'workplace.balance': '余额(元)',
'workplace.order.pending': '待支付',
'workplace.order.pendingRenewal': '待续费订单',
'workplace.onlineContent': '线上总内容',
'workplace.putIn': '投放中内容',
'workplace.newDay': '日新增评论',
'workplace.newFromYesterday': '较昨日新增',
'workplace.minute': '分钟',
'workplace.docs': '帮助文档',
'workplace.docs.productOverview': '产品概要',
'workplace.docs.userGuide': '使用指南',
'workplace.docs.workflow': '接入流程',
'workplace.docs.interfaceDocs': '接口文档',
'workplace.contentManagement': '内容管理',
'workplace.contentStatistical': '内容分析',
'workplace.advanced': '高级管理',
'workplace.onlinePromotion': '线上推广',
'workplace.contentPutIn': '内容投放',
'workplace.announcement': '公告',
'workplace.recently.visited': '最近访问',
'workplace.record.nodata': '暂无数据',
'workplace.quick.operation': '快捷操作',
'workplace.quickOperation.setup': '管理',
'workplace.allProject': '所有项目',
'workplace.loadMore': '加载更多',
'workplace.viewMore': '查看更多',
'workplace.contentData': '内容数据',
'workplace.popularContent': '线上热门内容',
'workplace.popularContent.text': '文本',
'workplace.popularContent.image': '图片',
'workplace.popularContent.video': '视频',
'workplace.categoriesPercent': '内容类型占比',
'workplace.pecs': '个',
};
-44
View File
@@ -1,44 +0,0 @@
<template>
<div class="error-container">
<a-result
status="error"
title="操作失败"
sub-title="很抱歉,您的操作未能完成,请检查并重试"
>
<template #extra>
<a-space>
<a-button key="retry" type="primary">重试</a-button>
<a-button key="back" @click="back">返回上一页</a-button>
</a-space>
</template>
</a-result>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useRouter } from 'vue-router';
export default defineComponent({
name: 'ResultError',
setup() {
const router = useRouter();
const back = () => {
router.back();
};
return {
back,
};
},
});
</script>
<style scoped lang="less">
.error-container {
padding: 24px;
background-color: var(--color-bg-2);
height: 100%;
}
</style>
-51
View File
@@ -1,51 +0,0 @@
<template>
<div class="success-container">
<a-result
status="success"
title="操作成功"
sub-title="操作已成功完成,您可以继续其他操作或返回首页"
>
<template #extra>
<a-space>
<a-button key="home" type="primary" @click="toHome"
>返回首页</a-button
>
<a-button key="back" @click="back">返回上一页</a-button>
</a-space>
</template>
</a-result>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useRouter } from 'vue-router';
export default defineComponent({
name: 'ResultSuccess',
setup() {
const router = useRouter();
const toHome = () => {
router.push('/');
};
const back = () => {
router.back();
};
return {
toHome,
back,
};
},
});
</script>
<style scoped lang="less">
.success-container {
padding: 24px;
background-color: var(--color-bg-2);
height: 100%;
}
</style>
@@ -3,7 +3,7 @@
<Breadcrumb <Breadcrumb
:items="['menu.setting', 'menu.setting.system', 'setting.contract']" :items="['menu.setting', 'menu.setting.system', 'setting.contract']"
/> />
<a-card class="full_height"> <a-card>
<a-row :gutter="24"> <a-row :gutter="24">
<a-col <a-col
v-if="contractFileInfo && Object.keys(contractFileInfo).length" v-if="contractFileInfo && Object.keys(contractFileInfo).length"
@@ -13,18 +13,84 @@
<div <div
class="mt-[24px] rounded-[6px] border-solid border-[#efefef] border-[1px] px-[12px] py-[20px]" class="mt-[24px] rounded-[6px] border-solid border-[#efefef] border-[1px] px-[12px] py-[20px]"
> >
<h4 class="text-[16px] font-bold">{{ contractFileInfo?.name }}</h4> <div class="flex items-center">
<p class="text-[16px] font-bold mr-[10px]">{{
contractFileInfo?.name
}}</p>
<p class="cursor-pointer px-[5px]" @click="openEditModal"
><icon-edit
/></p>
</div>
<p class="text-[14px] mt-[12px]" <p class="text-[14px] mt-[12px]"
><span class="mr-[5px] text-[#101010]">适用范围</span ><span class="mr-[5px] text-[#101010]">适用范围</span
>立思广州分公司</p >{{ departmentName }}</p
> >
<p class="text-[14px] mt-[4px]" <p class="text-[14px] mt-[4px]"
><span class="mr-[5px] text-[#101010]">适用项目</span ><span class="mr-[5px] text-[#101010]">适用项目</span
>星河商务中心</p >{{ projectNames }}</p
> >
</div> </div>
<a-modal
v-model:visible="editModalVisible"
title="编辑合同文档模板信息"
unmount-on-close
@ok="confirmEditModal"
@cancel="editModalVisible = false"
>
<a-form
ref="editFormRef"
:model="editForm"
:rules="editFormRules"
layout="vertical"
auto-label-width
>
<a-form-item label="模板名称" field="name" required>
<a-input
v-model="editForm.name"
placeholder="请输入合同模板名称"
/>
</a-form-item>
<a-form-item label="适用范围" field="departmentId" required>
<a-tree-select
v-model="editForm.departmentId"
:data="departmentListData"
:field-names="{
key: 'id',
title: 'name',
children: 'children',
}"
placeholder="请选择适用范围"
allow-clear
@change="onEditDepartmentChange"
/>
</a-form-item>
<a-form-item label="适用项目" field="projects" required>
<a-select
v-model="editForm.projects"
multiple
:disabled="!editForm.departmentId"
placeholder="请选择适用项目"
>
<a-option
v-for="item in editDeptProjectList"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</a-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
<div class="flex items-center mt-[24px]"> <div class="flex items-center mt-[24px]">
<a-input-search class="w-[256px] h-[30px]" placeholder="搜索名称" /> <a-input-search
v-model="contractPlaceholderListParams.keyword"
class="w-[256px] h-[30px]"
placeholder="搜索名称"
allow-clear
@search="handlePlaceholderSearch"
@press-enter="handlePlaceholderSearch"
/>
<div <div
class="flex items-center border-[1px] border-solid border-[#165DFF] px-[4px] py-[6px] ml-[10px]" class="flex items-center border-[1px] border-solid border-[#165DFF] px-[4px] py-[6px] ml-[10px]"
> >
@@ -32,20 +98,51 @@
<p class="ml-[5px] text-[#165DFF]">AI助手</p> <p class="ml-[5px] text-[#165DFF]">AI助手</p>
</div> </div>
</div> </div>
<a-tabs default-active-key="1" class="mt-[10px]"> <a-tabs
<a-tab-pane key="1" title="租赁条款"> 租赁条款 </a-tab-pane> v-model:active-key="contractPlaceholderListParams.type"
<a-tab-pane key="2" title="物业条款"> w物业条款 </a-tab-pane> class="mt-[10px]"
>
<a-tab-pane
v-for="tab in placeholderTabs"
:key="tab.key"
class="cursor-pointer"
:title="tab.title"
>
<a-descriptions bordered :column="1">
<a-descriptions-item
v-for="item in contractPlaceholderList"
:key="item.id"
:label="item.name"
>
{{ item.keyword }}
</a-descriptions-item>
</a-descriptions>
</a-tab-pane>
</a-tabs> </a-tabs>
</a-col> </a-col>
<a-col :span="17"> <a-col :span="17">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<a-button> <FileUpload
<template #icon> v-model:object-keys="uploadObjectKeys"
<icon-upload /> v-model:signed-put-u-r-l="uploadSignedPutURL"
:max-count="1"
:upload-type="1"
accept=".doc,.docx"
:show-file-list="false"
:disabled="!contractFileInfo?.id"
@upload-success="onUploadSuccess"
@upload-error="onUploadError"
>
<template #trigger>
<a-button :disabled="!contractFileInfo?.id">
<template #icon>
<icon-upload />
</template>
上传文档
</a-button>
</template> </template>
上传文档 </FileUpload>
</a-button>
<a-button class="ml-[10px]"> <a-button class="ml-[10px]">
<template #icon> <template #icon>
<icon-cloud-download /> <icon-cloud-download />
@@ -54,8 +151,8 @@
</a-button> </a-button>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<p>2025-09-10 00:25:30更新</p> <p>{{ contractFileInfo?.updatedAt }}更新</p>
<a-button type="primary" class="ml-[10px]"> <a-button type="primary" class="ml-[10px]" @click="handleSave">
<template #icon> <template #icon>
<icon-storage /> <icon-storage />
</template> </template>
@@ -65,14 +162,28 @@
</div> </div>
<!-- OnlyOffice 文档编辑器 --> <!-- OnlyOffice 文档编辑器 -->
<div class="onlyoffice-wrapper"> <div class="onlyoffice-wrapper">
<!-- <OnlyOfficeEditor v-if="contractFileInfo?.url" :document-url="contractFileInfo.url" <OnlyOfficeEditor
:document-title="contractFileInfo.name || '合同文档'" v-if="contractFileInfo?.url"
:document-type="getDocumentType(contractFileInfo.url)" :height="'600px'" :editable="true" :document-url="contractFileInfo.url"
:mode="'edit'" @ready="onEditorReady" @error="onEditorError" @save="onDocumentSave" :document-title="contractFileInfo?.name || '合同文档'"
@close="onEditorClose" /> :document-type="getDocumentType(contractFileInfo.url)"
<div v-else class="no-document"> height="600px"
<a-empty description="暂无文档内容" /> :editable="true"
</div> --> mode="edit"
@ready="onEditorReady"
@error="onEditorError"
@save="onDocumentSave"
@close="onEditorClose"
/>
<div v-else class="no-document">
<a-empty
:description="
contractFileInfo
? '暂无文档内容,请上传文档'
: '请先选择左侧合同模板'
"
/>
</div>
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
@@ -80,13 +191,136 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref, reactive, computed, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { getWordSettingFromId } from '@/api/setting'; import type { FormInstance } from '@arco-design/web-vue';
import { CreateFileSettingParam } from '@/types/setting'; import {
getWordSettingFromId,
getContractPlaceholderList,
updateWordSetting,
} from '@/api/contracts';
import { getDepartmentList } from '@/api/department';
import { getDeptProjectListFromDeptId } from '@/api/common';
import { CreateFileSettingParam, DepartmentItem } from '@/types/setting';
import FileUpload from '@/components/common/FileUpload.vue';
const contractFileInfo = ref<CreateFileSettingParam | null>(null); const contractFileInfo = ref<CreateFileSettingParam | null>(null);
const contractPlaceholderListParams = ref({ keyword: '', type: 1 });
const placeholderTabs = [
{ key: 1, title: '租赁条款' },
{ key: 2, title: '物业条款' },
];
const contractPlaceholderList = ref<any[]>([]);
const placeholderListLoading = ref(false);
// 占位符列表按 tab type 缓存:1 租赁条款,2 物业条款
const placeholderListCache = ref<Record<number, any[]>>({});
const uploadObjectKeys = ref<string[]>([]);
const uploadSignedPutURL = ref<string[]>([]);
// 编辑弹窗:仅改本地,不请求接口
const editModalVisible = ref(false);
const editFormRef = ref<FormInstance>();
const departmentListData = ref<DepartmentItem[]>([]);
const editDeptProjectList = ref<{ id: number; name: string }[]>([]);
const editForm = reactive<{
name: string;
departmentId: number | null;
projects: number[];
}>({
name: '',
departmentId: null,
projects: [],
});
const editFormRules = {
name: [{ required: true, message: '请输入合同模板名称' }],
departmentId: [{ required: true, message: '请选择适用范围' }],
projects: [
{ required: true, message: '请选择适用项目', type: 'array', min: 1 },
],
};
const departmentName = computed(() => {
if (!contractFileInfo.value?.departmentId) return '-';
const find = (list: DepartmentItem[], id: number): string | undefined => {
for (const item of list) {
if (item.id === id) return item.name;
if (item.children?.length) {
const n = find(item.children, id);
if (n) return n;
}
}
return undefined;
};
return (
find(departmentListData.value, contractFileInfo.value.departmentId) ?? '-'
);
});
// 适用项目展示:有则用顿号拼接名称,无则显示 -
const projectNames = computed(() => {
const list = contractFileInfo.value?.projects;
if (!list || !Array.isArray(list) || list.length === 0) return '-';
const names = list
.map((i: any) => (i?.name != null ? String(i.name).trim() : ''))
.filter(Boolean);
return names.length > 0 ? names.join('、') : '-';
});
const openEditModal = () => {
if (!contractFileInfo.value) return;
editForm.name = contractFileInfo.value.name ?? '';
editForm.departmentId = contractFileInfo.value.departmentId ?? null;
editForm.projects = (contractFileInfo.value.projects || []).map((p: any) =>
typeof p === 'object' && p?.id != null ? p.id : p
);
editModalVisible.value = true;
if (editForm.departmentId) {
getDeptProjectListFromDeptId(editForm.departmentId).then(
({ code, data }) => {
if (code === 200)
editDeptProjectList.value = data?.deptProjectList ?? [];
}
);
} else {
editDeptProjectList.value = [];
}
};
const onEditDepartmentChange = (id: number | undefined) => {
editForm.projects = [];
if (!id) {
editDeptProjectList.value = [];
return;
}
getDeptProjectListFromDeptId(id).then(({ code, data }) => {
if (code === 200) editDeptProjectList.value = data?.deptProjectList ?? [];
});
};
const confirmEditModal = async () => {
try {
await editFormRef.value?.validate();
} catch {
return false; // 校验失败时不关闭弹窗
}
if (!contractFileInfo.value) return false;
contractFileInfo.value.name = editForm.name.trim();
contractFileInfo.value.departmentId = editForm.departmentId;
contractFileInfo.value.projects = editForm.projects.map((id) => ({
id,
name: editDeptProjectList.value.find((p) => p.id === id)?.name ?? '',
})) as any;
editModalVisible.value = false;
Message.success('请点击保存按钮提交更新');
};
const getDepartmentListData = async () => {
const { code, data } = await getDepartmentList();
if (code === 200) departmentListData.value = data?.departmentList ?? [];
};
const route = useRoute(); const route = useRoute();
// 获取文档数据 // 获取文档数据
@@ -96,16 +330,17 @@
if (code === 200 && data) { if (code === 200 && data) {
const { contractWordSetting } = data; const { contractWordSetting } = data;
contractFileInfo.value = contractWordSetting || null; contractFileInfo.value = contractWordSetting || null;
contractFileInfo.value.url = // contractFileInfo.value.url =
'http://oss.kola.work/documents%2F20250910%2F%E5%90%88%E5%90%8C%E6%A8%A1%E7%89%88.docx?Expires=1757479342&OSSAccessKeyId=LTAI5tDDNxXnZq7QcpZEf6hR&Signature=ydBzbF2yCyRWZ0G52s%2FPr%2BFIUzo%3D'; // 'http://oss.kola.work/documents%2F20250910%2F%E5%90%88%E5%90%8C%E6%A8%A1%E7%89%88.docx?Expires=1757479342&OSSAccessKeyId=LTAI5tDDNxXnZq7QcpZEf6hR&Signature=ydBzbF2yCyRWZ0G52s%2FPr%2BFIUzo%3D';
} }
}; };
// 根据文件 URL 获取文档类型 // 根据文件 URL 获取文档类型(兼容带 ? 的签名链接)
const getDocumentType = (url: string): 'word' | 'excel' | 'powerpoint' => { const getDocumentType = (url: string): 'word' | 'excel' | 'powerpoint' => {
if (!url) return 'word'; if (!url) return 'word';
const extension = url.toLowerCase().split('.').pop(); const pathWithoutQuery = url.split('?')[0];
const extension = pathWithoutQuery.toLowerCase().split('.').pop() || '';
switch (extension) { switch (extension) {
case 'xlsx': case 'xlsx':
case 'xls': case 'xls':
@@ -142,8 +377,92 @@
Message.info('编辑器已关闭'); Message.info('编辑器已关闭');
}; };
// 上传成功:仅更新本地 url,不请求接口(统一由保存按钮提交)
const onUploadSuccess = (payload: {
objectKey: string;
signedPutURL: string;
}) => {
if (!contractFileInfo.value?.id) return;
contractFileInfo.value.url = payload.signedPutURL;
Message.success('文档已上传,请点击保存按钮提交到服务器');
};
const onUploadError = () => {
Message.error('上传失败,请重试');
};
// 占位符搜索:按当前关键词重新拉取列表
const handlePlaceholderSearch = () => {
getContractPlaceholderListData();
};
// 获取合同占位符列表(type: 1 租赁,2 物业),请求成功后写入缓存
const getContractPlaceholderListData = async () => {
const { type, keyword } = contractPlaceholderListParams.value;
placeholderListLoading.value = true;
try {
const { code, data } = await getContractPlaceholderList({
type,
keyword,
});
const list = code === 200 ? data?.placeholderList ?? [] : [];
contractPlaceholderList.value = list;
placeholderListCache.value[type] = [...list];
} catch (e) {
Message.error('获取占位符列表失败');
contractPlaceholderList.value = [];
} finally {
placeholderListLoading.value = false;
}
};
// 切换 tab:有缓存则用缓存,无则请求并写入缓存(immediate 负责首屏请求)
watch(
() => contractPlaceholderListParams.value.type,
(newType) => {
const cached = placeholderListCache.value[newType];
if (cached !== undefined) {
contractPlaceholderList.value = cached;
} else {
getContractPlaceholderListData();
}
},
{ immediate: true }
);
// 保存按钮:统一调用更新接口
const handleSave = async () => {
if (!contractFileInfo.value?.id) {
Message.warning('请先选择合同模板');
return;
}
const departmentId = contractFileInfo.value.departmentId ?? 0;
const projects = (contractFileInfo.value.projects || []).map((p: any) =>
typeof p === 'object' && p?.id != null ? p.id : p
);
const url = contractFileInfo.value.url ?? '';
const params = {
id: contractFileInfo.value.id,
name: contractFileInfo.value.name,
departmentId,
projects,
url,
};
try {
const { code, message } = await updateWordSetting(params);
if (code === 200) {
Message.success(message || '保存成功');
} else {
Message.error(message || '保存失败');
}
} catch (e) {
Message.error('保存失败');
}
};
onMounted(() => { onMounted(() => {
getWordSettingFromIdData(); getWordSettingFromIdData();
getDepartmentListData();
}); });
</script> </script>
@@ -152,7 +471,6 @@
margin-top: 16px; margin-top: 16px;
border: 1px solid #e5e6eb; border: 1px solid #e5e6eb;
border-radius: 6px; border-radius: 6px;
overflow: hidden;
background: #fff; background: #fff;
} }
@@ -162,28 +480,7 @@
background: #f7f8fa; background: #f7f8fa;
} }
.system-setting-contract-file { /* .system-setting-contract-file {
height: 100vh; height: 100vh;
overflow: hidden; } */
}
.full_height {
height: calc(100vh - 120px);
overflow: hidden;
}
:deep(.arco-card-body) {
height: 100%;
padding: 20px;
overflow: hidden;
}
:deep(.arco-row) {
height: 100%;
}
:deep(.arco-col) {
height: 100%;
overflow: hidden;
}
</style> </style>
@@ -64,8 +64,8 @@
delWordSetting, delWordSetting,
getWordSettingList, getWordSettingList,
disableWordSetting, disableWordSetting,
getDepartmentList, } from '@/api/contracts';
} from '@/api/setting'; import { getDepartmentList } from '@/api/department';
import { ContractWordItem, DepartmentItem } from '@/types/setting'; import { ContractWordItem, DepartmentItem } from '@/types/setting';
import { UserStatus } from '@/types/comm'; import { UserStatus } from '@/types/comm';
import { camelToSnake } from '@/utils/caseConvert'; import { camelToSnake } from '@/utils/caseConvert';
@@ -65,7 +65,7 @@
getCodeSettingFromId, getCodeSettingFromId,
disableCodeSetting, disableCodeSetting,
delCodeSetting, delCodeSetting,
} from '@/api/setting'; } from '@/api/contracts';
import { ContractTermInfo } from '@/types/setting'; import { ContractTermInfo } from '@/types/setting';
import { UserStatus } from '@/types/comm'; import { UserStatus } from '@/types/comm';
import { camelToSnake } from '@/utils/caseConvert'; import { camelToSnake } from '@/utils/caseConvert';
@@ -256,13 +256,13 @@
import { import {
getTemplateSettingFromId, getTemplateSettingFromId,
getTermsSettingFromId, getTermsSettingFromId,
getDepartmentList,
getAllCodeSettingList, getAllCodeSettingList,
getContractTagsList, getContractTagsList,
updateTemplateSetting, updateTemplateSetting,
getAllTermsSettingList, getAllTermsSettingList,
getTermsDataFromId, getTermsDataFromId,
} from '@/api/setting'; } from '@/api/contracts';
import { getDepartmentList } from '@/api/department';
import contractTemplateForm from './modal/contract-template-form.vue'; import contractTemplateForm from './modal/contract-template-form.vue';
import { DepartmentItem } from '@/types/setting'; import { DepartmentItem } from '@/types/setting';
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
@@ -63,9 +63,9 @@
import { import {
delTemplateSetting, delTemplateSetting,
getTemplateSettingList, getTemplateSettingList,
getDepartmentList,
disableTemplateSetting, disableTemplateSetting,
} from '@/api/setting'; } from '@/api/contracts';
import { getDepartmentList } from '@/api/department';
import { ContractTemplateSettingItem, DepartmentItem } from '@/types/setting'; import { ContractTemplateSettingItem, DepartmentItem } from '@/types/setting';
import { UserStatus } from '@/types/comm'; import { UserStatus } from '@/types/comm';
import contractTemplateForm from './modal/contract-template-form.vue'; import contractTemplateForm from './modal/contract-template-form.vue';
@@ -65,7 +65,7 @@
getTermsSettingFromId, getTermsSettingFromId,
disableTermsSetting, disableTermsSetting,
delTermsSetting, delTermsSetting,
} from '@/api/setting'; } from '@/api/contracts';
import { TermSettingItem } from '@/types/setting'; import { TermSettingItem } from '@/types/setting';
import { UserStatus } from '@/types/comm'; import { UserStatus } from '@/types/comm';
import contractNumberFormDrawer from './drawer/contract-terms-form-drawer.vue'; import contractNumberFormDrawer from './drawer/contract-terms-form-drawer.vue';
@@ -105,7 +105,7 @@
import { ref, watch, reactive, computed } from 'vue'; import { ref, watch, reactive, computed } from 'vue';
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { createCodeSetting, updateCodeSetting } from '@/api/setting'; import { createCodeSetting, updateCodeSetting } from '@/api/contracts';
// import { CreateCodeSettingParam } from '@/types/setting' // import { CreateCodeSettingParam } from '@/types/setting'
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
@@ -102,11 +102,8 @@
import { ref, watch, reactive, onMounted } from 'vue'; import { ref, watch, reactive, onMounted } from 'vue';
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { import { createTermsSetting, updateTermsSetting } from '@/api/contracts';
createTermsSetting, import { getUseFeeList } from '@/api/finance-setting';
updateTermsSetting,
getUseFeeList,
} from '@/api/setting';
import { feeTypeList, financeFeeFeriodicityList } from '@/utils/constant'; import { feeTypeList, financeFeeFeriodicityList } from '@/utils/constant';
import { CreateTermSettingParam, FeeManagement } from '@/types/setting'; import { CreateTermSettingParam, FeeManagement } from '@/types/setting';
import { FeeManagementType } from '@/types/comm'; import { FeeManagementType } from '@/types/comm';
@@ -199,20 +199,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, onMounted, computed, watch } from 'vue'; import { ref, reactive, onMounted, computed, watch } from 'vue';
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
const priceUnits = [ import { PRICE_UNITS, PRICE_UNIT_MAP } from '@/constants';
{ value: 1, text: '元/㎡*月' },
{ value: 2, text: '元/㎡*天' }, const priceUnits = PRICE_UNITS;
{ value: 3, text: '元/天' }, const priceUnitMap = PRICE_UNIT_MAP;
{ value: 4, text: '元/月' },
{ value: 5, text: '元/年' },
];
const priceUnitMap = new Map([
[1, '元/㎡*月'],
[2, '元/㎡*天'],
[3, '元/天'],
[4, '元/月'],
[5, '元/年'],
]);
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean;
record?: any | null; record?: any | null;
@@ -259,7 +249,7 @@
emit('update:visible', false); emit('update:visible', false);
emit('submitted', form); emit('submitted', form);
} catch { } catch {
console.warn('❌ 校验未通过'); // 校验未通过
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@@ -64,7 +64,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, watch } from 'vue'; import { ref, reactive, watch } from 'vue';
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
import { createWordSetting, updateWordSetting } from '@/api/setting'; import { createWordSetting, updateWordSetting } from '@/api/contracts';
import { DepartmentItem } from '@/types/setting'; import { DepartmentItem } from '@/types/setting';
import { getDeptProjectListFromDeptId } from '@/api/common'; import { getDeptProjectListFromDeptId } from '@/api/common';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
@@ -34,7 +34,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, watch } from 'vue'; import { ref, reactive, watch } from 'vue';
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
import { createTemplateSetting, updateTemplateSetting } from '@/api/setting'; import {
createTemplateSetting,
updateTemplateSetting,
} from '@/api/contracts';
import { DepartmentItem, CreateTemplateSettingParam } from '@/types/setting'; import { DepartmentItem, CreateTemplateSettingParam } from '@/types/setting';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@@ -74,7 +74,7 @@
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
import rentSettingDrawer from '../drawer/rent-setting-drawer.vue'; import rentSettingDrawer from '../drawer/rent-setting-drawer.vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { getUseTurnoverAccountList } from '@/api/setting'; import { getUseTurnoverAccountList } from '@/api/finance-setting';
const isShowDrawer = ref<boolean>(false); const isShowDrawer = ref<boolean>(false);
// 与抽屉保持一致的默认表单,用于初始回显与编辑回填 // 与抽屉保持一致的默认表单,用于初始回显与编辑回填
const defaultForm: any = { const defaultForm: any = {
@@ -71,7 +71,7 @@
createContractTags, createContractTags,
updateContractTags, updateContractTags,
delContractTags, delContractTags,
} from '@/api/setting'; } from '@/api/dictionary';
import { ProjectTypeChecked } from '@/types/comm'; import { ProjectTypeChecked } from '@/types/comm';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
@@ -104,7 +104,7 @@
createProjectType, createProjectType,
updateProjectType, updateProjectType,
delProjectType, delProjectType,
} from '@/api/setting'; } from '@/api/dictionary';
import { projectTypeInfo, ProvinceItem } from '@/types/setting'; import { projectTypeInfo, ProvinceItem } from '@/types/setting';
import { ProjectTypeChecked } from '@/types/comm'; import { ProjectTypeChecked } from '@/types/comm';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
@@ -71,7 +71,7 @@
createRoomTag, createRoomTag,
updateRoomTag, updateRoomTag,
delRoomTag, delRoomTag,
} from '@/api/setting'; } from '@/api/dictionary';
import { projectTypeInfo } from '@/types/setting'; import { projectTypeInfo } from '@/types/setting';
import { ProjectTypeChecked } from '@/types/comm'; import { ProjectTypeChecked } from '@/types/comm';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
@@ -202,7 +202,7 @@
createTenantDictionary, createTenantDictionary,
updateTenantDictionary, updateTenantDictionary,
delTenantDictionary, delTenantDictionary,
} from '@/api/setting'; } from '@/api/dictionary';
import { ProjectTypeChecked, TenantDictionaryType } from '@/types/comm'; import { ProjectTypeChecked, TenantDictionaryType } from '@/types/comm';
import { projectTypeInfo } from '@/types/setting'; import { projectTypeInfo } from '@/types/setting';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
@@ -74,7 +74,7 @@
import { ref, watch, reactive } from 'vue'; import { ref, watch, reactive } from 'vue';
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { createFee, updateFee } from '@/api/setting'; import { createFee, updateFee } from '@/api/finance-setting';
import { CreateFeeParams } from '@/types/setting'; import { CreateFeeParams } from '@/types/setting';
import { feeTypeList } from '@/utils/constant'; import { feeTypeList } from '@/utils/constant';
@@ -43,7 +43,7 @@
import { onMounted, ref, watch } from 'vue'; import { onMounted, ref, watch } from 'vue';
import useDataTable from '@/composables/useDataTable'; import useDataTable from '@/composables/useDataTable';
import type { TableColumnData } from '@arco-design/web-vue'; import type { TableColumnData } from '@arco-design/web-vue';
import { getFeeList, disableFee, delFee } from '@/api/setting'; import { getFeeList, disableFee, delFee } from '@/api/finance-setting';
import { FeeManagement } from '@/types/setting'; import { FeeManagement } from '@/types/setting';
import { FeeType, UserStatus } from '@/types/comm'; import { FeeType, UserStatus } from '@/types/comm';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
@@ -32,7 +32,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { getFeeFromId } from '@/api/setting'; import { getFeeFromId } from '@/api/finance-setting';
import feeManagementTable from './fee-management-table.vue'; import feeManagementTable from './fee-management-table.vue';
import feeManagementDrawer from './drawer/fee-management-drawer.vue'; import feeManagementDrawer from './drawer/fee-management-drawer.vue';
@@ -67,7 +67,7 @@
import { ref, reactive, watch } from 'vue'; import { ref, reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
import { createDepartment, updateDepartment } from '@/api/setting'; import { createDepartment, updateDepartment } from '@/api/department';
import { DepartmentItem, UserList } from '@/types/setting'; import { DepartmentItem, UserList } from '@/types/setting';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
// Props & Emits // Props & Emits
@@ -129,15 +129,15 @@
import { ref, watch, onMounted } from 'vue'; import { ref, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import useDepartmentList from '@/composables/useDepartmentList'; import useDepartmentList from '@/composables/useDepartmentList';
import { getAllUserList } from '@/api/user';
import { import {
getAllUserList,
departmentSetUp, departmentSetUp,
departmentSetDown, departmentSetDown,
delDepartment, delDepartment,
saveDataPermission, saveDataPermission,
} from '@/api/setting'; } from '@/api/department';
import { DepartmentItem, UserList } from '@/types/setting'; import { DepartmentItem, UserList } from '@/types/setting';
import { getAllProjectList } from '@/api/business'; import { getAllProjectList } from '@/api/project';
import { debounce } from '@/utils'; import { debounce } from '@/utils';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import departmentFormModal from './components/department-form-modal.vue'; import departmentFormModal from './components/department-form-modal.vue';
@@ -62,7 +62,7 @@
import { ref, watch, reactive } from 'vue'; import { ref, watch, reactive } from 'vue';
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { createWorkflows } from '@/api/setting'; import { createWorkflows } from '@/api/workflows';
import { getAllProjectIdNameList, getWorkflowsTypeList } from '@/api/common'; import { getAllProjectIdNameList, getWorkflowsTypeList } from '@/api/common';
const allProjetList = ref<any>([]); const allProjetList = ref<any>([]);
const isLoading = ref<boolean>(false); const isLoading = ref<boolean>(false);
@@ -149,7 +149,7 @@
isLoading.value = false; isLoading.value = false;
} }
}; };
// 部门列表
async function getAllProjectListData() { async function getAllProjectListData() {
try { try {
const { code, data } = await getAllProjectIdNameList(); const { code, data } = await getAllProjectIdNameList();
@@ -215,7 +215,7 @@
saveWorkflows, saveWorkflows,
toggleWorkflowsDisable, toggleWorkflowsDisable,
delWorkflows, delWorkflows,
} from '@/api/setting'; } from '@/api/workflows';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import addApproval from './components/add-approval.vue'; import addApproval from './components/add-approval.vue';
import StartNode from './components/custom-nodes/StartNode.vue'; import StartNode from './components/custom-nodes/StartNode.vue';
@@ -37,7 +37,7 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { updateUserRole } from '@/api/setting'; import { updateUserRole } from '@/api/role';
import { UpdateUserRoleParam, RoleItem } from '@/types/setting'; import { UpdateUserRoleParam, RoleItem } from '@/types/setting';
const isLoading = ref<boolean>(false); const isLoading = ref<boolean>(false);
-268
View File
@@ -1,268 +0,0 @@
{
"permission": [
{
"system_tenant_permission_id": 100,
"tenant_id": 1,
"perm_id": 1,
"id": 1,
"parent_id": 0,
"prem_name": "业务",
"level": 1,
"perm_key": "business",
"prem_type": 1,
"path": "/1/",
"sort": 1,
"visible": 1,
"createdAt": "2024-06-20 10:00:00",
"updatedAt": "2024-06-20 10:00:00",
"children": [
{
"system_tenant_permission_id": 101,
"tenant_id": 1,
"perm_id": 2,
"id": 2,
"parent_id": 1,
"prem_name": "项目管理",
"level": 2,
"perm_key": "project_manage",
"prem_type": 1,
"path": "/1/2/",
"sort": 1,
"visible": 1,
"createdAt": "2024-06-20 10:01:00",
"updatedAt": "2024-06-20 10:01:00",
"children": [
{
"system_tenant_permission_id": 102,
"tenant_id": 1,
"perm_id": 3,
"id": 3,
"parent_id": 2,
"prem_name": "查看项目",
"level": 3,
"perm_key": "view_project",
"prem_type": 4,
"path": "/1/2/3/",
"sort": 1,
"visible": 1,
"createdAt": "2024-06-20 10:02:00",
"updatedAt": "2024-06-20 10:02:00",
"children": [
{
"system_tenant_permission_id": 103,
"tenant_id": 1,
"perm_id": 4,
"id": 4,
"parent_id": 3,
"prem_name": "添加收入目标",
"level": 4,
"perm_key": "add_income_target",
"prem_type": 2,
"path": "/1/2/3/4/",
"sort": 1,
"visible": 1,
"createdAt": "2024-06-20 10:03:00",
"updatedAt": "2024-06-20 10:03:00",
"children": []
},
{
"system_tenant_permission_id": 104,
"tenant_id": 1,
"perm_id": 5,
"id": 5,
"parent_id": 3,
"prem_name": "编辑收入目标",
"level": 4,
"perm_key": "edit_income_target",
"prem_type": 2,
"path": "/1/2/3/5/",
"sort": 2,
"visible": 1,
"createdAt": "2024-06-20 10:04:00",
"updatedAt": "2024-06-20 10:04:00",
"children": []
}
]
},
{
"system_tenant_permission_id": 105,
"tenant_id": 1,
"perm_id": 6,
"id": 6,
"parent_id": 2,
"prem_name": "编辑项目",
"level": 3,
"perm_key": "edit_project",
"prem_type": 4,
"path": "/1/2/6/",
"sort": 2,
"visible": 1,
"createdAt": "2024-06-20 10:05:00",
"updatedAt": "2024-06-20 10:05:00",
"children": [
{
"system_tenant_permission_id": 106,
"tenant_id": 1,
"perm_id": 7,
"id": 7,
"parent_id": 6,
"prem_name": "删除收入目标",
"level": 4,
"perm_key": "delete_income_target",
"prem_type": 2,
"path": "/1/2/6/7/",
"sort": 1,
"visible": 1,
"createdAt": "2024-06-20 10:06:00",
"updatedAt": "2024-06-20 10:06:00",
"children": []
}
]
}
]
},
{
"system_tenant_permission_id": 107,
"tenant_id": 1,
"perm_id": 8,
"id": 8,
"parent_id": 1,
"prem_name": "合同管理",
"level": 2,
"perm_key": "contract_manage",
"prem_type": 1,
"path": "/1/8/",
"sort": 2,
"visible": 1,
"createdAt": "2024-06-20 10:07:00",
"updatedAt": "2024-06-20 10:07:00",
"children": [
{
"system_tenant_permission_id": 108,
"tenant_id": 1,
"perm_id": 9,
"id": 9,
"parent_id": 8,
"prem_name": "查看合同",
"level": 3,
"perm_key": "view_contract",
"prem_type": 4,
"path": "/1/8/9/",
"sort": 1,
"visible": 1,
"createdAt": "2024-06-20 10:08:00",
"updatedAt": "2024-06-20 10:08:00",
"children": [
{
"system_tenant_permission_id": 109,
"tenant_id": 1,
"perm_id": 10,
"id": 10,
"parent_id": 9,
"prem_name": "导出合同",
"level": 4,
"perm_key": "export_contract",
"prem_type": 2,
"path": "/1/8/9/10/",
"sort": 1,
"visible": 1,
"createdAt": "2024-06-20 10:09:00",
"updatedAt": "2024-06-20 10:09:00",
"children": []
}
]
}
]
}
]
},
{
"system_tenant_permission_id": 200,
"tenant_id": 1,
"perm_id": 20,
"id": 20,
"parent_id": 0,
"prem_name": "房源视图",
"level": 1,
"perm_key": "house_view",
"prem_type": 1,
"path": "/20/",
"sort": 2,
"visible": 1,
"createdAt": "2024-06-20 11:00:00",
"updatedAt": "2024-06-20 11:00:00",
"children": [
{
"system_tenant_permission_id": 201,
"tenant_id": 1,
"perm_id": 21,
"id": 21,
"parent_id": 20,
"prem_name": "新建房源",
"level": 2,
"perm_key": "add_house",
"prem_type": 2,
"path": "/20/21/",
"sort": 1,
"visible": 1,
"createdAt": "2024-06-20 11:01:00",
"updatedAt": "2024-06-20 11:01:00",
"children": []
},
{
"system_tenant_permission_id": 202,
"tenant_id": 1,
"perm_id": 22,
"id": 22,
"parent_id": 20,
"prem_name": "导入房源",
"level": 2,
"perm_key": "import_house",
"prem_type": 3,
"path": "/20/22/",
"sort": 2,
"visible": 1,
"createdAt": "2024-06-20 11:02:00",
"updatedAt": "2024-06-20 11:02:00",
"children": [
{
"system_tenant_permission_id": 203,
"tenant_id": 1,
"perm_id": 23,
"id": 23,
"parent_id": 22,
"prem_name": "批量导入",
"level": 3,
"perm_key": "batch_import",
"prem_type": 3,
"path": "/20/22/23/",
"sort": 1,
"visible": 1,
"createdAt": "2024-06-20 11:03:00",
"updatedAt": "2024-06-20 11:03:00",
"children": [
{
"system_tenant_permission_id": 204,
"tenant_id": 1,
"perm_id": 24,
"id": 24,
"parent_id": 23,
"prem_name": "导入校验",
"level": 4,
"perm_key": "import_check",
"prem_type": 2,
"path": "/20/22/23/24/",
"sort": 1,
"visible": 1,
"createdAt": "2024-06-20 11:04:00",
"updatedAt": "2024-06-20 11:04:00",
"children": []
}
]
}
]
}
]
}
]
}
@@ -33,7 +33,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { getRolePermission } from '@/api/setting'; import { getRolePermission } from '@/api/role';
import PermissionNode from './permission-node.vue'; import PermissionNode from './permission-node.vue';
const props = defineProps<{ const props = defineProps<{
@@ -37,7 +37,7 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { updateRole, createRole } from '@/api/setting'; import { updateRole, createRole } from '@/api/role';
import { RoleFormParam, RoleItem } from '@/types/setting'; import { RoleFormParam, RoleItem } from '@/types/setting';
const isLoading = ref<boolean>(false); const isLoading = ref<boolean>(false);
@@ -111,7 +111,7 @@
import { ref, watch, reactive } from 'vue'; import { ref, watch, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
import { createUser, updateUser } from '@/api/setting'; import { createUser, updateUser } from '@/api/user';
import { DepartmentItem, RoleItem, FormUserItem } from '@/types/setting'; import { DepartmentItem, RoleItem, FormUserItem } from '@/types/setting';
const isLoading = ref<boolean>(false); const isLoading = ref<boolean>(false);
+3 -8
View File
@@ -119,13 +119,8 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import type { TableColumnData } from '@arco-design/web-vue'; import type { TableColumnData } from '@arco-design/web-vue';
import { import { getUserList, getUserById, disableUser, delUser } from '@/api/user';
getUserList, import { getRoleList } from '@/api/role';
getRoleList,
getUserInfo,
disableUser,
delUser,
} from '@/api/setting';
import { UserList, RoleItem, FormUserItem } from '@/types/setting'; import { UserList, RoleItem, FormUserItem } from '@/types/setting';
import { UserStatus } from '@/types/comm'; import { UserStatus } from '@/types/comm';
import useDepartmentList from '@/composables/useDepartmentList'; import useDepartmentList from '@/composables/useDepartmentList';
@@ -310,7 +305,7 @@
}; };
const getUserInfoData = async (id: number) => { const getUserInfoData = async (id: number) => {
try { try {
const { code, data } = await getUserInfo(id); const { code, data } = await getUserById(id);
if (code === 200) userInfo.value = data?.user; if (code === 200) userInfo.value = data?.user;
} catch (error) { } catch (error) {
// 处理错误 // 处理错误

Some files were not shown because too many files have changed in this diff Show More