feat: 完成空间管理模块
This commit is contained in:
+11
-3
@@ -4,6 +4,7 @@ import {
|
|||||||
ProjectListRes,
|
ProjectListRes,
|
||||||
CreateProjectParam,
|
CreateProjectParam,
|
||||||
ProjectDetailRes,
|
ProjectDetailRes,
|
||||||
|
TopBuildingTagsRes,
|
||||||
} from '@/types/business';
|
} from '@/types/business';
|
||||||
import {
|
import {
|
||||||
ProjectPaginationParams,
|
ProjectPaginationParams,
|
||||||
@@ -50,8 +51,11 @@ 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}`);
|
||||||
|
}
|
||||||
|
// ------------- 空间管理 -------------
|
||||||
// 创建房源
|
// 创建房源
|
||||||
export function createRoom(params: any): Promise<HttpResponse> {
|
export function createRoom(params: any): Promise<HttpResponse> {
|
||||||
return axios.post('rooms/createRoom', params);
|
return axios.post('rooms/createRoom', params);
|
||||||
@@ -79,7 +83,7 @@ export function changeRoomStatus(params: any): Promise<HttpResponse> {
|
|||||||
// 房源获取头部楼栋tags
|
// 房源获取头部楼栋tags
|
||||||
export function getTopBuildingTagsByProjectId(
|
export function getTopBuildingTagsByProjectId(
|
||||||
id: number
|
id: number
|
||||||
): Promise<HttpResponse> {
|
): Promise<TopBuildingTagsRes> {
|
||||||
return axios.get(`roomOver/getTopBuildingTags?project_id=${id}`);
|
return axios.get(`roomOver/getTopBuildingTags?project_id=${id}`);
|
||||||
}
|
}
|
||||||
// 房源剖视-数据统计
|
// 房源剖视-数据统计
|
||||||
@@ -90,3 +94,7 @@ export function getStatisticsData(params: any): Promise<HttpResponse> {
|
|||||||
export function getRoomOverList(params: any): Promise<HttpResponse> {
|
export function getRoomOverList(params: any): Promise<HttpResponse> {
|
||||||
return axios.post('roomOver/getRoomList', params);
|
return axios.post('roomOver/getRoomList', params);
|
||||||
}
|
}
|
||||||
|
// 房源图谱列表
|
||||||
|
export function getRoomsGraphList(params: any): Promise<HttpResponse> {
|
||||||
|
return axios.post('roomsGraph/getRoomList', params);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,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 +40,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);
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
|
||||||
@@ -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 接口。
|
|
||||||
@@ -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. 上传类型必须从父组件传入,用于区分不同的上传场景
|
|
||||||
@@ -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.
|
|
||||||
+167
-100
@@ -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 {
|
|
||||||
.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>
|
</style>
|
||||||
@@ -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` 适用于需要等待用户操作后再获取数据的场景
|
|
||||||
@@ -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,
|
||||||
|
|||||||
+1
-7
@@ -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';
|
||||||
@@ -47,6 +43,7 @@ export default {
|
|||||||
'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': '合同归档',
|
||||||
@@ -131,9 +128,6 @@ export default {
|
|||||||
...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;
|
|
||||||
@@ -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/index.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',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// 合同管理
|
// 合同管理
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; // 列表显示的招商面积
|
||||||
@@ -115,6 +116,17 @@ 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 {
|
export interface CreateHouseParam {
|
||||||
id?: number; // 编辑时需要
|
id?: number; // 编辑时需要
|
||||||
|
|||||||
@@ -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[]> = {
|
||||||
|
|||||||
@@ -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/setting';
|
||||||
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>
|
||||||
|
|||||||
@@ -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/business';
|
||||||
|
import { getAllProjectTypeList } from '@/api/setting';
|
||||||
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,
|
||||||
@@ -203,15 +223,36 @@
|
|||||||
// 处理错误
|
// 处理错误
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
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) => {
|
||||||
// TODO: 实现删除功能
|
const { code, message } = await delProject(id);
|
||||||
|
if (code === 200) {
|
||||||
|
Message.success(message);
|
||||||
|
getProjectListData();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// 分页事件处理
|
// 分页事件处理
|
||||||
const onPageChange = (page: number) => {
|
const onPageChange = (page: number) => {
|
||||||
@@ -244,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,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/business';
|
||||||
|
import { ProjectPaginationParams } from '@/types/comm';
|
||||||
|
import houseFormDrawer from '../drawer/house-form-drawer.vue';
|
||||||
|
import houseInfoDrawer from '../drawer/house-info-drawer.vue';
|
||||||
|
import houseListFilterDrawer from '../drawer/house-list-filter-drawer.vue';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
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>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
type="error"
|
type="error"
|
||||||
@cancel="handleDel(record?.id)"
|
@cancel="handleDel(record?.id)"
|
||||||
>
|
>
|
||||||
<a-button status="danger">删除项目</a-button>
|
<a-button status="danger">删除房源</a-button>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
@@ -132,6 +132,7 @@
|
|||||||
import houseInfoDrawer from '../drawer/house-info-drawer.vue';
|
import houseInfoDrawer from '../drawer/house-info-drawer.vue';
|
||||||
import houseListFilterDrawer from '../drawer/house-list-filter-drawer.vue';
|
import houseListFilterDrawer from '../drawer/house-list-filter-drawer.vue';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { PRICE_UNIT_MAP } from '@/constants';
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { loading } = useLoading(false);
|
const { loading } = useLoading(false);
|
||||||
const showDrawer = ref<boolean>(false);
|
const showDrawer = ref<boolean>(false);
|
||||||
@@ -201,7 +202,11 @@
|
|||||||
title: '租金(实时)',
|
title: '租金(实时)',
|
||||||
dataIndex: 'price',
|
dataIndex: 'price',
|
||||||
key: 'price',
|
key: 'price',
|
||||||
render: ({ record }) => (Number(record.price) <= 0 ? '-' : record?.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: 'tags', key: 'tags', slotName: 'tags' },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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/business';
|
||||||
|
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/setting';
|
||||||
|
|
||||||
|
// 特殊值用于表示"所有"选项(使用负数避免与真实ID冲突)
|
||||||
|
const ALL_BUILDING_ID = -1;
|
||||||
|
const ALL_FLOOR_ID = -2;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
roomList?: any[]; // 房源列表数据,用于提取筛选选项
|
||||||
|
projectName?: string; // 项目名称(仅显示)
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', val: boolean): void;
|
||||||
|
(
|
||||||
|
e: 'confirm',
|
||||||
|
payload: {
|
||||||
|
buildingIds: number[] | null;
|
||||||
|
floorIds: number[] | null;
|
||||||
|
statusIds: number[] | null;
|
||||||
|
rentArea: number[];
|
||||||
|
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>
|
||||||
@@ -1,3 +1,351 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>house-view</div>
|
<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>
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch, onMounted, computed } from 'vue';
|
||||||
|
import {
|
||||||
|
getTopBuildingTagsByProjectId,
|
||||||
|
getStatisticsData,
|
||||||
|
getRoomOverList,
|
||||||
|
} from '@/api/business';
|
||||||
|
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>
|
||||||
|
|||||||
@@ -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': '在看',
|
|
||||||
};
|
|
||||||
@@ -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': '个',
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container">
|
|
||||||
<a-card class="general-card" title="数据分析">
|
|
||||||
<a-row :gutter="16">
|
|
||||||
<a-col :span="24" :lg="16">
|
|
||||||
<!-- 主要图表区域 -->
|
|
||||||
<a-card class="content-card">
|
|
||||||
<template #title>销售趋势分析</template>
|
|
||||||
<div class="chart-wrapper">
|
|
||||||
<div ref="chartRef" class="chart"></div>
|
|
||||||
</div>
|
|
||||||
</a-card>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="24" :lg="8">
|
|
||||||
<!-- 数据统计卡片 -->
|
|
||||||
<a-space direction="vertical" fill style="width: 100%">
|
|
||||||
<a-card class="content-card">
|
|
||||||
<a-statistic
|
|
||||||
title="总销售额"
|
|
||||||
:value="12345678"
|
|
||||||
:precision="2"
|
|
||||||
show-group-separator
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<icon-thumb-up />
|
|
||||||
</template>
|
|
||||||
</a-statistic>
|
|
||||||
<div class="statistic-footer">
|
|
||||||
<div class="footer-item">
|
|
||||||
<span>日环比</span>
|
|
||||||
<span class="green">
|
|
||||||
<icon-arrow-rise />
|
|
||||||
10.5%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="footer-item">
|
|
||||||
<span>周环比</span>
|
|
||||||
<span class="green">
|
|
||||||
<icon-arrow-rise />
|
|
||||||
12.8%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-card>
|
|
||||||
<a-card class="content-card">
|
|
||||||
<a-statistic
|
|
||||||
title="访问量"
|
|
||||||
:value="8846"
|
|
||||||
:precision="0"
|
|
||||||
show-group-separator
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<icon-user />
|
|
||||||
</template>
|
|
||||||
</a-statistic>
|
|
||||||
<div class="statistic-footer">
|
|
||||||
<div class="footer-item">
|
|
||||||
<span>日环比</span>
|
|
||||||
<span class="red">
|
|
||||||
<icon-arrow-fall />
|
|
||||||
2.5%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="footer-item">
|
|
||||||
<span>周环比</span>
|
|
||||||
<span class="green">
|
|
||||||
<icon-arrow-rise />
|
|
||||||
15.2%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-card>
|
|
||||||
</a-space>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
|
|
||||||
<a-divider />
|
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
|
||||||
<a-card class="content-card">
|
|
||||||
<template #title>销售数据详情</template>
|
|
||||||
<a-table :data="tableData" :pagination="{ pageSize: 5 }">
|
|
||||||
<template #columns>
|
|
||||||
<a-table-column title="ID" data-index="id" />
|
|
||||||
<a-table-column title="商品名称" data-index="name" />
|
|
||||||
<a-table-column title="销售额" data-index="sales">
|
|
||||||
<template #cell="{ record }">{{
|
|
||||||
record.sales.toLocaleString()
|
|
||||||
}}</template>
|
|
||||||
</a-table-column>
|
|
||||||
<a-table-column title="销售量" data-index="count" />
|
|
||||||
<a-table-column title="日期" data-index="date" />
|
|
||||||
</template>
|
|
||||||
</a-table>
|
|
||||||
</a-card>
|
|
||||||
</a-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, onMounted, ref } from 'vue';
|
|
||||||
import * as echarts from 'echarts';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'DataAnalysis',
|
|
||||||
setup() {
|
|
||||||
const chartRef = ref<HTMLElement | null>(null);
|
|
||||||
const chart = ref<echarts.ECharts | null>(null);
|
|
||||||
|
|
||||||
const tableData = [
|
|
||||||
{ id: 1, name: '商品A', sales: 12345, count: 123, date: '2023-01-01' },
|
|
||||||
{ id: 2, name: '商品B', sales: 23456, count: 234, date: '2023-01-02' },
|
|
||||||
{ id: 3, name: '商品C', sales: 34567, count: 345, date: '2023-01-03' },
|
|
||||||
{ id: 4, name: '商品D', sales: 45678, count: 456, date: '2023-01-04' },
|
|
||||||
{ id: 5, name: '商品E', sales: 56789, count: 567, date: '2023-01-05' },
|
|
||||||
{ id: 6, name: '商品F', sales: 67890, count: 678, date: '2023-01-06' },
|
|
||||||
{ id: 7, name: '商品G', sales: 78901, count: 789, date: '2023-01-07' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const initChart = () => {
|
|
||||||
if (!chartRef.value) return;
|
|
||||||
|
|
||||||
chart.value = echarts.init(chartRef.value);
|
|
||||||
|
|
||||||
const option = {
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
data: ['销售额', '访问量'],
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: '3%',
|
|
||||||
right: '4%',
|
|
||||||
bottom: '3%',
|
|
||||||
containLabel: true,
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
boundaryGap: false,
|
|
||||||
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: '销售额',
|
|
||||||
type: 'line',
|
|
||||||
stack: '总量',
|
|
||||||
data: [120, 132, 101, 134, 90, 230, 210],
|
|
||||||
smooth: true,
|
|
||||||
lineStyle: {
|
|
||||||
width: 2,
|
|
||||||
color: '#1890ff',
|
|
||||||
},
|
|
||||||
areaStyle: {
|
|
||||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
||||||
{ offset: 0, color: 'rgba(24,144,255,0.3)' },
|
|
||||||
{ offset: 1, color: 'rgba(24,144,255,0.1)' },
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '访问量',
|
|
||||||
type: 'line',
|
|
||||||
stack: '总量',
|
|
||||||
data: [220, 182, 191, 234, 290, 330, 310],
|
|
||||||
smooth: true,
|
|
||||||
lineStyle: {
|
|
||||||
width: 2,
|
|
||||||
color: '#52c41a',
|
|
||||||
},
|
|
||||||
areaStyle: {
|
|
||||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
||||||
{ offset: 0, color: 'rgba(82,196,26,0.3)' },
|
|
||||||
{ offset: 1, color: 'rgba(82,196,26,0.1)' },
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
chart.value.setOption(option);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initChart();
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
chart.value?.resize();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
chartRef,
|
|
||||||
tableData,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.container {
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
.general-card {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-card {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-wrapper {
|
|
||||||
height: 360px;
|
|
||||||
|
|
||||||
.chart {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.statistic-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 16px;
|
|
||||||
|
|
||||||
.footer-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
span:first-child {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--color-text-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
span:last-child {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.green {
|
|
||||||
color: #52c41a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.red {
|
|
||||||
color: #f5222d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container">
|
|
||||||
<a-card class="general-card" title="多维数据分析">
|
|
||||||
<a-row :gutter="16">
|
|
||||||
<a-col :span="24">
|
|
||||||
<!-- 图表切换控制 -->
|
|
||||||
<div class="chart-control">
|
|
||||||
<a-radio-group v-model="chartType" type="button">
|
|
||||||
<a-radio value="bar">柱状图</a-radio>
|
|
||||||
<a-radio value="line">折线图</a-radio>
|
|
||||||
<a-radio value="pie">饼图</a-radio>
|
|
||||||
</a-radio-group>
|
|
||||||
<a-select v-model="timeRange" style="width: 120px">
|
|
||||||
<a-option value="week">本周</a-option>
|
|
||||||
<a-option value="month">本月</a-option>
|
|
||||||
<a-option value="year">本年</a-option>
|
|
||||||
</a-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 图表区域 -->
|
|
||||||
<div class="chart-wrapper">
|
|
||||||
<div ref="chartRef" class="chart"></div>
|
|
||||||
</div>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
|
|
||||||
<a-divider />
|
|
||||||
|
|
||||||
<!-- 数据维度选择 -->
|
|
||||||
<a-card class="content-card">
|
|
||||||
<template #title>数据维度配置</template>
|
|
||||||
<a-row :gutter="16">
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-form :model="formData" layout="vertical">
|
|
||||||
<a-form-item field="dimension1" label="维度1">
|
|
||||||
<a-select v-model="formData.dimension1">
|
|
||||||
<a-option value="sales">销售额</a-option>
|
|
||||||
<a-option value="traffic">流量</a-option>
|
|
||||||
<a-option value="users">用户数</a-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item field="dimension2" label="维度2">
|
|
||||||
<a-select v-model="formData.dimension2">
|
|
||||||
<a-option value="region">地区</a-option>
|
|
||||||
<a-option value="age">年龄段</a-option>
|
|
||||||
<a-option value="gender">性别</a-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-form :model="formData" layout="vertical">
|
|
||||||
<a-form-item field="dimension3" label="维度3">
|
|
||||||
<a-select v-model="formData.dimension3">
|
|
||||||
<a-option value="channel">渠道</a-option>
|
|
||||||
<a-option value="product">商品</a-option>
|
|
||||||
<a-option value="time">时间</a-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<a-button type="primary" @click="updateChart"
|
|
||||||
>更新图表</a-button
|
|
||||||
>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</a-card>
|
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
|
||||||
<a-card class="content-card">
|
|
||||||
<template #title>多维数据详情</template>
|
|
||||||
<a-table :data="tableData" :pagination="{ pageSize: 5 }">
|
|
||||||
<template #columns>
|
|
||||||
<a-table-column title="ID" data-index="id" />
|
|
||||||
<a-table-column title="维度1" data-index="dim1" />
|
|
||||||
<a-table-column title="维度2" data-index="dim2" />
|
|
||||||
<a-table-column title="维度3" data-index="dim3" />
|
|
||||||
<a-table-column title="数值" data-index="value">
|
|
||||||
<template #cell="{ record }">{{
|
|
||||||
record.value.toLocaleString()
|
|
||||||
}}</template>
|
|
||||||
</a-table-column>
|
|
||||||
</template>
|
|
||||||
</a-table>
|
|
||||||
</a-card>
|
|
||||||
</a-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, onMounted, ref, reactive, watch } from 'vue';
|
|
||||||
import * as echarts from 'echarts';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'MultiDimensionDataAnalysis',
|
|
||||||
setup() {
|
|
||||||
const chartRef = ref<HTMLElement | null>(null);
|
|
||||||
const chart = ref<echarts.ECharts | null>(null);
|
|
||||||
|
|
||||||
const chartType = ref('bar');
|
|
||||||
const timeRange = ref('month');
|
|
||||||
|
|
||||||
const formData = reactive({
|
|
||||||
dimension1: 'sales',
|
|
||||||
dimension2: 'region',
|
|
||||||
dimension3: 'channel',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 模拟数据
|
|
||||||
const tableData = ref([
|
|
||||||
{ id: 1, dim1: '销售额', dim2: '华东', dim3: '线上', value: 4332 },
|
|
||||||
{ id: 2, dim1: '销售额', dim2: '华南', dim3: '线上', value: 2986 },
|
|
||||||
{ id: 3, dim1: '销售额', dim2: '华北', dim3: '线上', value: 3219 },
|
|
||||||
{ id: 4, dim1: '销售额', dim2: '华东', dim3: '线下', value: 3721 },
|
|
||||||
{ id: 5, dim1: '销售额', dim2: '华南', dim3: '线下', value: 2413 },
|
|
||||||
{ id: 6, dim1: '销售额', dim2: '华北', dim3: '线下', value: 1893 },
|
|
||||||
{ id: 7, dim1: '流量', dim2: '华东', dim3: '线上', value: 10243 },
|
|
||||||
{ id: 8, dim1: '流量', dim2: '华南', dim3: '线上', value: 8752 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const getChartData = () => {
|
|
||||||
// 模拟根据维度和时间范围获取不同的数据
|
|
||||||
// 实际应用中可以通过API获取
|
|
||||||
return {
|
|
||||||
xAxis: ['华东', '华南', '华北', '西南', '西北', '东北'],
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: '线上',
|
|
||||||
data: [4332, 2986, 3219, 2851, 1845, 2016],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '线下',
|
|
||||||
data: [3721, 2413, 1893, 2173, 1634, 1723],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateChart = () => {
|
|
||||||
if (!chart.value) return;
|
|
||||||
|
|
||||||
const chartData = getChartData();
|
|
||||||
|
|
||||||
let option: echarts.EChartsOption = {
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: {
|
|
||||||
type: 'shadow',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
data: ['线上', '线下'],
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: '3%',
|
|
||||||
right: '4%',
|
|
||||||
bottom: '3%',
|
|
||||||
containLabel: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (chartType.value === 'bar') {
|
|
||||||
option = {
|
|
||||||
...option,
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: chartData.xAxis,
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
},
|
|
||||||
series: chartData.series.map((item) => ({
|
|
||||||
name: item.name,
|
|
||||||
type: 'bar',
|
|
||||||
data: item.data,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
} else if (chartType.value === 'line') {
|
|
||||||
option = {
|
|
||||||
...option,
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: chartData.xAxis,
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
},
|
|
||||||
series: chartData.series.map((item) => ({
|
|
||||||
name: item.name,
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
data: item.data,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
} else if (chartType.value === 'pie') {
|
|
||||||
// 将数据转换为饼图格式
|
|
||||||
const pieData = chartData.xAxis.map((region, index) => ({
|
|
||||||
name: region,
|
|
||||||
value:
|
|
||||||
chartData.series[0].data[index] + chartData.series[1].data[index],
|
|
||||||
}));
|
|
||||||
|
|
||||||
option = {
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
formatter: '{a} <br/>{b} : {c} ({d}%)',
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
orient: 'vertical',
|
|
||||||
left: 'left',
|
|
||||||
data: chartData.xAxis,
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: formData.dimension1,
|
|
||||||
type: 'pie',
|
|
||||||
radius: '50%',
|
|
||||||
center: ['50%', '60%'],
|
|
||||||
data: pieData,
|
|
||||||
emphasis: {
|
|
||||||
itemStyle: {
|
|
||||||
shadowBlur: 10,
|
|
||||||
shadowOffsetX: 0,
|
|
||||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
chart.value.setOption(option, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initChart = () => {
|
|
||||||
if (!chartRef.value) return;
|
|
||||||
|
|
||||||
chart.value = echarts.init(chartRef.value);
|
|
||||||
updateChart();
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
chart.value?.resize();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
watch([chartType, timeRange], () => {
|
|
||||||
updateChart();
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initChart();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
chartRef,
|
|
||||||
chartType,
|
|
||||||
timeRange,
|
|
||||||
formData,
|
|
||||||
tableData,
|
|
||||||
updateChart,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.container {
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
.general-card {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-card {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-control {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-wrapper {
|
|
||||||
height: 400px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
.chart {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Reference in New Issue
Block a user