mirror of
http://183.237.82.114:10086/kilo/leaser_front.git
synced 2026-10-23 19:35:25 +00:00
feat: 完成空间管理模块
This commit is contained in:
parent
a53cd0e8b8
commit
f582098dc6
46 changed files with 3231 additions and 2309 deletions
BIN
dist.zip
Normal file
BIN
dist.zip
Normal file
Binary file not shown.
|
|
@ -4,6 +4,7 @@ import {
|
|||
ProjectListRes,
|
||||
CreateProjectParam,
|
||||
ProjectDetailRes,
|
||||
TopBuildingTagsRes,
|
||||
} from '@/types/business';
|
||||
import {
|
||||
ProjectPaginationParams,
|
||||
|
|
@ -50,8 +51,11 @@ export function updateProject(
|
|||
export function getProjectCityList(): Promise<CommonCityRes> {
|
||||
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> {
|
||||
return axios.post('rooms/createRoom', params);
|
||||
|
|
@ -79,7 +83,7 @@ export function changeRoomStatus(params: any): Promise<HttpResponse> {
|
|||
// 房源获取头部楼栋tags
|
||||
export function getTopBuildingTagsByProjectId(
|
||||
id: number
|
||||
): Promise<HttpResponse> {
|
||||
): Promise<TopBuildingTagsRes> {
|
||||
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> {
|
||||
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) {
|
||||
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[];
|
||||
}
|
||||
|
||||
export interface UserProject {
|
||||
id: number;
|
||||
name: string;
|
||||
image?: string;
|
||||
imageUrl?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface UserState {
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
|
|
@ -32,6 +40,8 @@ export interface UserState {
|
|||
certification?: number;
|
||||
role: '' | '*' | 'admin' | 'user';
|
||||
permission: UserPermission[];
|
||||
projects?: UserProject[];
|
||||
currentProjectId?: number;
|
||||
}
|
||||
export async function login(data: LoginData) {
|
||||
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.
|
||||
|
|
@ -1,135 +1,202 @@
|
|||
<template>
|
||||
<div class="top-menu">
|
||||
<a-select :style="{ width: '200px' }" placeholder="请选择" :bordered="false">
|
||||
<template #label="{ data }">
|
||||
<div class="current-building">
|
||||
<img class="img" src="@/assets/images/building1.png" alt="">
|
||||
<p class="building-name">星河商务中心</p>
|
||||
</div>
|
||||
</template>
|
||||
<a-option>
|
||||
<p class="building-name">星河商务中心</p>
|
||||
</a-option>
|
||||
</a-select>
|
||||
<a-menu :style="{ width: '100%' }" mode="horizontal" :selected-keys="[selectedKey]"
|
||||
@menu-item-click="onMenuClick" style="width: 100%">
|
||||
<a-menu-item v-for="item in topLevelMenus" :key="item.path">
|
||||
{{ t(item.meta?.locale || '') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</div>
|
||||
<div class="top-menu">
|
||||
<a-select
|
||||
v-model="selectedProjectId"
|
||||
:style="{ width: '200px' }"
|
||||
placeholder="请选择"
|
||||
:bordered="false"
|
||||
@change="handleProjectChange"
|
||||
>
|
||||
<template #label>
|
||||
<div v-if="currentProject" class="current-building">
|
||||
<img
|
||||
class="img"
|
||||
:src="
|
||||
currentProject.imageUrl || currentProject.image || defaultImage
|
||||
"
|
||||
:alt="currentProject.name"
|
||||
/>
|
||||
<p class="building-name">{{ currentProject.name }}</p>
|
||||
</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>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { RouteRecordNormalized } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { RouteRecordNormalized } from 'vue-router';
|
||||
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 topLevelMenus = computed(() => {
|
||||
return router.getRoutes()
|
||||
.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 router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const buildingList = ref([
|
||||
{ logo: new URL('@/assets/images/building1.png', import.meta.url).href, title: '星河商务中心', id: 1 },
|
||||
{ logo: new URL('@/assets/images/building1.png', import.meta.url).href, title: '南山科学科学园', id: 2 },
|
||||
{ logo: new URL('@/assets/images/building1.png', import.meta.url).href, title: '金融科技大厦', id: 3 },
|
||||
{ logo: new URL('@/assets/images/building1.png', import.meta.url).href, title: '福田平安大厦', id: 4 },
|
||||
]);
|
||||
// 获取一级路由(顶级菜单)
|
||||
const topLevelMenus = computed(() => {
|
||||
return router
|
||||
.getRoutes()
|
||||
.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) => {
|
||||
return buildingList.value.find(building => building.id === id);
|
||||
};
|
||||
// 选中的项目ID,优先使用 store 中保存的值
|
||||
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;
|
||||
}>();
|
||||
}>();
|
||||
|
||||
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) {
|
||||
// 跳转到对应路由
|
||||
router.push(targetRoute.path);
|
||||
// 跳转到对应路由
|
||||
router.push(targetRoute.path);
|
||||
}
|
||||
// 触发父组件事件
|
||||
emit('menu-click', key);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<style lang="less">
|
||||
.top-menu {
|
||||
.top-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.current-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;
|
||||
align-items: center;
|
||||
|
||||
.img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.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 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 locale404 from '@/views/exception/404/locale/en-US';
|
||||
import locale500 from '@/views/exception/500/locale/en-US';
|
||||
|
|
@ -30,9 +26,6 @@ export default {
|
|||
...localeSettings,
|
||||
...localeMessageBox,
|
||||
...localeLogin,
|
||||
...localeWorkplace,
|
||||
|
||||
...localeMonitor,
|
||||
...locale403,
|
||||
...locale404,
|
||||
...locale500,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import localeMessageBox from '@/components/message-box/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 locale404 from '@/views/exception/404/locale/zh-CN';
|
||||
import locale500 from '@/views/exception/500/locale/zh-CN';
|
||||
|
|
@ -47,6 +43,7 @@ export default {
|
|||
'menu.business.space': '空间管理',
|
||||
'menu.business.house-view': '房源剖视',
|
||||
'menu.business.house-list': '房源列表',
|
||||
'menu.business.house-atlas': '房源图谱',
|
||||
'menu.business.contract': '合同管理',
|
||||
'menu.business.contract-list': '合同列表',
|
||||
'menu.business.contract-file': '合同归档',
|
||||
|
|
@ -131,9 +128,6 @@ export default {
|
|||
...localeSettings,
|
||||
...localeMessageBox,
|
||||
...localeLogin,
|
||||
...localeWorkplace,
|
||||
|
||||
...localeMonitor,
|
||||
...locale403,
|
||||
...locale404,
|
||||
...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',
|
||||
name: 'business-space-house-list',
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "business-space" */
|
||||
'@/views/business/space/house-list/index.vue'
|
||||
),
|
||||
import('@/views/business/space/house-list/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.business.house-list',
|
||||
requiresAuth: true,
|
||||
|
|
@ -81,6 +78,20 @@ const BUSINESS: AppRouteRecordRaw = {
|
|||
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,
|
||||
role: '',
|
||||
permission: [],
|
||||
projects: undefined,
|
||||
currentProjectId: undefined,
|
||||
}),
|
||||
|
||||
// 配置持久化
|
||||
|
|
@ -37,7 +39,16 @@ const useUserStore = defineStore('user', {
|
|||
key: 'user-store',
|
||||
storage: localStorage,
|
||||
// 只持久化关键的用户信息,排除敏感信息
|
||||
pick: ['name', 'avatar', 'role', 'permission', 'accountId', 'email'],
|
||||
pick: [
|
||||
'name',
|
||||
'avatar',
|
||||
'role',
|
||||
'permission',
|
||||
'accountId',
|
||||
'email',
|
||||
'projects',
|
||||
'currentProjectId',
|
||||
],
|
||||
},
|
||||
|
||||
getters: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
import { UserPermission } from '@/types/api';
|
||||
|
||||
export type RoleType = '' | '*' | 'admin' | 'user';
|
||||
|
||||
export interface UserProject {
|
||||
id: number;
|
||||
name: string;
|
||||
image?: string;
|
||||
imageUrl?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface UserState {
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
|
|
@ -19,4 +28,6 @@ export interface UserState {
|
|||
certification?: number;
|
||||
role: RoleType;
|
||||
permission: UserPermission[];
|
||||
projects?: UserProject[];
|
||||
currentProjectId?: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export interface ProjectItem extends BaseEntity {
|
|||
roomArea: number;
|
||||
rentArea: number;
|
||||
useArea: number;
|
||||
keepArea: number;
|
||||
rentRate: number;
|
||||
RoomAreaStr: string; // 列表显示的计租面积
|
||||
RentAreaStr: string; // 列表显示的招商面积
|
||||
|
|
@ -115,6 +116,17 @@ export type ProjectDetailRes = HttpResponse<{
|
|||
project: ProjectItem;
|
||||
}>;
|
||||
|
||||
// 楼栋标签响应类型
|
||||
export interface BuildingTag {
|
||||
buildingId: number;
|
||||
projectName: string;
|
||||
buildingName: string;
|
||||
}
|
||||
|
||||
export type TopBuildingTagsRes = HttpResponse<{
|
||||
tags: BuildingTag[];
|
||||
}>;
|
||||
|
||||
// 创建房源参数
|
||||
export interface CreateHouseParam {
|
||||
id?: number; // 编辑时需要
|
||||
|
|
|
|||
|
|
@ -15,12 +15,6 @@ function preloadRouteComponent(routeName: string): void {
|
|||
'@/views/business/project/index.vue'
|
||||
);
|
||||
break;
|
||||
case 'dashboard-workplace':
|
||||
import(
|
||||
/* webpackChunkName: "dashboard-workplace" */
|
||||
'@/views/dashboard/workplace/index.vue'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// 未知路由,跳过预加载
|
||||
break;
|
||||
|
|
@ -63,7 +57,7 @@ export function smartPreload(
|
|||
userRole: string,
|
||||
currentRoute?: RouteLocationNormalized
|
||||
): void {
|
||||
const commonRoutes = ['dashboard-workplace'];
|
||||
const commonRoutes: string[] = [];
|
||||
|
||||
// 根据用户角色预加载不同的路由
|
||||
const roleBasedRoutes: Record<string, string[]> = {
|
||||
|
|
|
|||
|
|
@ -7,20 +7,70 @@
|
|||
unmount-on-close
|
||||
@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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted } from 'vue';
|
||||
import { watch, onMounted, computed } from 'vue';
|
||||
// import { Message } from '@arco-design/web-vue';
|
||||
// import { projectTypeList } from '@/utils/constant';
|
||||
import { ProjectItem } from '@/types/business';
|
||||
import Chart from '@/components/chart/index.vue';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
projectId: number | null;
|
||||
detailInfo: ProjectItem | null;
|
||||
projectTypeList: any[];
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
|
|
@ -36,6 +86,158 @@
|
|||
const handleCancel = () => {
|
||||
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(() => {});
|
||||
</script>
|
||||
|
||||
|
|
@ -43,4 +245,33 @@
|
|||
:deep(label.arco-form-item-label) {
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@
|
|||
<div class="slider">
|
||||
<a-slider
|
||||
v-model="rentRate"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:min="rentRateMin"
|
||||
:max="rentRateMax"
|
||||
range
|
||||
:marks="marks"
|
||||
></a-slider>
|
||||
|
|
@ -51,12 +51,12 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { getProjectCityList } from '@/api/business';
|
||||
import { CommonCityItem } from '@/types/comm';
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import { getAllProjectTypeList } from '@/api/setting';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
projectList?: any[]; // 项目列表数据,用于提取筛选选项
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
|
|
@ -66,30 +66,120 @@
|
|||
typeIds: number[] | null;
|
||||
cityIds: number[] | null;
|
||||
rentRate: number | number[];
|
||||
filteredList: any[]; // 筛选后的列表数据
|
||||
}
|
||||
): void;
|
||||
}>();
|
||||
// 保存的筛选状态
|
||||
const savedFilterState = ref<{
|
||||
projectTypeChecked: number[];
|
||||
projectCityChecked: number[];
|
||||
rentRate: number | number[];
|
||||
} | null>(null);
|
||||
|
||||
const projectTypeChecked = ref<number[]>([0]);
|
||||
const projectCityChecked = ref<number[]>([0]);
|
||||
const rentRate = ref<number | number[]>(0);
|
||||
const projectTypeList = [
|
||||
{ text: '全部', value: 0 },
|
||||
{ text: '写字楼', value: 1 },
|
||||
{ text: '园区', value: 2 },
|
||||
{ text: '商业综合体', value: 3 },
|
||||
];
|
||||
const marks = {
|
||||
0: '0%',
|
||||
100: '100%',
|
||||
};
|
||||
const projectCityList = ref<CommonCityItem[]>([]);
|
||||
const getProjectCityListData = async () => {
|
||||
const { code, data } = await getProjectCityList();
|
||||
if (code === 200 && data) {
|
||||
projectCityList.value = Array.isArray(data.cityList) ? data.cityList : [];
|
||||
projectCityList.value.unshift({ id: 0, code: '0', name: '全部' });
|
||||
const rentRate = ref<number | number[]>([0, 100]);
|
||||
|
||||
// 所有项目类型列表(从API获取)
|
||||
const allProjectTypeList = ref<any[]>([]);
|
||||
|
||||
// 获取项目类型列表
|
||||
const getAllProjectTypeListData = async () => {
|
||||
try {
|
||||
const { code, data } = await getAllProjectTypeList();
|
||||
if (code === 200 && data) {
|
||||
allProjectTypeList.value = data?.projectTypesList || [];
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 项目类型列表(从项目列表中提取实际存在的类型)
|
||||
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) => {
|
||||
if (typeof id !== 'number') return;
|
||||
if (id === 0) {
|
||||
|
|
@ -129,26 +219,114 @@
|
|||
const handleCancel = () => {
|
||||
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[]) =>
|
||||
ids.length === 1 && ids[0] === 0 ? null : ids;
|
||||
const handleOk = () => {
|
||||
// 保存当前筛选状态
|
||||
savedFilterState.value = {
|
||||
projectTypeChecked: [...projectTypeChecked.value],
|
||||
projectCityChecked: [...projectCityChecked.value],
|
||||
rentRate: Array.isArray(rentRate.value)
|
||||
? [...rentRate.value]
|
||||
: rentRate.value,
|
||||
};
|
||||
|
||||
const filteredList = filterProjectList();
|
||||
emit('confirm', {
|
||||
typeIds: normalizeIds(projectTypeChecked.value),
|
||||
cityIds: normalizeIds(projectCityChecked.value),
|
||||
rentRate: rentRate.value,
|
||||
filteredList, // 筛选后的列表数据
|
||||
});
|
||||
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(
|
||||
() => props.visible,
|
||||
async (val) => {
|
||||
if (val && projectCityList.value.length === 0) {
|
||||
getProjectCityListData();
|
||||
(val) => {
|
||||
if (val) {
|
||||
initFilterForm();
|
||||
if (allProjectTypeList.value.length === 0) {
|
||||
getAllProjectTypeListData();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
onMounted(() => {});
|
||||
|
||||
onMounted(() => {
|
||||
getAllProjectTypeListData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
@page-size-change="onPageSizeChange"
|
||||
>
|
||||
<template #type="{ record }">
|
||||
<a-tag checkable :color="record.color">
|
||||
<a-tag checkable :color="getTypeColor(record.type)">
|
||||
{{ record.typeName }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
|
@ -71,9 +71,20 @@
|
|||
<a-button @click="handleWatchHouse(record.id)">
|
||||
查看房源
|
||||
</a-button>
|
||||
<a-button status="danger" @click="handleDel(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>
|
||||
|
|
@ -91,9 +102,11 @@
|
|||
v-model:visible="showProjectInfoDrawer"
|
||||
:project-id="currentProjectId"
|
||||
:detail-info="currentProjectInfo"
|
||||
:project-type-list="projectTypeList"
|
||||
/>
|
||||
<projectListFilterDrawer
|
||||
v-model:visible="showListFilterDrawer"
|
||||
:project-list="projectList"
|
||||
@confirm="handleFilterConfirm"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -106,12 +119,13 @@
|
|||
import type { TableColumnData } from '@arco-design/web-vue';
|
||||
import useDataTable from '@/composables/useDataTable';
|
||||
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 projectFormDrawer from './drawer/project-form-drawer.vue';
|
||||
import projectInfoDrawer from './drawer/project-info-drawer.vue';
|
||||
import projectListFilterDrawer from './drawer/project-list-filter-drawer.vue';
|
||||
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
const { t } = useI18n();
|
||||
const { loading } = useLoading(false);
|
||||
const showDrawer = ref<boolean>(false);
|
||||
|
|
@ -155,7 +169,13 @@
|
|||
},
|
||||
{ title: '房源数量', dataIndex: 'roomNum', key: 'roomNum' },
|
||||
{ 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>({
|
||||
page: current.value,
|
||||
|
|
@ -203,15 +223,36 @@
|
|||
// 处理错误
|
||||
}
|
||||
};
|
||||
const handleFilterConfirm = ({ typeIds, cityIds, rentRate }) => {
|
||||
queryParams.value.type = typeIds;
|
||||
queryParams.value.province_ids = cityIds;
|
||||
queryParams.value.rent_rate_min = rentRate[0];
|
||||
queryParams.value.rent_rate_max = rentRate[1];
|
||||
getProjectListData();
|
||||
const handleFilterConfirm = (payload: {
|
||||
typeIds: number[] | null;
|
||||
cityIds: number[] | null;
|
||||
rentRate: number | number[];
|
||||
filteredList: any[]; // 筛选后的列表数据
|
||||
}) => {
|
||||
// 更新查询参数(用于后端筛选)
|
||||
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) => {
|
||||
// TODO: 实现删除功能
|
||||
const handleDel = async (id: number) => {
|
||||
const { code, message } = await delProject(id);
|
||||
if (code === 200) {
|
||||
Message.success(message);
|
||||
getProjectListData();
|
||||
}
|
||||
};
|
||||
// 分页事件处理
|
||||
const onPageChange = (page: number) => {
|
||||
|
|
@ -244,8 +285,20 @@
|
|||
const handleFilter = () => {
|
||||
showListFilterDrawer.value = true;
|
||||
};
|
||||
onMounted(() => {
|
||||
setColumns(columns);
|
||||
getProjectListData();
|
||||
const projectTypeList = ref<any[]>([]);
|
||||
const getAllProjectTypeListData = async () => {
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
413
src/views/business/space/house-atlas/index.vue
Normal file
413
src/views/business/space/house-atlas/index.vue
Normal file
|
|
@ -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"
|
||||
@cancel="handleDel(record?.id)"
|
||||
>
|
||||
<a-button status="danger">删除项目</a-button>
|
||||
<a-button status="danger">删除房源</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
|
|
@ -132,6 +132,7 @@
|
|||
import houseInfoDrawer from '../drawer/house-info-drawer.vue';
|
||||
import houseListFilterDrawer from '../drawer/house-list-filter-drawer.vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { PRICE_UNIT_MAP } from '@/constants';
|
||||
const { t } = useI18n();
|
||||
const { loading } = useLoading(false);
|
||||
const showDrawer = ref<boolean>(false);
|
||||
|
|
@ -201,7 +202,11 @@
|
|||
title: '租金(实时)',
|
||||
dataIndex: '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' },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<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>
|
||||
<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>
|
||||
Loading…
Add table
Reference in a new issue