Merge pull request 'master' (#5) from master into prod
All checks were successful
/ Explore-Gitea-Actions (push) Successful in 15m33s
All checks were successful
/ Explore-Gitea-Actions (push) Successful in 15m33s
Reviewed-on: #5
This commit is contained in:
@@ -20,6 +20,8 @@ VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj6
|
||||
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
|
||||
# 客户端id
|
||||
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||
# 开启WEBSOCKET
|
||||
VITE_APP_WEBSOCKET=true
|
||||
|
||||
# 开启SSE
|
||||
VITE_GLOB_SSE_ENABLE=true
|
||||
|
@@ -26,6 +26,8 @@ VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj6
|
||||
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
|
||||
# 客户端id
|
||||
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||
# 开启WEBSOCKET
|
||||
VITE_APP_WEBSOCKET=true
|
||||
|
||||
# 开启SSE
|
||||
VITE_GLOB_SSE_ENABLE=true
|
||||
|
63
apps/web-antd/src/api/analytics/index.ts
Normal file
63
apps/web-antd/src/api/analytics/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 查询工单数量
|
||||
* @param params
|
||||
* @returns 工单数量
|
||||
*/
|
||||
export function getIndexCount() {
|
||||
return requestClient.get<any>('/property/index/indexCount');
|
||||
}
|
||||
|
||||
// 今日预警分类统计
|
||||
export function getStatisticsCurrDay() {
|
||||
return requestClient.get<any>('/sis/alarmEvents/query/statistics/currDay');
|
||||
}
|
||||
// 所有预警信息分类统计
|
||||
export function getStatistics() {
|
||||
return requestClient.get<any>('/sis/alarmEvents/query/statistics');
|
||||
}
|
||||
// /**
|
||||
// * 导出资产管理列表
|
||||
// * @param params
|
||||
// * @returns 资产管理列表
|
||||
// */
|
||||
// export function assetExport(params?: AssetQuery) {
|
||||
// return commonExport('/property/asset/export', params ?? {});
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 查询资产管理详情
|
||||
// * @param id id
|
||||
// * @returns 资产管理详情
|
||||
// */
|
||||
// export function assetInfo(id: ID) {
|
||||
// return requestClient.get<AssetVO>(`/property/asset/${id}`);
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 新增资产管理
|
||||
// * @param data
|
||||
// * @returns void
|
||||
// */
|
||||
// export function assetAdd(data: AssetForm) {
|
||||
// return requestClient.postWithMsg<void>('/property/asset', data);
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 更新资产管理
|
||||
// * @param data
|
||||
// * @returns void
|
||||
// */
|
||||
// export function assetUpdate(data: AssetForm) {
|
||||
// return requestClient.putWithMsg<void>('/property/asset', data);
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 删除资产管理
|
||||
// * @param id id
|
||||
// * @returns void
|
||||
// */
|
||||
// export function assetRemove(id: ID | IDS) {
|
||||
// return requestClient.deleteWithMsg<void>(`/property/asset/${id}`);
|
||||
// }
|
@@ -1,6 +1,6 @@
|
||||
import type { MeterInfoVO, MeterInfoForm, MeterInfoQuery } from './model'
|
||||
|
||||
import type { ID, IDS, PageResult, TreeNode } from '#/api/common';
|
||||
import type { ID, IDS, PageResult, TreeNode } from '#/api/common'
|
||||
|
||||
import { commonExport } from '#/api/helper'
|
||||
import { requestClient } from '#/api/request'
|
||||
@@ -64,6 +64,13 @@ export function meterInfoRemove(id: ID | IDS) {
|
||||
* @param level
|
||||
* @returns 水电气树
|
||||
*/
|
||||
export function queryTree(meterType: number | string) {
|
||||
return requestClient.get<TreeNode<Number>[]>(`/property/meterInfo/tree/${meterType}`)
|
||||
export function queryTree(params?: any) {
|
||||
return requestClient.get<TreeNode<Number>[]>(`/property/meterInfo/tree`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取水/电/气表当前读数/状态
|
||||
*/
|
||||
export function currentReading(params?: any) {
|
||||
return requestClient.get<void>(`/property/meterInfo/currentReading`, { params })
|
||||
}
|
@@ -1,4 +1,9 @@
|
||||
import type { MeterRecordVO, MeterRecordForm, MeterRecordQuery, MeterRecordTrend } from './model';
|
||||
import type {
|
||||
MeterRecordVO,
|
||||
MeterRecordForm,
|
||||
MeterRecordQuery,
|
||||
MeterRecordTrend,
|
||||
} from './model';
|
||||
|
||||
import type { ID, IDS } from '#/api/common';
|
||||
import type { PageResult } from '#/api/common';
|
||||
@@ -7,12 +12,15 @@ import { commonExport } from '#/api/helper';
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 查询抄表记录列表
|
||||
* @param params
|
||||
* @returns 抄表记录列表
|
||||
*/
|
||||
* 查询抄表记录列表
|
||||
* @param params
|
||||
* @returns 抄表记录列表
|
||||
*/
|
||||
export function meterRecordList(params?: MeterRecordQuery) {
|
||||
return requestClient.get<PageResult<MeterRecordVO>>('/property/meterRecord/list', { params });
|
||||
return requestClient.get<PageResult<MeterRecordVO>>(
|
||||
'/property/meterRecord/list',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,10 +70,10 @@ export function meterRecordRemove(id: ID | IDS) {
|
||||
|
||||
/**
|
||||
* 获取用电/气/水趋势分析数据
|
||||
*
|
||||
*
|
||||
* @param params
|
||||
* @returns 用电/气/水趋势分析数据
|
||||
*/
|
||||
export function meterRecordTrend(params: MeterRecordTrend) {
|
||||
return requestClient.get<void>('/property/meterRecord/trend', { params });
|
||||
return requestClient.get<any>('/property/meterRecord/trend', { params });
|
||||
}
|
||||
|
@@ -1,182 +1,179 @@
|
||||
import type { PageQuery, BaseEntity } from '#/api/common'
|
||||
import type { PageQuery, BaseEntity } from '#/api/common';
|
||||
|
||||
export interface MeterRecordVO {
|
||||
/**
|
||||
* 记录ID
|
||||
*/
|
||||
id: string | number
|
||||
id: string | number;
|
||||
|
||||
/**
|
||||
* 仪表编号
|
||||
*/
|
||||
meterId: string | number
|
||||
meterId: string | number;
|
||||
|
||||
/**
|
||||
* 仪表类型
|
||||
*/
|
||||
meterType: string | number
|
||||
* 仪表类型
|
||||
*/
|
||||
meterType: string | number;
|
||||
|
||||
/**
|
||||
* 抄表员ID
|
||||
*/
|
||||
readerId: string | number
|
||||
readerId: string | number;
|
||||
|
||||
/**
|
||||
* 抄表时间
|
||||
*/
|
||||
readingTime: string
|
||||
readingTime: string;
|
||||
|
||||
/**
|
||||
* 当前读数
|
||||
*/
|
||||
currentReading: number
|
||||
currentReading: number;
|
||||
|
||||
/**
|
||||
* 上次读数
|
||||
*/
|
||||
previousReading: number
|
||||
previousReading: number;
|
||||
|
||||
/**
|
||||
* 用量
|
||||
*/
|
||||
consumption: number
|
||||
consumption: number;
|
||||
|
||||
/**
|
||||
* 抄表方式(1手动 2自动 3用户上报)
|
||||
*/
|
||||
readingMethod: number
|
||||
readingMethod: number;
|
||||
|
||||
/**
|
||||
* 抄表照片
|
||||
*/
|
||||
imgOssid: string | number
|
||||
|
||||
imgOssid: string | number;
|
||||
}
|
||||
|
||||
export interface MeterRecordForm extends BaseEntity {
|
||||
/**
|
||||
* 记录ID
|
||||
*/
|
||||
id?: string | number
|
||||
id?: string | number;
|
||||
|
||||
/**
|
||||
* 仪表编号
|
||||
*/
|
||||
meterId?: string | number
|
||||
meterId?: string | number;
|
||||
|
||||
/**
|
||||
* 抄表员ID
|
||||
*/
|
||||
readerId?: string | number
|
||||
readerId?: string | number;
|
||||
|
||||
/**
|
||||
* 抄表时间
|
||||
*/
|
||||
readingTime?: string
|
||||
readingTime?: string;
|
||||
|
||||
/**
|
||||
* 当前读数
|
||||
*/
|
||||
currentReading?: number
|
||||
currentReading?: number;
|
||||
|
||||
/**
|
||||
* 上次读数
|
||||
*/
|
||||
previousReading?: number
|
||||
previousReading?: number;
|
||||
|
||||
/**
|
||||
* 用量
|
||||
*/
|
||||
consumption?: number
|
||||
consumption?: number;
|
||||
|
||||
/**
|
||||
* 抄表方式(1手动 2自动 3用户上报)
|
||||
*/
|
||||
readingMethod?: number
|
||||
readingMethod?: number;
|
||||
|
||||
/**
|
||||
* 抄表照片
|
||||
*/
|
||||
imgOssid?: string | number
|
||||
|
||||
imgOssid?: string | number;
|
||||
}
|
||||
|
||||
export interface MeterRecordQuery extends PageQuery {
|
||||
/**
|
||||
* 仪表编号
|
||||
*/
|
||||
meterId?: string | number
|
||||
meterId?: string | number;
|
||||
|
||||
/**
|
||||
* 抄表员ID
|
||||
*/
|
||||
readerId?: string | number
|
||||
readerId?: string | number;
|
||||
|
||||
/**
|
||||
* 抄表时间
|
||||
*/
|
||||
readingTime?: string
|
||||
readingTime?: string;
|
||||
|
||||
/**
|
||||
* 当前读数
|
||||
*/
|
||||
currentReading?: number
|
||||
currentReading?: number;
|
||||
|
||||
/**
|
||||
* 上次读数
|
||||
*/
|
||||
previousReading?: number
|
||||
previousReading?: number;
|
||||
|
||||
/**
|
||||
* 用量
|
||||
*/
|
||||
consumption?: number
|
||||
consumption?: number;
|
||||
|
||||
/**
|
||||
* 抄表方式(1手动 2自动 3用户上报)
|
||||
*/
|
||||
readingMethod?: number
|
||||
readingMethod?: number;
|
||||
|
||||
/**
|
||||
* 抄表照片
|
||||
*/
|
||||
imgOssid?: string | number
|
||||
imgOssid?: string | number;
|
||||
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
}
|
||||
|
||||
|
||||
export interface MeterRecordTrend {
|
||||
/**
|
||||
* 仪表类型
|
||||
*/
|
||||
meterType?: string | number
|
||||
meterType?: string | number;
|
||||
|
||||
/**
|
||||
* 仪表ID
|
||||
*/
|
||||
meterId: string | number
|
||||
meterId: any;
|
||||
|
||||
/**
|
||||
* 楼层ID
|
||||
*/
|
||||
floorId: string | number
|
||||
floorId: any;
|
||||
|
||||
/**
|
||||
* 日期
|
||||
*/
|
||||
day?: string
|
||||
day?: string;
|
||||
|
||||
/**
|
||||
* 月份
|
||||
*/
|
||||
month?: string
|
||||
month?: string;
|
||||
|
||||
/**
|
||||
* 年份
|
||||
*/
|
||||
year?: string
|
||||
year?: string;
|
||||
}
|
||||
|
35
apps/web-antd/src/api/property/room/model.d.ts
vendored
35
apps/web-antd/src/api/property/room/model.d.ts
vendored
@@ -20,6 +20,7 @@ export interface RoomVO {
|
||||
* 房间类型('住宅','商铺','办公室','设备间','公共区域')
|
||||
*/
|
||||
roomType: string;
|
||||
roomTypeName: string;
|
||||
|
||||
/**
|
||||
* 建筑面积(平方米)
|
||||
@@ -50,6 +51,40 @@ export interface RoomVO {
|
||||
* 状态('空置','已售','已租','自用')
|
||||
*/
|
||||
status: string;
|
||||
/**
|
||||
* 房间图片
|
||||
*/
|
||||
imgUrl: string;
|
||||
imgPath: string;
|
||||
|
||||
|
||||
/**
|
||||
* 是否重要
|
||||
*/
|
||||
isMatter: string;
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
statusName: string;
|
||||
|
||||
/**
|
||||
* 小区
|
||||
*/
|
||||
communityText: string;
|
||||
|
||||
/**
|
||||
* 建筑
|
||||
*/
|
||||
buildingText: string;
|
||||
|
||||
/**
|
||||
* 楼层
|
||||
*/
|
||||
floorText: string;
|
||||
/**
|
||||
* 所属单位
|
||||
*/
|
||||
residentUnitText: string;
|
||||
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { PageQuery, BaseEntity } from '#/api/common';
|
||||
import type {PageQuery, BaseEntity} from '#/api/common';
|
||||
|
||||
export interface MeetVO {
|
||||
/**
|
||||
@@ -90,6 +90,10 @@ export interface MeetVO {
|
||||
* 负责人
|
||||
*/
|
||||
principalsName: string;
|
||||
/**
|
||||
* 保密等级
|
||||
*/
|
||||
secrecyGrade: string;
|
||||
}
|
||||
|
||||
export interface MeetForm extends BaseEntity {
|
||||
@@ -236,8 +240,8 @@ export interface MeetQuery extends PageQuery {
|
||||
searchValue?: string;
|
||||
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
|
||||
/**
|
||||
@@ -274,7 +278,7 @@ export interface MeetQuery extends PageQuery {
|
||||
picture: string;
|
||||
}
|
||||
|
||||
export interface ConferenceSettingsDetail{
|
||||
export interface ConferenceSettingsDetail {
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@@ -375,7 +379,7 @@ export interface ConferenceSettingsDetail{
|
||||
|
||||
}
|
||||
|
||||
export interface MeetBo{
|
||||
export interface MeetBo {
|
||||
/**
|
||||
* 会议室名称
|
||||
*/
|
||||
|
@@ -0,0 +1,59 @@
|
||||
import type { ParticipantsVO, ParticipantsForm, ParticipantsQuery } from './model';
|
||||
import type { ID, IDS } from '#/api/common';
|
||||
import type { PageResult } from '#/api/common';
|
||||
import { commonExport } from '#/api/helper';
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 查询会议室参会记录列表
|
||||
* @param params
|
||||
* @returns 会议室参会记录列表
|
||||
*/
|
||||
export function participantsList(params?: ParticipantsQuery) {
|
||||
return requestClient.get<PageResult<ParticipantsVO>>('/property/participants/list', { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出会议室参会记录列表
|
||||
* @param params
|
||||
* @returns 会议室参会记录列表
|
||||
*/
|
||||
export function participantsExport(params?: ParticipantsQuery) {
|
||||
return commonExport('/property/participants/export', params ?? {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询会议室参会记录详情
|
||||
* @param id id
|
||||
* @returns 会议室参会记录详情
|
||||
*/
|
||||
export function participantsInfo(id: ID) {
|
||||
return requestClient.get<ParticipantsVO>(`/property/participants/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增会议室参会记录
|
||||
* @param data
|
||||
* @returns void
|
||||
*/
|
||||
export function participantsAdd(data: ParticipantsForm) {
|
||||
return requestClient.postWithMsg<void>('/property/participants', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会议室参会记录
|
||||
* @param data
|
||||
* @returns void
|
||||
*/
|
||||
export function participantsUpdate(data: ParticipantsForm) {
|
||||
return requestClient.putWithMsg<void>('/property/participants', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会议室参会记录
|
||||
* @param id id
|
||||
* @returns void
|
||||
*/
|
||||
export function participantsRemove(id: ID | IDS) {
|
||||
return requestClient.deleteWithMsg<void>(`/property/participants/${id}`);
|
||||
}
|
54
apps/web-antd/src/api/property/roomBooking/participants/model.d.ts
vendored
Normal file
54
apps/web-antd/src/api/property/roomBooking/participants/model.d.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { PageQuery, BaseEntity } from '#/api/common';
|
||||
|
||||
export interface ParticipantsVO {
|
||||
/**
|
||||
* 主键id
|
||||
*/
|
||||
id: string | number;
|
||||
|
||||
/**
|
||||
* 会议室id
|
||||
*/
|
||||
meetId: string | number;
|
||||
|
||||
/**
|
||||
* 入驻人员id
|
||||
*/
|
||||
residentPersonId: string | number;
|
||||
|
||||
}
|
||||
|
||||
export interface ParticipantsForm extends BaseEntity {
|
||||
/**
|
||||
* 主键id
|
||||
*/
|
||||
id?: string | number;
|
||||
|
||||
/**
|
||||
* 会议室id
|
||||
*/
|
||||
meetId?: string | number;
|
||||
|
||||
/**
|
||||
* 入驻人员id
|
||||
*/
|
||||
residentPersonId?: string | number;
|
||||
|
||||
}
|
||||
|
||||
export interface ParticipantsQuery extends PageQuery {
|
||||
/**
|
||||
* 会议室id
|
||||
*/
|
||||
meetId?: string | number;
|
||||
|
||||
/**
|
||||
* 入驻人员id
|
||||
*/
|
||||
residentPersonId?: string | number;
|
||||
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
}
|
162
apps/web-antd/src/api/websocket.ts
Normal file
162
apps/web-antd/src/api/websocket.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
interface WebSocketCallbacks {
|
||||
onOpen?: (event: Event) => void;
|
||||
onMessage?: (event: MessageEvent) => void;
|
||||
onError?: (error: Event) => void;
|
||||
onClose?: (event: CloseEvent) => void;
|
||||
}
|
||||
|
||||
export default class WebSocketService {
|
||||
private webSocket: WebSocket | null = null;
|
||||
private webSocketURL: string = '';
|
||||
|
||||
// 默认回调函数
|
||||
private onOpenCallback: (event: Event) => void;
|
||||
private onMessageCallback: (event: MessageEvent) => void;
|
||||
private onErrorCallback: (error: Event) => void;
|
||||
private onCloseCallback: (event: CloseEvent) => void;
|
||||
|
||||
constructor(callbacks?: WebSocketCallbacks) {
|
||||
// 设置回调函数,使用自定义回调或默认回调
|
||||
this.onOpenCallback = callbacks?.onOpen || this.defaultOnOpen;
|
||||
this.onMessageCallback = callbacks?.onMessage || this.defaultOnMessage;
|
||||
this.onErrorCallback = callbacks?.onError || this.defaultOnError;
|
||||
this.onCloseCallback = callbacks?.onClose || this.defaultOnClose;
|
||||
}
|
||||
|
||||
// 初始化WebSocket连接
|
||||
initWebSocket(webSocketURL: string): void {
|
||||
this.webSocketURL = webSocketURL;
|
||||
try {
|
||||
this.webSocket = new WebSocket(webSocketURL);
|
||||
|
||||
this.webSocket.onopen = this.onOpenCallback;
|
||||
this.webSocket.onmessage = this.onMessageCallback;
|
||||
this.webSocket.onerror = this.onErrorCallback;
|
||||
this.webSocket.onclose = this.onCloseCallback;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize WebSocket:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置onOpen回调并更新WebSocket事件处理器
|
||||
setOnOpenCallback(callback: (event: Event) => void): void {
|
||||
this.onOpenCallback = callback;
|
||||
if (this.webSocket) {
|
||||
this.webSocket.onopen = this.onOpenCallback;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置onMessage回调并更新WebSocket事件处理器
|
||||
setOnMessageCallback(callback: (event: MessageEvent) => void): void {
|
||||
this.onMessageCallback = callback;
|
||||
if (this.webSocket) {
|
||||
this.webSocket.onmessage = this.onMessageCallback;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置onError回调并更新WebSocket事件处理器
|
||||
setOnErrorCallback(callback: (error: Event) => void): void {
|
||||
this.onErrorCallback = callback;
|
||||
if (this.webSocket) {
|
||||
this.webSocket.onerror = this.onErrorCallback;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置onClose回调并更新WebSocket事件处理器
|
||||
setOnCloseCallback(callback: (event: CloseEvent) => void): void {
|
||||
this.onCloseCallback = callback;
|
||||
if (this.webSocket) {
|
||||
this.webSocket.onclose = this.onCloseCallback;
|
||||
}
|
||||
}
|
||||
|
||||
// 默认连接建立成功的回调
|
||||
private defaultOnOpen(event: Event): void {
|
||||
console.log('WebSocket连接建立成功', event);
|
||||
}
|
||||
|
||||
// 默认收到服务器消息的回调
|
||||
private defaultOnMessage(event: MessageEvent): void {
|
||||
console.log('收到服务器消息', event.data);
|
||||
// 通常这里会解析数据并更新页面
|
||||
// const data = JSON.parse(event.data);
|
||||
// 根据消息类型处理不同业务...
|
||||
}
|
||||
|
||||
// 默认发生错误的回调
|
||||
private defaultOnError(error: Event): void {
|
||||
console.error('WebSocket连接错误', error);
|
||||
}
|
||||
|
||||
// 默认连接关闭的回调
|
||||
private defaultOnClose(event: CloseEvent): void {
|
||||
console.log('WebSocket连接关闭', event);
|
||||
}
|
||||
|
||||
// 关闭连接
|
||||
close(): void {
|
||||
if (this.webSocket) {
|
||||
this.webSocket.close();
|
||||
this.webSocket = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
sendMessage(message: string | object): void {
|
||||
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
|
||||
const dataToSend = typeof message === 'string' ? message : JSON.stringify(message);
|
||||
this.webSocket.send(dataToSend);
|
||||
} else {
|
||||
console.error('WebSocket连接未就绪');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前WebSocket状态
|
||||
getReadyState(): number | null {
|
||||
return this.webSocket ? this.webSocket.readyState : null;
|
||||
}
|
||||
|
||||
// 获取WebSocket URL
|
||||
getWebSocketURL(): string {
|
||||
return this.webSocketURL;
|
||||
}
|
||||
|
||||
// 重新连接
|
||||
reconnect(): void {
|
||||
if (this.webSocketURL) {
|
||||
this.close();
|
||||
this.initWebSocket(this.webSocketURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建一个可导出的WebSocket实例
|
||||
let globalWebSocketService: WebSocketService | null = null;
|
||||
|
||||
/**
|
||||
* 初始化WebSocket连接的可导出方法
|
||||
* @param url WebSocket服务器地址
|
||||
* @param callbacks 回调函数对象(可选)
|
||||
* @returns WebSocketService实例
|
||||
*/
|
||||
export function initWebSocket(callbacks?: WebSocketCallbacks): void {
|
||||
if (!globalWebSocketService) {
|
||||
globalWebSocketService = new WebSocketService(callbacks);
|
||||
}
|
||||
const accessStore = useAccessStore();
|
||||
const clinetId = import.meta.env.VITE_GLOB_APP_CLIENT_ID;
|
||||
const api = import.meta.env.VITE_GLOB_API_URL;
|
||||
const host = window.location.host;
|
||||
const url = `ws://${host}${api}/resource/websocket?clientid=${clinetId}&Authorization=Bearer ${accessStore.accessToken}`;
|
||||
globalWebSocketService.initWebSocket(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局WebSocket服务实例
|
||||
* @returns WebSocketService实例或null
|
||||
*/
|
||||
export function getWebSocketService(): WebSocketService | null {
|
||||
return globalWebSocketService;
|
||||
}
|
BIN
apps/web-antd/src/assets/tree/player-err.png
Normal file
BIN
apps/web-antd/src/assets/tree/player-err.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
apps/web-antd/src/assets/tree/playering.png
Normal file
BIN
apps/web-antd/src/assets/tree/playering.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
apps/web-antd/src/assets/tree/unplayer.png
Normal file
BIN
apps/web-antd/src/assets/tree/unplayer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
@@ -1,30 +1,30 @@
|
||||
import { createApp, watchEffect } from 'vue';
|
||||
import { createApp, watchEffect } from 'vue'
|
||||
|
||||
import { registerAccessDirective } from '@vben/access';
|
||||
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { initStores } from '@vben/stores';
|
||||
import '@vben/styles';
|
||||
import '@vben/styles/antd';
|
||||
import { registerAccessDirective } from '@vben/access'
|
||||
import { registerLoadingDirective } from '@vben/common-ui/es/loading'
|
||||
import { preferences } from '@vben/preferences'
|
||||
import { initStores } from '@vben/stores'
|
||||
import '@vben/styles'
|
||||
import '@vben/styles/antd'
|
||||
|
||||
import { useTitle } from '@vueuse/core';
|
||||
import { useTitle } from '@vueuse/core'
|
||||
|
||||
import { setupGlobalComponent } from '#/components/global';
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
import { setupGlobalComponent } from '#/components/global'
|
||||
import { $t, setupI18n } from '#/locales'
|
||||
|
||||
import { initComponentAdapter } from './adapter/component';
|
||||
import { initSetupVbenForm } from './adapter/form';
|
||||
import App from './app.vue';
|
||||
import { router } from './router';
|
||||
import { initComponentAdapter } from './adapter/component'
|
||||
import { initSetupVbenForm } from './adapter/form'
|
||||
import App from './app.vue'
|
||||
import { router } from './router'
|
||||
|
||||
|
||||
|
||||
async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
await initComponentAdapter();
|
||||
await initComponentAdapter()
|
||||
|
||||
// 初始化表单组件
|
||||
await initSetupVbenForm();
|
||||
await initSetupVbenForm()
|
||||
|
||||
// // 设置弹窗的默认配置
|
||||
// setDefaultModalProps({
|
||||
@@ -35,49 +35,50 @@ async function bootstrap(namespace: string) {
|
||||
// zIndex: 1020,
|
||||
// });
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 全局组件
|
||||
setupGlobalComponent(app);
|
||||
setupGlobalComponent(app)
|
||||
// 注册v-loading指令
|
||||
registerLoadingDirective(app, {
|
||||
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
|
||||
spinning: 'spinning',
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
// 国际化 i18n 配置
|
||||
await setupI18n(app);
|
||||
await setupI18n(app)
|
||||
|
||||
// 配置 pinia-tore
|
||||
await initStores(app, { namespace });
|
||||
await initStores(app, { namespace })
|
||||
|
||||
// 安装权限指令
|
||||
registerAccessDirective(app);
|
||||
registerAccessDirective(app)
|
||||
|
||||
// 初始化 tippy
|
||||
const { initTippy } = await import('@vben/common-ui/es/tippy');
|
||||
initTippy(app);
|
||||
const { initTippy } = await import('@vben/common-ui/es/tippy')
|
||||
initTippy(app)
|
||||
|
||||
// 配置路由及路由守卫
|
||||
app.use(router);
|
||||
app.use(router)
|
||||
|
||||
|
||||
// 配置Motion插件
|
||||
const { MotionPlugin } = await import('@vben/plugins/motion');
|
||||
app.use(MotionPlugin);
|
||||
const { MotionPlugin } = await import('@vben/plugins/motion')
|
||||
app.use(MotionPlugin)
|
||||
|
||||
// 动态更新标题
|
||||
watchEffect(() => {
|
||||
if (preferences.app.dynamicTitle) {
|
||||
const routeTitle = router.currentRoute.value.meta?.title;
|
||||
const routeTitle = router.currentRoute.value.meta?.title
|
||||
const pageTitle =
|
||||
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
|
||||
useTitle(pageTitle);
|
||||
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name
|
||||
useTitle(pageTitle)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
app.mount('#app');
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
export { bootstrap };
|
||||
export { bootstrap }
|
||||
|
@@ -1,27 +1,27 @@
|
||||
import type { LoginAndRegisterParams } from '@vben/common-ui';
|
||||
import type { UserInfo } from '@vben/types';
|
||||
import type { LoginAndRegisterParams } from '@vben/common-ui'
|
||||
import type { UserInfo } from '@vben/types'
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { LOGIN_PATH } from '@vben/constants'
|
||||
import { preferences } from '@vben/preferences'
|
||||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'
|
||||
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { notification } from 'ant-design-vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { doLogout, getUserInfoApi, loginApi, seeConnectionClose } from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
import { doLogout, getUserInfoApi, loginApi, seeConnectionClose } from '#/api'
|
||||
import { $t } from '#/locales'
|
||||
|
||||
import { useDictStore } from './dict';
|
||||
import { useDictStore } from './dict'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const accessStore = useAccessStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const accessStore = useAccessStore()
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
const loginLoading = ref(false);
|
||||
const loginLoading = ref(false)
|
||||
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
@@ -33,30 +33,30 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
onSuccess?: () => Promise<void> | void,
|
||||
) {
|
||||
// 异步处理用户登录操作并获取 accessToken
|
||||
let userInfo: null | UserInfo = null;
|
||||
let userInfo: null | UserInfo = null
|
||||
try {
|
||||
loginLoading.value = true;
|
||||
const { access_token } = await loginApi(params);
|
||||
loginLoading.value = true
|
||||
const { access_token } = await loginApi(params)
|
||||
|
||||
// 将 accessToken 存储到 accessStore 中
|
||||
accessStore.setAccessToken(access_token);
|
||||
accessStore.setRefreshToken(access_token);
|
||||
accessStore.setAccessToken(access_token)
|
||||
accessStore.setRefreshToken(access_token)
|
||||
|
||||
// 获取用户信息并存储到 accessStore 中
|
||||
userInfo = await fetchUserInfo();
|
||||
userInfo = await fetchUserInfo()
|
||||
/**
|
||||
* 设置用户信息
|
||||
*/
|
||||
userStore.setUserInfo(userInfo);
|
||||
userStore.setUserInfo(userInfo)
|
||||
/**
|
||||
* 在这里设置权限
|
||||
*/
|
||||
accessStore.setAccessCodes(userInfo.permissions);
|
||||
accessStore.setAccessCodes(userInfo.permissions)
|
||||
|
||||
if (accessStore.loginExpired) {
|
||||
accessStore.setLoginExpired(false);
|
||||
accessStore.setLoginExpired(false)
|
||||
} else {
|
||||
onSuccess ? await onSuccess?.() : await router.push('/analytics');
|
||||
onSuccess ? await onSuccess?.() : await router.push('/analytics')
|
||||
}
|
||||
|
||||
if (userInfo?.realName) {
|
||||
@@ -64,48 +64,48 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
||||
duration: 3,
|
||||
message: $t('authentication.loginSuccess'),
|
||||
});
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
loginLoading.value = false;
|
||||
loginLoading.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function logout(redirect: boolean = true) {
|
||||
try {
|
||||
await seeConnectionClose();
|
||||
await doLogout();
|
||||
await seeConnectionClose()
|
||||
await doLogout()
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error(error)
|
||||
} finally {
|
||||
resetAllStores();
|
||||
accessStore.setLoginExpired(false);
|
||||
resetAllStores()
|
||||
accessStore.setLoginExpired(false)
|
||||
|
||||
// 回登陆页带上当前路由地址
|
||||
await router.replace({
|
||||
path: LOGIN_PATH,
|
||||
query: redirect
|
||||
? {
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
}
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
}
|
||||
: {},
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUserInfo() {
|
||||
const backUserInfo = await getUserInfoApi();
|
||||
const backUserInfo = await getUserInfoApi()
|
||||
/**
|
||||
* 登录超时的情况
|
||||
*/
|
||||
if (!backUserInfo) {
|
||||
throw new Error('获取用户信息失败.');
|
||||
throw new Error('获取用户信息失败.')
|
||||
}
|
||||
const { permissions = [], roles = [], user } = backUserInfo;
|
||||
const { permissions = [], roles = [], user } = backUserInfo
|
||||
/**
|
||||
* 从后台user -> vben user转换
|
||||
*/
|
||||
@@ -116,19 +116,20 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
roles,
|
||||
userId: user.userId,
|
||||
username: user.userName,
|
||||
};
|
||||
userStore.setUserInfo(userInfo);
|
||||
}
|
||||
userStore.setUserInfo(userInfo)
|
||||
|
||||
/**
|
||||
* 需要重新加载字典
|
||||
* 比如退出登录切换到其他租户
|
||||
*/
|
||||
const dictStore = useDictStore();
|
||||
dictStore.resetCache();
|
||||
return userInfo;
|
||||
const dictStore = useDictStore()
|
||||
dictStore.resetCache()
|
||||
return userInfo
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
loginLoading.value = false;
|
||||
loginLoading.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -137,5 +138,5 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
fetchUserInfo,
|
||||
loginLoading,
|
||||
logout,
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
|
@@ -1,21 +1,21 @@
|
||||
import type { NotificationItem } from '@vben/layouts';
|
||||
import type { NotificationItem } from '@vben/layouts'
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { SvgMessageUrl } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { useAppConfig } from '@vben/hooks'
|
||||
import { SvgMessageUrl } from '@vben/icons'
|
||||
import { $t } from '@vben/locales'
|
||||
import { useAccessStore, useUserStore } from '@vben/stores'
|
||||
|
||||
import { useEventSource } from '@vueuse/core';
|
||||
import { notification } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useEventSource } from '@vueuse/core'
|
||||
import { notification } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
const { apiURL, clientId, sseEnable } = useAppConfig(
|
||||
import.meta.env,
|
||||
import.meta.env.PROD,
|
||||
);
|
||||
)
|
||||
|
||||
export const useNotifyStore = defineStore(
|
||||
'app-notify',
|
||||
@@ -23,18 +23,18 @@ export const useNotifyStore = defineStore(
|
||||
/**
|
||||
* return才会被持久化 存储全部消息
|
||||
*/
|
||||
const notificationList = ref<NotificationItem[]>([]);
|
||||
const sseList = ref<string[]>(["111"]);
|
||||
const userStore = useUserStore();
|
||||
const notificationList = ref<NotificationItem[]>([])
|
||||
const sseList = ref<string[]>(["111"])
|
||||
const userStore = useUserStore()
|
||||
const userId = computed(() => {
|
||||
return userStore.userInfo?.userId || '0';
|
||||
});
|
||||
return userStore.userInfo?.userId || '0'
|
||||
})
|
||||
|
||||
const notifications = computed(() => {
|
||||
return notificationList.value.filter(
|
||||
(item) => item.userId === userId.value,
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* 开始监听sse消息
|
||||
@@ -44,40 +44,45 @@ export const useNotifyStore = defineStore(
|
||||
* 未开启 不监听
|
||||
*/
|
||||
if (!sseEnable) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const accessStore = useAccessStore();
|
||||
const token = accessStore.accessToken;
|
||||
const accessStore = useAccessStore()
|
||||
const token = accessStore.accessToken
|
||||
|
||||
const sseAddr = `${apiURL}/resource/sse?clientid=${clientId}&Authorization=Bearer ${token}`;
|
||||
const sseAddr = `${apiURL}/resource/sse?clientid=${clientId}&Authorization=Bearer ${token}`
|
||||
|
||||
const { data } = useEventSource(sseAddr, [], {
|
||||
autoReconnect: {
|
||||
delay: 1000,
|
||||
onFailed() {
|
||||
console.error('sse重连失败.');
|
||||
console.error('sse重连失败.')
|
||||
},
|
||||
retries: 3,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
watch(data, (message) => {
|
||||
if (!message) return;
|
||||
console.log(`接收到消息: ${message}`);
|
||||
if (!message) return
|
||||
console.log(`接收到消息: ${message}`)
|
||||
|
||||
try {
|
||||
// 尝试解析JSON
|
||||
const obj = JSON.parse(message);
|
||||
const obj = JSON.parse(message)
|
||||
// 检查解析结果是否为对象且不为null
|
||||
if (obj.getType() ==="yvjin"){
|
||||
sseList.value.join(message)
|
||||
if (obj.type === "yvjin") {
|
||||
sseList.value.push(message)
|
||||
}
|
||||
|
||||
// 仪表数据
|
||||
if (obj.type === "meter") {
|
||||
sseList.value.push(message)
|
||||
}
|
||||
} catch (e) {
|
||||
notification.success({
|
||||
description: message,
|
||||
duration: 3,
|
||||
message: $t('component.notice.received'),
|
||||
});
|
||||
})
|
||||
|
||||
notificationList.value.unshift({
|
||||
// avatar: `https://api.multiavatar.com/${random(0, 10_000)}.png`, 随机头像
|
||||
@@ -87,12 +92,12 @@ export const useNotifyStore = defineStore(
|
||||
message,
|
||||
title: $t('component.notice.title'),
|
||||
userId: userId.value,
|
||||
});
|
||||
})
|
||||
|
||||
// 需要手动置空 vue3在值相同时不会触发watch
|
||||
data.value = null;
|
||||
data.value = null
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,10 +107,10 @@ export const useNotifyStore = defineStore(
|
||||
notificationList.value
|
||||
.filter((item) => item.userId === userId.value)
|
||||
.forEach((item) => {
|
||||
item.isRead = true;
|
||||
});
|
||||
item.isRead = true
|
||||
})
|
||||
}
|
||||
function getsseList(){
|
||||
function getsseList() {
|
||||
console.log(sseList.value)
|
||||
return sseList.value
|
||||
}
|
||||
@@ -115,7 +120,7 @@ export const useNotifyStore = defineStore(
|
||||
* @param item 通知
|
||||
*/
|
||||
function setRead(item: NotificationItem) {
|
||||
!item.isRead && (item.isRead = true);
|
||||
!item.isRead && (item.isRead = true)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +129,7 @@ export const useNotifyStore = defineStore(
|
||||
function clearAllMessage() {
|
||||
notificationList.value = notificationList.value.filter(
|
||||
(item) => item.userId !== userId.value,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +146,7 @@ export const useNotifyStore = defineStore(
|
||||
notificationList.value
|
||||
.filter((item) => item.userId === userId.value)
|
||||
.some((item) => !item.isRead),
|
||||
);
|
||||
)
|
||||
|
||||
return {
|
||||
$reset,
|
||||
@@ -154,11 +159,11 @@ export const useNotifyStore = defineStore(
|
||||
setRead,
|
||||
showDot,
|
||||
startListeningMessage,
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
pick: ['notificationList'],
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
|
@@ -2,12 +2,46 @@
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { onMounted, ref, defineExpose } from 'vue';
|
||||
import { meterRecordTrend } from '#/api/property/energyManagement/meterRecord';
|
||||
import dayjs from 'dayjs';
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
// 添加日期选择相关逻辑
|
||||
const selectedDate = ref<string>(dayjs().format('YYYY-MM'));
|
||||
// 获取当月天数
|
||||
const getDaysInMonth = (date: any) => {
|
||||
const year = parseInt(date.split('-')[0]);
|
||||
const month = parseInt(date.split('-')[1]);
|
||||
const daysInMonth = new Date(year, month, 0).getDate();
|
||||
return Array.from({ length: daysInMonth }, (_, i) => i + 1);
|
||||
};
|
||||
const getMeterRecordTrend = async (selectedDate: any) => {
|
||||
const res = await meterRecordTrend({
|
||||
day: dayjs().format('YYYY-MM-DD'),
|
||||
month: selectedDate.value,
|
||||
year: dayjs().format('YYYY'),
|
||||
meterType: 1,
|
||||
meterId: null,
|
||||
floorId: null,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 处理返回的数据
|
||||
const chartData = res.day.nowMonth.data || [];
|
||||
|
||||
// 创建一个映射,将日期和数值对应起来
|
||||
const dataMap: Record<string, string> = {};
|
||||
chartData.forEach(([day, value]: [string, string]) => {
|
||||
dataMap[day] = value;
|
||||
});
|
||||
|
||||
// 获取当月所有日期
|
||||
const days = getDaysInMonth(selectedDate.value);
|
||||
|
||||
// 为没有数据的日期填充0,构建完整的数据数组
|
||||
const seriesData = days.map((day) => {
|
||||
return parseFloat(dataMap[day.toString()] || '0');
|
||||
});
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
@@ -19,29 +53,13 @@ onMounted(() => {
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
|
||||
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
|
||||
111,
|
||||
],
|
||||
data: seriesData,
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
|
||||
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#019680',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
@@ -65,7 +83,7 @@ onMounted(() => {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
data: days,
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
@@ -80,7 +98,7 @@ onMounted(() => {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: 80_000,
|
||||
// max: 800,
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
@@ -89,9 +107,18 @@ onMounted(() => {
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
onMounted(async () => {
|
||||
getMeterRecordTrend(selectedDate);
|
||||
});
|
||||
// 暴露方法给父组件调用
|
||||
defineExpose({
|
||||
getMeterRecordTrend,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
<div>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -2,77 +2,79 @@
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { watch, onMounted, ref, defineProps, computed } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
const props = defineProps<{
|
||||
statisticsData?: any[]; // 根据实际数据结构调整类型
|
||||
}>();
|
||||
// 在组件顶层定义 computed 属性
|
||||
const chartData = computed(() => {
|
||||
if (!props.statisticsData) return [];
|
||||
return props.statisticsData.map((item) => ({
|
||||
name: item.typeName,
|
||||
value: item.total,
|
||||
}));
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 监听数据变化,重新渲染图表
|
||||
watch(
|
||||
() => props.statisticsData,
|
||||
(newData) => {
|
||||
if (newData) {
|
||||
updateChart();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 封装图表更新逻辑
|
||||
const updateChart = () => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: 0,
|
||||
data: ['访问', '趋势'],
|
||||
},
|
||||
radar: {
|
||||
indicator: [
|
||||
{
|
||||
name: '网页',
|
||||
},
|
||||
{
|
||||
name: '移动端',
|
||||
},
|
||||
{
|
||||
name: 'Ipad',
|
||||
},
|
||||
{
|
||||
name: '客户端',
|
||||
},
|
||||
{
|
||||
name: '第三方',
|
||||
},
|
||||
{
|
||||
name: '其它',
|
||||
},
|
||||
],
|
||||
radius: '60%',
|
||||
splitNumber: 8,
|
||||
bottom: '2%',
|
||||
left: 'center',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {
|
||||
opacity: 1,
|
||||
shadowBlur: 0,
|
||||
shadowColor: 'rgba(0,0,0,.2)',
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 10,
|
||||
animationDelay() {
|
||||
return Math.random() * 100;
|
||||
},
|
||||
data: [
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#b6a2de',
|
||||
},
|
||||
name: '访问',
|
||||
value: [90, 50, 86, 40, 50, 20],
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
avoidLabelOverlap: false,
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: chartData.value,
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold',
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
name: '趋势',
|
||||
value: [70, 75, 70, 76, 20, 85],
|
||||
},
|
||||
],
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
symbolSize: 0,
|
||||
type: 'radar',
|
||||
label: {
|
||||
position: 'center',
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
name: '预警类型',
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 组件挂载时尝试更新图表
|
||||
if (props.statisticsData) {
|
||||
updateChart();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -2,12 +2,34 @@
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { watch, onMounted, ref, defineProps, computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
workData?: any[]; // 根据实际数据结构调整类型
|
||||
}>();
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
const chartData = computed(() => {
|
||||
if (!props.workData) return [];
|
||||
return props.workData.map((item) => ({
|
||||
name: item.type,
|
||||
value: item.quantity,
|
||||
}));
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 监听数据变化,重新渲染图表
|
||||
watch(
|
||||
() => props.workData,
|
||||
(newData) => {
|
||||
if (newData) {
|
||||
updateChart();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 封装图表更新逻辑
|
||||
const updateChart = () => {
|
||||
renderEcharts({
|
||||
series: [
|
||||
{
|
||||
@@ -18,15 +40,8 @@ onMounted(() => {
|
||||
animationType: 'scale',
|
||||
center: ['50%', '50%'],
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '外包', value: 500 },
|
||||
{ name: '定制', value: 310 },
|
||||
{ name: '技术支持', value: 274 },
|
||||
{ name: '远程', value: 400 },
|
||||
].sort((a, b) => {
|
||||
return a.value - b.value;
|
||||
}),
|
||||
name: '商业占比',
|
||||
data: chartData.value,
|
||||
name: '工单类型',
|
||||
radius: '80%',
|
||||
roseType: 'radius',
|
||||
type: 'pie',
|
||||
@@ -37,6 +52,13 @@ onMounted(() => {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 组件挂载时尝试更新图表
|
||||
if (props.workData) {
|
||||
updateChart();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -2,12 +2,41 @@
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { watch, onMounted, ref, defineProps, computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
statusData?: any[]; // 根据实际数据结构调整类型
|
||||
}>();
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
const chartData = computed(() => {
|
||||
if (!props.statusData) return [];
|
||||
return props.statusData.map((item) => ({
|
||||
name:
|
||||
item.state == 0
|
||||
? '使用中'
|
||||
: item.state == 1
|
||||
? '停用中'
|
||||
: item.state == 2
|
||||
? '已报废'
|
||||
: '闲置中',
|
||||
value: item.quantity,
|
||||
}));
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 监听数据变化,重新渲染图表
|
||||
watch(
|
||||
() => props.statusData,
|
||||
(newData) => {
|
||||
if (newData) {
|
||||
updateChart();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 封装图表更新逻辑
|
||||
const updateChart = () => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: '2%',
|
||||
@@ -22,12 +51,7 @@ onMounted(() => {
|
||||
animationType: 'scale',
|
||||
avoidLabelOverlap: false,
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '搜索引擎', value: 1048 },
|
||||
{ name: '直接访问', value: 735 },
|
||||
{ name: '邮件营销', value: 580 },
|
||||
// { name: '联盟广告', value: 484 },
|
||||
],
|
||||
data: chartData.value,
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: '12',
|
||||
@@ -47,7 +71,7 @@ onMounted(() => {
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
name: '访问来源',
|
||||
name: '设备状态',
|
||||
radius: ['40%', '65%'],
|
||||
type: 'pie',
|
||||
},
|
||||
@@ -56,6 +80,14 @@ onMounted(() => {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
console.log('Child component mounted, props:', props.statusData);
|
||||
// 组件挂载时尝试更新图表
|
||||
if (props.statusData) {
|
||||
updateChart();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -1,12 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||
import type { TabOption } from '@vben/types';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
AnalysisChartsTabs,
|
||||
AnalysisOverview,
|
||||
} from '@vben/common-ui';
|
||||
import { AnalysisChartCard, AnalysisOverview } from '@vben/common-ui';
|
||||
import {
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
@@ -18,72 +11,192 @@ import AnalyticsTrends from './analytics-trends.vue';
|
||||
import AnalyticsVisitsData from './analytics-visits-data.vue';
|
||||
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
|
||||
import AnalyticsVisitsSource from './analytics-visits-source.vue';
|
||||
import AnalyticsVisits from './analytics-visits.vue';
|
||||
|
||||
const overviewItems: AnalysisOverviewItem[] = [
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { DatePicker } from 'ant-design-vue';
|
||||
import { Radio } from 'ant-design-vue';
|
||||
import {
|
||||
getIndexCount,
|
||||
getStatisticsCurrDay,
|
||||
getStatistics,
|
||||
} from '#/api/analytics/index';
|
||||
const overviewItems = ref<any[]>([
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '用户量',
|
||||
totalTitle: '总用户量',
|
||||
totalValue: 78,
|
||||
value: 15,
|
||||
title: '今日访客量',
|
||||
totalTitle: '总访客量',
|
||||
totalValue: 0,
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '访问量',
|
||||
totalTitle: '总访问量',
|
||||
totalValue: 2_278,
|
||||
value: 461,
|
||||
title: '今日车流量',
|
||||
totalTitle: '总车流量',
|
||||
totalValue: 0,
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '下载量',
|
||||
totalTitle: '总下载量',
|
||||
totalValue: 17,
|
||||
value: 2,
|
||||
title: '今日预警数量',
|
||||
totalTitle: '总预警量',
|
||||
totalValue: 0,
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '使用量',
|
||||
totalTitle: '总使用量',
|
||||
totalValue: 6_652,
|
||||
value: 3_739,
|
||||
title: '今日工单数量',
|
||||
totalTitle: '总工单量',
|
||||
totalValue: 0,
|
||||
value: 0,
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
const chartTabs: TabOption[] = [
|
||||
{
|
||||
label: '流量趋势',
|
||||
value: 'trends',
|
||||
},
|
||||
{
|
||||
label: '月访问量',
|
||||
value: 'visits',
|
||||
},
|
||||
];
|
||||
//tab选择
|
||||
const timeUnit = ref(1);
|
||||
// 月份选择
|
||||
const selectedDate = ref<any>(null);
|
||||
const paramDate = ref(null);
|
||||
|
||||
const statisticsList = ref<any>();
|
||||
const workList = ref<any>();
|
||||
const statusDataList = ref<any>();
|
||||
const analyticsTrendsRef = ref<InstanceType<typeof AnalyticsTrends> | null>(
|
||||
null,
|
||||
);
|
||||
const handleDateChange = (date: any) => {
|
||||
paramDate.value = date.format('YYYY-MM');
|
||||
// 调用子组件的方法
|
||||
if (
|
||||
analyticsTrendsRef.value &&
|
||||
analyticsTrendsRef.value.getMeterRecordTrend
|
||||
) {
|
||||
analyticsTrendsRef.value.getMeterRecordTrend(paramDate);
|
||||
}
|
||||
};
|
||||
|
||||
// 工单数量
|
||||
async function getIndex() {
|
||||
const res = await getIndexCount();
|
||||
overviewItems.value[3].value = res.workOrdersToday;
|
||||
overviewItems.value[3].totalValue = res.workOrdersTotal;
|
||||
overviewItems.value[0].value = res.visitorsToday;
|
||||
overviewItems.value[0].totalValue = res.visitorsTotal;
|
||||
workList.value = res.satisfactionChartList;
|
||||
statusDataList.value = res.statusChartVoChartList;
|
||||
}
|
||||
// 车流量
|
||||
async function getCarCount() {
|
||||
const startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
const endDate = new Date();
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
// 转换为正确的 ISO 格式,考虑时区偏移
|
||||
const toLocalISOString = (date: Date) => {
|
||||
const timezoneOffset = date.getTimezoneOffset() * 60000; // 转换为毫秒
|
||||
const localTime = new Date(date.getTime() - timezoneOffset);
|
||||
return localTime.toISOString().slice(0, -1) + 'Z';
|
||||
};
|
||||
const response = await fetch(
|
||||
'https://server.cqnctc.com:6081/web/thirdParty/queryInParkData',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pageReq: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
plNos: ['PFN000000012', 'PFN000000022', 'PFN000000025'],
|
||||
parkInBeginTime: toLocalISOString(startDate),
|
||||
parkInEndTime: toLocalISOString(endDate),
|
||||
orgId: 10012,
|
||||
parkOrderTypes: [100, 200, 201, 300, 500],
|
||||
terminalSource: 50,
|
||||
remark: '',
|
||||
}),
|
||||
},
|
||||
);
|
||||
const result = await response.json();
|
||||
overviewItems.value[1].value = result.data.todayCount;
|
||||
|
||||
overviewItems.value[1].totalValue = result.data.allCount;
|
||||
}
|
||||
// 预警
|
||||
async function getStatisticsCurrDayCount() {
|
||||
const res = await getStatisticsCurrDay();
|
||||
let total = 0;
|
||||
for (const item of res) {
|
||||
total += item.total;
|
||||
}
|
||||
overviewItems.value[2].value = total;
|
||||
}
|
||||
async function getStatisticsCount() {
|
||||
const res = await getStatistics();
|
||||
let total = 0;
|
||||
for (const item of res) {
|
||||
total += item.total;
|
||||
}
|
||||
overviewItems.value[2].totalValue = total;
|
||||
statisticsList.value = res;
|
||||
}
|
||||
// 电量
|
||||
|
||||
onMounted(() => {
|
||||
getIndex();
|
||||
getCarCount();
|
||||
getStatisticsCurrDayCount();
|
||||
getStatisticsCount();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
<!-- <AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
<template #trends>
|
||||
<AnalyticsTrends />
|
||||
</template>
|
||||
<template #visits>
|
||||
<AnalyticsVisits />
|
||||
</template>
|
||||
</AnalysisChartsTabs>
|
||||
|
||||
</AnalysisChartsTabs> -->
|
||||
<div class="mt-5 rounded-lg bg-white">
|
||||
<div
|
||||
class="flex items-center justify-between p-5"
|
||||
style="width: 100%; height: 100px"
|
||||
>
|
||||
<Radio.Group v-model:value="timeUnit" size="large">
|
||||
<Radio.Button value="1">电量</Radio.Button>
|
||||
</Radio.Group>
|
||||
<DatePicker
|
||||
v-model:value="selectedDate"
|
||||
picker="month"
|
||||
placeholder="请选择月份"
|
||||
style="width: 150px"
|
||||
@change="handleDateChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="timeUnit == 1">
|
||||
<AnalyticsTrends
|
||||
ref="analyticsTrendsRef"
|
||||
:selected-date="selectedDate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 w-full md:flex">
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
|
||||
<AnalyticsVisitsData />
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="预警类型">
|
||||
<AnalyticsVisitsData :statisticsData="statisticsList" />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
<AnalysisChartCard
|
||||
class="mt-5 md:mr-4 md:mt-0 md:w-1/3"
|
||||
title="设备使用状态"
|
||||
>
|
||||
<AnalyticsVisitsSource :statusData="statusDataList" />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSales />
|
||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="工单类型">
|
||||
<AnalyticsVisitsSales :workData="workList" />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -23,7 +23,7 @@ const [BasicForm, formApi] = useVbenForm({
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-1',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 80,
|
||||
labelWidth: 90,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
|
@@ -13,7 +13,7 @@ let arr: CommunityVO[] = [];
|
||||
const communitySelect: VbenFormSchema = {
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'communityId',
|
||||
label: '社区',
|
||||
label: '园区名称',
|
||||
componentProps: {
|
||||
resultField: 'list', // 根据API返回结构调整
|
||||
labelField: 'communityName',
|
||||
@@ -45,6 +45,10 @@ export const querySchema: FormSchemaGetter = () => [
|
||||
// export const columns: () => VxeGridProps['columns'] = () => [
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
title: '园区名称',
|
||||
field: 'communityText',
|
||||
},
|
||||
{
|
||||
title: '建筑名称',
|
||||
field: 'buildingName',
|
||||
@@ -69,9 +73,21 @@ export const columns: VxeGridProps['columns'] = [
|
||||
title: '竣工日期',
|
||||
field: 'completionDate',
|
||||
},
|
||||
// {
|
||||
// title: '地址',
|
||||
// field: 'addr',
|
||||
// },
|
||||
{
|
||||
title: '地址',
|
||||
field: 'addr',
|
||||
title: '建筑面积(㎡)',
|
||||
field: 'area',
|
||||
},
|
||||
{
|
||||
title: '套内面积(㎡)',
|
||||
field: 'insideInArea',
|
||||
},
|
||||
{
|
||||
title: '公摊面积(㎡)',
|
||||
field: 'sharedArea',
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
@@ -107,8 +123,8 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
fieldName: 'floorCount',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min:1,
|
||||
precision:0,
|
||||
min: 1,
|
||||
precision: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -116,8 +132,8 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
fieldName: 'unitCount',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min:1,
|
||||
precision:0,
|
||||
min: 1,
|
||||
precision: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -134,8 +150,8 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
fieldName: 'elevatorCount',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min:0,
|
||||
precision:0,
|
||||
min: 0,
|
||||
precision: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -147,10 +163,37 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
},
|
||||
},
|
||||
// {
|
||||
// label: '地址',
|
||||
// fieldName: 'addr',
|
||||
// component: 'Input',
|
||||
// rules: 'required',
|
||||
// },
|
||||
{
|
||||
label: '地址',
|
||||
fieldName: 'addr',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
label: '建筑面积(㎡)',
|
||||
fieldName: 'area',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '套内面积(㎡)',
|
||||
fieldName: 'insideInArea',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '公摊面积(㎡)',
|
||||
fieldName: 'sharedArea',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@@ -1,19 +1,24 @@
|
||||
import type {FormSchemaGetter} from '#/adapter/form';
|
||||
import type {VxeGridProps} from '#/adapter/vxe-table';
|
||||
import {getPopupContainer} from '@vben/utils';
|
||||
import {getDictOptions} from '#/utils/dict';
|
||||
import {DictEnum} from '@vben/constants';
|
||||
import {renderDict} from "#/utils/render";
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
import { getDictOptions } from '#/utils/dict';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
// {
|
||||
// component: 'Select',
|
||||
// componentProps: {
|
||||
// getPopupContainer,
|
||||
// options: getDictOptions(DictEnum.wy_sqlx),
|
||||
// },
|
||||
// fieldName: 'communityType',
|
||||
// label: '园区类型',
|
||||
// },
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
getPopupContainer,
|
||||
options: getDictOptions(DictEnum.wy_sqlx),
|
||||
},
|
||||
fieldName: 'communityType',
|
||||
label: '社区类型',
|
||||
component: 'Input',
|
||||
fieldName: 'communityName',
|
||||
label: '园区名称',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
@@ -25,20 +30,20 @@ export const querySchema: FormSchemaGetter = () => [
|
||||
// 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新
|
||||
// export const columns: () => VxeGridProps['columns'] = () => [
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{type: 'checkbox', width: 60},
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
title: '社区名称',
|
||||
title: '园区名称',
|
||||
field: 'communityName',
|
||||
},
|
||||
{
|
||||
title: '社区类型',
|
||||
field: 'communityType',
|
||||
slots: {
|
||||
default: ({row}) => {
|
||||
return renderDict(row.communityType, DictEnum.wy_sqlx)
|
||||
}
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '园区类型',
|
||||
// field: 'communityType',
|
||||
// slots: {
|
||||
// default: ({row}) => {
|
||||
// return renderDict(row.communityType, DictEnum.wy_sqlx)
|
||||
// }
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: '城市',
|
||||
field: 'cityFullName',
|
||||
@@ -72,13 +77,13 @@ export const columns: VxeGridProps['columns'] = [
|
||||
field: 'contactPhone',
|
||||
},
|
||||
{
|
||||
title: '社区描述',
|
||||
title: '园区描述',
|
||||
field: 'description',
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: {default: 'action'},
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
width: 180,
|
||||
},
|
||||
@@ -95,21 +100,21 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '社区名称',
|
||||
label: '园区名称',
|
||||
fieldName: 'communityName',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '社区类型',
|
||||
fieldName: 'communityType',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
getPopupContainer,
|
||||
options: getDictOptions(DictEnum.wy_sqlx),
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
// {
|
||||
// label: '园区类型',
|
||||
// fieldName: 'communityType',
|
||||
// component: 'Select',
|
||||
// componentProps: {
|
||||
// getPopupContainer,
|
||||
// options: getDictOptions(DictEnum.wy_sqlx),
|
||||
// },
|
||||
// rules: 'selectRequired',
|
||||
// },
|
||||
{
|
||||
component: 'TreeSelect',
|
||||
fieldName: 'cityFullCode',
|
||||
@@ -177,12 +182,12 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '社区描述',
|
||||
label: '园区描述',
|
||||
fieldName: 'description',
|
||||
component: 'Textarea',
|
||||
},
|
||||
{
|
||||
label: '社区/园区图片',
|
||||
label: '园区图片',
|
||||
fieldName: 'img',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
|
@@ -5,9 +5,12 @@ import {renderDict} from "#/utils/render";
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'searchValue',
|
||||
label: '搜索值',
|
||||
label: '费用类型',
|
||||
fieldName: 'costType',
|
||||
component: 'Select',
|
||||
componentProps: () => ({
|
||||
options: getDictOptions('wy_cbfylx'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -63,11 +63,6 @@ export const columns: VxeGridProps['columns'] = [
|
||||
},
|
||||
width:120
|
||||
},
|
||||
{
|
||||
title: '客服电话',
|
||||
field: 'serviceName',
|
||||
width:120
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
@@ -122,10 +117,10 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '客服电话',
|
||||
fieldName: 'serviceName',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
label: '反馈单位',
|
||||
fieldName: 'feedbackUnit',
|
||||
component: 'Select',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
label: '反馈内容',
|
||||
|
@@ -18,6 +18,7 @@ import type {FeedbacksVO} from "#/api/property/customerService/feedbacks/model";
|
||||
import {
|
||||
workOrdersTypeTree
|
||||
} from "#/api/property/businessManagement/workOrdersType";
|
||||
import {resident_unitList} from "#/api/property/resident/unit";
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
@@ -66,6 +67,7 @@ const [BasicModal, modalApi] = useVbenModal({
|
||||
const {id} = modalApi.getData() as { id?: number | string };
|
||||
isUpdate.value = !!id;
|
||||
await initWorkOrderTypeOption()
|
||||
await initUnitOption()
|
||||
if (isUpdate.value && id) {
|
||||
const record = await feedbacksInfo(id);
|
||||
detail.value = record
|
||||
@@ -127,6 +129,25 @@ async function initWorkOrderTypeOption() {
|
||||
]);
|
||||
}
|
||||
|
||||
async function initUnitOption() {
|
||||
let params = {
|
||||
pageSize: 1000,
|
||||
pageNum: 1
|
||||
}
|
||||
const res = await resident_unitList(params)
|
||||
formApi.updateSchema([{
|
||||
componentProps: () => ({
|
||||
options: res.rows,
|
||||
//开启搜索
|
||||
showSearch: true,
|
||||
//数据过滤字段
|
||||
optionFilterProp: 'name',
|
||||
//下拉显示字段
|
||||
fieldNames: {label: 'name', value: 'id'},
|
||||
}),
|
||||
fieldName: 'feedbackUnit',
|
||||
}])
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@@ -1,14 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from "vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { handleNode } from "@vben/utils";
|
||||
import { Empty, Skeleton, Tree } from "ant-design-vue";
|
||||
import { queryTree } from "#/api/property/energyManagement/meterInfo";
|
||||
import type { TreeNode } from "#/api/common";
|
||||
import type { PropType } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { handleNode } from '@vben/utils';
|
||||
import { Empty, Skeleton, Tree } from 'ant-design-vue';
|
||||
import { queryTree } from '#/api/property/energyManagement/meterInfo';
|
||||
import type { TreeNode } from '#/api/common';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
withDefaults(defineProps<{ showSearch?: boolean }>(), { showSearch: true });
|
||||
const props = defineProps({
|
||||
isMeter: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
@@ -23,13 +28,13 @@ const emit = defineEmits<{
|
||||
select: [selectedKeys: string[], info: any];
|
||||
}>();
|
||||
|
||||
const selectTreeId = defineModel("selectTreeId", {
|
||||
const selectTreeId = defineModel('selectTreeId', {
|
||||
type: Array as PropType<string[]>,
|
||||
});
|
||||
|
||||
const searchValue = defineModel("searchValue", {
|
||||
const searchValue = defineModel('searchValue', {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
});
|
||||
|
||||
const treeArray = ref<TreeNode[]>([]);
|
||||
@@ -38,8 +43,8 @@ const showTreeSkeleton = ref<boolean>(true);
|
||||
|
||||
async function loadTree() {
|
||||
showTreeSkeleton.value = true;
|
||||
searchValue.value = "";
|
||||
const ret = await queryTree(1);
|
||||
searchValue.value = '';
|
||||
const ret = await queryTree({ meterType: 1, isMeter: props.isMeter });
|
||||
handleNode(ret, 3);
|
||||
treeArray.value = ret;
|
||||
showTreeSkeleton.value = false;
|
||||
@@ -62,33 +67,46 @@ onMounted(loadTree);
|
||||
|
||||
<template>
|
||||
<div :class="$attrs.class">
|
||||
<Skeleton :loading="showTreeSkeleton"
|
||||
:paragraph="{ rows: 8 }"
|
||||
active
|
||||
class="p-[8px] flex-1 min-h-0">
|
||||
<div class="bg-background flex h-full flex-col overflow-y-auto rounded-lg">
|
||||
<Skeleton
|
||||
:loading="showTreeSkeleton"
|
||||
:paragraph="{ rows: 8 }"
|
||||
active
|
||||
class="min-h-0 flex-1 p-[8px]"
|
||||
>
|
||||
<div
|
||||
class="bg-background flex h-full flex-col overflow-y-auto rounded-lg"
|
||||
>
|
||||
<div class="h-full overflow-x-hidden px-[8px]">
|
||||
<Tree v-bind="$attrs"
|
||||
v-if="treeArray.length > 0"
|
||||
v-model:selected-keys="selectTreeId"
|
||||
:show-line="{ showLeafIcon: false }"
|
||||
:tree-data="treeArray"
|
||||
:virtual="false"
|
||||
default-expand-all
|
||||
@select="(selectedKeys, info) => $emit('select', selectedKeys, info)">
|
||||
<Tree
|
||||
v-bind="$attrs"
|
||||
v-if="treeArray.length > 0"
|
||||
v-model:selected-keys="selectTreeId"
|
||||
:show-line="{ showLeafIcon: false }"
|
||||
:tree-data="treeArray"
|
||||
:virtual="false"
|
||||
default-expand-all
|
||||
@select="
|
||||
(selectedKeys, info) => $emit('select', selectedKeys, info)
|
||||
"
|
||||
>
|
||||
<template #title="{ label }">
|
||||
<span v-if="label.indexOf(searchValue) > -1">
|
||||
{{ label.substring(0, label.indexOf(searchValue)) }}
|
||||
<span style="color: #f50">{{ searchValue }}</span>
|
||||
{{ label.substring(label.indexOf(searchValue) + searchValue.length) }}
|
||||
{{
|
||||
label.substring(
|
||||
label.indexOf(searchValue) + searchValue.length,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>{{ label }}</span>
|
||||
</template>
|
||||
</Tree>
|
||||
<div v-else
|
||||
class="mt-5">
|
||||
<Empty :image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="暂无数据" />
|
||||
<div v-else class="mt-5">
|
||||
<Empty
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="暂无数据"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,516 +1,289 @@
|
||||
<script setup lang="ts">
|
||||
import { RadioGroup, RadioButton, message } from 'ant-design-vue';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { ref, onMounted, onBeforeUnmount, reactive } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts, EChartsOption } from 'echarts';
|
||||
import { useNotifyStore } from '#/store';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import FloorTree from '../components/floor-tree.vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { meterRecordTrend } from '#/api/property/energyManagement/meterRecord';
|
||||
import { BackTop, message, Spin } from 'ant-design-vue';
|
||||
import { Page, VbenCountToAnimator } from '@vben/common-ui';
|
||||
import { ref, onMounted, onBeforeUnmount, reactive, watch } from 'vue';
|
||||
import { currentReading } from '#/api/property/energyManagement/meterInfo';
|
||||
|
||||
// 左边楼层用
|
||||
const selectFloorId = ref<string[]>([]);
|
||||
const notifyStore = useNotifyStore();
|
||||
const readingData = ref<any>({});
|
||||
const readingTime = ref('');
|
||||
const readingLoading = ref(false);
|
||||
|
||||
const chainData = reactive({
|
||||
todayEnergy: '231.78',
|
||||
yesterdaySamePeriodEnergy: '269.56',
|
||||
dayTrendPercentage: '-14.02%',
|
||||
dayTrendValue: '-37.78',
|
||||
currentMonthEnergy: '18758.39',
|
||||
lastMonthSamePeriodEnergy: '--',
|
||||
monthTrendPercentage: '--',
|
||||
monthTrendValue: '--',
|
||||
currentYearEnergy: '18758.39',
|
||||
lastYearSamePeriodEnergy: '--',
|
||||
yearTrendPercentage: '--',
|
||||
yearTrendValue: '--',
|
||||
onMounted(() => {
|
||||
notifyStore.startListeningMessage();
|
||||
// 监听sseList的变化
|
||||
watch(
|
||||
() => notifyStore.sseList,
|
||||
(val) => {
|
||||
const latestMessage = val[val.length - 1];
|
||||
|
||||
try {
|
||||
// 尝试解析消息内容
|
||||
const parsedMessage = JSON.parse(latestMessage);
|
||||
console.log('收到sse消息:', parsedMessage);
|
||||
if (parsedMessage.type === 'meter') {
|
||||
// 根据消息内容执行相应操作
|
||||
handleSSEMessage(parsedMessage);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('收到非JSON消息:', latestMessage);
|
||||
// 处理非JSON格式消息
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
});
|
||||
|
||||
const peakData = reactive({
|
||||
todayPeakPower: '2961.08',
|
||||
todayPeakTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
yesterdayPeakPower: '2993.89',
|
||||
yesterdayPeakTime: dayjs().subtract(1, 'day').format('YYYY-MM-DD HH:mm:ss'),
|
||||
});
|
||||
|
||||
const energyTrendTime = ref('1');
|
||||
|
||||
// 能耗趋势图表容器
|
||||
const energyTrendChart = ref<HTMLElement | null>(null);
|
||||
const energyTrendInstance = ref<ECharts | null>(null);
|
||||
|
||||
const energyTrendOption: EChartsOption = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
name: '时间',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: 'kW.h',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [],
|
||||
type: 'bar',
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: 'Max' },
|
||||
{ type: 'min', name: 'Min' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const initEnergyTrendChart = () => {
|
||||
if (energyTrendChart.value) {
|
||||
// 销毁旧实例
|
||||
energyTrendInstance.value?.dispose();
|
||||
// 创建新实例
|
||||
energyTrendInstance.value = echarts.init(energyTrendChart.value);
|
||||
// 设置配置项
|
||||
energyTrendInstance.value.setOption(energyTrendOption);
|
||||
// buildingEnergyTrendData('1');
|
||||
// 可选:添加窗口大小变化监听
|
||||
const resizeHandler = () => {
|
||||
energyTrendInstance.value?.resize();
|
||||
};
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
|
||||
// 在组件卸载前移除监听
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resizeHandler);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function buildingEnergyTrendData(val: string) {
|
||||
if (trendData.value.hour == null) {
|
||||
message.warning('请先选择楼层或电表!');
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let timeArr = [];
|
||||
let valArr = [];
|
||||
let name = '时间';
|
||||
if (val == '1') {
|
||||
const hour = now.getHours();
|
||||
for (let i = 0; i < hour; i++) {
|
||||
timeArr.push(i + ':00');
|
||||
}
|
||||
valArr = trendData.value.hour.today.data;
|
||||
} else if (val == '2') {
|
||||
const day = now.getDate();
|
||||
for (let i = 1; i < day; i++) {
|
||||
timeArr.push(i);
|
||||
}
|
||||
name = '日期';
|
||||
valArr = trendData.value.day.nowMonth.data;
|
||||
} else {
|
||||
const month = now.getMonth() + 1;
|
||||
for (let i = 1; i < month + 1; i++) {
|
||||
timeArr.push(i);
|
||||
}
|
||||
name = '月份';
|
||||
valArr = trendData.value.month.nowYear.data;
|
||||
}
|
||||
|
||||
if (energyTrendInstance.value) {
|
||||
energyTrendInstance.value.setOption({
|
||||
xAxis: { data: timeArr, name },
|
||||
series: [{ data: valArr }],
|
||||
});
|
||||
function handleSSEMessage(data: any) {
|
||||
if (data.data.length === 0) {
|
||||
message.warn('当前楼层暂无电表!');
|
||||
}
|
||||
readingData.value = data.data;
|
||||
readingTime.value = data.readingTime;
|
||||
readingLoading.value = false;
|
||||
}
|
||||
|
||||
//日用电率
|
||||
const powerCurveChart = ref<HTMLElement | null>(null);
|
||||
const powerCurveInstance = ref<ECharts | null>(null);
|
||||
|
||||
const powerCurveOption: EChartsOption = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
crossStyle: {
|
||||
color: '#999',
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
magicType: { show: true, type: ['line', 'bar'] },
|
||||
restore: { show: true },
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ['今日', '昨日'],
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: [
|
||||
'0:00',
|
||||
'1:00',
|
||||
'2:00',
|
||||
'3:00',
|
||||
'4:00',
|
||||
'5:00',
|
||||
'6:00',
|
||||
'7:00',
|
||||
'8:00',
|
||||
'9:00',
|
||||
'10:00',
|
||||
'11:00',
|
||||
'12:00',
|
||||
'13:00',
|
||||
'14:00',
|
||||
'15:00',
|
||||
'16:00',
|
||||
'17:00',
|
||||
'18:00',
|
||||
'19:00',
|
||||
'20:00',
|
||||
'21:00',
|
||||
'22:00',
|
||||
'23:00',
|
||||
],
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
name: '时',
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: 'kW',
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '昨日',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [],
|
||||
markPoint: {
|
||||
data: [{ type: 'max' }, { type: 'min' }],
|
||||
},
|
||||
itemStyle: { color: '#cbb0e3' }, // 数据点颜色
|
||||
lineStyle: { color: '#cbb0e3' }, // 线条颜色
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const initPowerCurveChart = () => {
|
||||
if (powerCurveChart.value) {
|
||||
// 销毁旧实例
|
||||
powerCurveInstance.value?.dispose();
|
||||
// 创建新实例
|
||||
powerCurveInstance.value = echarts.init(powerCurveChart.value);
|
||||
// 设置配置项
|
||||
powerCurveInstance.value.setOption(powerCurveOption);
|
||||
// 可选:添加窗口大小变化监听
|
||||
const resizeHandler = () => {
|
||||
powerCurveInstance.value?.resize();
|
||||
};
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
|
||||
// 在组件卸载前移除监听
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resizeHandler);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载后初始化图表
|
||||
onMounted(() => {
|
||||
initEnergyTrendChart();
|
||||
initPowerCurveChart();
|
||||
});
|
||||
|
||||
// 组件卸载前销毁图表实例
|
||||
onBeforeUnmount(() => {
|
||||
energyTrendInstance.value?.dispose();
|
||||
powerCurveInstance.value?.dispose();
|
||||
// 关闭页面,发送请求停止心跳
|
||||
currentReading({ meterType: 0, floorId: 0 });
|
||||
});
|
||||
|
||||
const trendData = ref<any>({});
|
||||
async function handleSelectFloor(selectedKeys, info) {
|
||||
const now = new Date();
|
||||
// 获取年、月、日
|
||||
const year = now.getFullYear();
|
||||
// 月份从0开始,所以要+1,并格式化为两位数
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
// 日期格式化为两位数
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
|
||||
let data = {
|
||||
day: year + '-' + month + '-' + day,
|
||||
month: year + '-' + month,
|
||||
year: year,
|
||||
if (typeof selectedKeys[0] === 'undefined') {
|
||||
return;
|
||||
}
|
||||
readingLoading.value = true;
|
||||
await currentReading({
|
||||
meterType: 1,
|
||||
meterId: null,
|
||||
floorId: null,
|
||||
};
|
||||
floorId: selectedKeys[0],
|
||||
});
|
||||
}
|
||||
|
||||
if (info.node.level == 3) {
|
||||
data.floorId = selectedKeys[0];
|
||||
} else {
|
||||
data.meterId = selectedKeys[0];
|
||||
}
|
||||
|
||||
const trend = await meterRecordTrend(data);
|
||||
trendData.value = trend;
|
||||
|
||||
// 趋势曲线
|
||||
let timeArr = [];
|
||||
let valArr = [];
|
||||
let name = '时间';
|
||||
if (energyTrendTime.value == '1') {
|
||||
const hour = now.getHours();
|
||||
for (let i = 0; i < hour; i++) {
|
||||
timeArr.push(i + ':00');
|
||||
}
|
||||
valArr = trend.hour.today.data;
|
||||
} else if (energyTrendTime.value == '2') {
|
||||
const day = now.getDate();
|
||||
for (let i = 1; i < day; i++) {
|
||||
timeArr.push(i);
|
||||
}
|
||||
name = '日期';
|
||||
valArr = trend.day.nowMonth.data;
|
||||
} else {
|
||||
const month = now.getMonth() + 1;
|
||||
for (let i = 1; i < month + 1; i++) {
|
||||
timeArr.push(i);
|
||||
}
|
||||
name = '月份';
|
||||
valArr = trend.month.nowYear.data;
|
||||
}
|
||||
if (energyTrendInstance.value) {
|
||||
energyTrendInstance.value.setOption({
|
||||
xAxis: { data: timeArr, name },
|
||||
series: [{ data: valArr }],
|
||||
});
|
||||
}
|
||||
|
||||
if (powerCurveInstance.value) {
|
||||
powerCurveInstance.value.setOption({
|
||||
series: [
|
||||
{
|
||||
name: '今日',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: trend.hour.today.data,
|
||||
markPoint: {
|
||||
data: [{ type: 'max' }, { type: 'min' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '昨日',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: trend.hour.yesterday.data,
|
||||
markPoint: {
|
||||
data: [{ type: 'max' }, { type: 'min' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
function targetFn() {
|
||||
return document.getElementById('right-panel');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-[8px]">
|
||||
<FloorTree class="w-[260px]" @select="handleSelectFloor"></FloorTree>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div class="row">
|
||||
<div class="energy-trend-container">
|
||||
<div class="energy-trend-top">
|
||||
<div class="section-header">
|
||||
<div class="header-title">能耗趋势</div>
|
||||
<FloorTree
|
||||
:isMeter="false"
|
||||
class="w-[260px]"
|
||||
@select="handleSelectFloor"
|
||||
></FloorTree>
|
||||
<div class="flex-1" id="right-panel">
|
||||
<Spin :spinning="readingLoading" size="large" style="height: 100px">
|
||||
<div class="cards-container">
|
||||
<div v-for="item in readingData" class="meterInfo-card">
|
||||
<h2>
|
||||
{{ item.meterName }}
|
||||
</h2>
|
||||
<div class="meterInfo-reading">
|
||||
<p>
|
||||
<VbenCountToAnimator
|
||||
:end-val="item.initReading"
|
||||
:decimals="2"
|
||||
prefix=""
|
||||
/>
|
||||
</p>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="meterInfo-list">
|
||||
<ul>
|
||||
<li>
|
||||
<span>采集时间:</span>
|
||||
<span>{{ readingTime }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>设备位置:</span>
|
||||
<span>{{ item.installLocation }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="button-get-plan">
|
||||
<a
|
||||
v-if="item.communicationState !== 0"
|
||||
style="background: #6bb1e3"
|
||||
>
|
||||
<span>在线</span>
|
||||
</a>
|
||||
<a v-else style="background: orange">
|
||||
<span>离线</span>
|
||||
</a>
|
||||
</div>
|
||||
<RadioGroup
|
||||
v-model:value="energyTrendTime"
|
||||
button-style="solid"
|
||||
size="small"
|
||||
@change="buildingEnergyTrendData(energyTrendTime)"
|
||||
>
|
||||
<RadioButton value="1">当日</RadioButton>
|
||||
<RadioButton value="2">当月</RadioButton>
|
||||
<RadioButton value="3">当年</RadioButton>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div class="chart-placeholder" ref="energyTrendChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="power-curve-container">
|
||||
<div class="section-header">
|
||||
<div class="header-title">平均电功率曲线</div>
|
||||
</div>
|
||||
<div class="power-chart" ref="powerCurveChart"></div>
|
||||
</div>
|
||||
<!-- <div class="power-peak-container">
|
||||
<div class="section-header">
|
||||
<div class="header-title">电功率峰值</div>
|
||||
</div>
|
||||
<div class="peak-item">
|
||||
<p class="value">{{ peakData.todayPeakPower }}</p>
|
||||
<p class="time">{{ peakData.todayPeakTime }}</p>
|
||||
<div class="bottom-text">当日(kW)</div>
|
||||
</div>
|
||||
<div class="peak-item">
|
||||
<p class="value">{{ peakData.yesterdayPeakPower }}</p>
|
||||
<p class="time">{{ peakData.yesterdayPeakTime }}</p>
|
||||
<div class="bottom-text">昨日(kW)</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
<BackTop class="back-to-top" :target="targetFn"></BackTop>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.comparison-section-container {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.energy-trend-container {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
.power-curve-container {
|
||||
margin-top: 10px;
|
||||
flex: 5;
|
||||
}
|
||||
|
||||
.power-peak-container {
|
||||
margin-top: 10px;
|
||||
/* 右侧内容区域样式 */
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
overflow-y: auto; /* 添加垂直滚动条 */
|
||||
padding: 10px;
|
||||
height: 100%; /* 确保高度占满父容器 */
|
||||
box-sizing: border-box; /* 包括padding在高度计算中 */
|
||||
}
|
||||
|
||||
.energy-trend-top {
|
||||
/* 卡片容器样式 */
|
||||
.cards-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
gap: 20px; /* 使用gap替代margin控制间距 */
|
||||
}
|
||||
|
||||
.section-header {
|
||||
border-left: 4px solid #3671e8;
|
||||
margin-bottom: 15px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.comparison-section-container,
|
||||
.energy-trend-container,
|
||||
.power-curve-container,
|
||||
.power-peak-container {
|
||||
.meterInfo-card {
|
||||
background: #fff;
|
||||
width: 15rem;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 20px;
|
||||
border-radius: 10px;
|
||||
border-bottom: 4px solid #87ceeb;
|
||||
box-shadow: 0 6px 30px rgba(173, 216, 230, 0.3);
|
||||
font-family: 'Poppins', sans-serif;
|
||||
height: 270px;
|
||||
margin: 0 5px 20px 5px;
|
||||
}
|
||||
|
||||
.meterInfo-card h2 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 27px;
|
||||
color: #5faee3;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.meterInfo-card h2 span {
|
||||
display: block;
|
||||
margin-top: -4px;
|
||||
color: #7eb8da;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.meterInfo-reading {
|
||||
position: relative;
|
||||
background: #f0f8ff;
|
||||
width: 14.46rem;
|
||||
margin-left: -0.65rem;
|
||||
padding: 0.2rem 1.2rem;
|
||||
border-radius: 5px 0 0 5px;
|
||||
}
|
||||
|
||||
.meterInfo-reading p {
|
||||
margin: 0;
|
||||
padding-top: 0.4rem;
|
||||
display: flex;
|
||||
font-size: 1.9rem;
|
||||
font-weight: 500;
|
||||
color: #4a8cbb;
|
||||
}
|
||||
|
||||
.meterInfo-reading p:before {
|
||||
content: '';
|
||||
margin-right: 5px;
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.meterInfo-reading p:after {
|
||||
content: '/ KW.H';
|
||||
margin-left: 5px;
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.meterInfo-reading div {
|
||||
position: absolute;
|
||||
bottom: -23px;
|
||||
right: 0px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 13px solid #87ceeb;
|
||||
border-bottom: 10px solid transparent;
|
||||
border-right: 13px solid transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.meterInfo-list {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.meterInfo-list ul {
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.meterInfo-list ul li {
|
||||
color: #5a7d9a;
|
||||
list-style: none;
|
||||
margin-bottom: 0.2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.meterInfo-list ul li svg {
|
||||
width: 0.9rem;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.meterInfo-list ul li span {
|
||||
font-weight: 300;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.button-get-plan {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
.button-get-plan a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
color: #fff;
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 1rem;
|
||||
text-decoration: none;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 5px;
|
||||
.button-get-plan a:hover {
|
||||
transform: translateY(-3%);
|
||||
box-shadow: 0 3px 10px rgba(123, 180, 220, 0.6); /* 浅蓝色阴影 */
|
||||
background: #5fa0d0; /* 悬停时略深的蓝色 */
|
||||
}
|
||||
|
||||
.comparison-item {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
.button-get-plan .svg-rocket {
|
||||
margin-right: 10px;
|
||||
width: 0.9rem;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
font-size: 22px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
.back-to-top {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.item-top {
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.item-percent {
|
||||
color: #7fb926;
|
||||
margin-bottom: 5px;
|
||||
border-bottom: 1px solid #666;
|
||||
}
|
||||
|
||||
.item-unit {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
height: 36vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.power-chart {
|
||||
height: 38vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.peak-item {
|
||||
padding: 15px 0 0 0;
|
||||
border: 1px solid #3671e8;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.bottom-text {
|
||||
background-color: #3671e8;
|
||||
color: white;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.peak-item .value {
|
||||
font-size: 22px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.peak-item .time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin: 5px 0;
|
||||
/* 返回顶部按钮 */
|
||||
.back-to-top:hover {
|
||||
background-color: #6bb1e3;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
</style>
|
||||
|
@@ -266,6 +266,9 @@ async function handleSelectFloor(selectedKeys, info) {
|
||||
{ type: 'min', name: 'Min' },
|
||||
],
|
||||
},
|
||||
markLine: {
|
||||
data: [{ type: 'average', name: 'Avg' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '当日',
|
||||
@@ -277,6 +280,9 @@ async function handleSelectFloor(selectedKeys, info) {
|
||||
{ type: 'min', name: 'Min' },
|
||||
],
|
||||
},
|
||||
markLine: {
|
||||
data: [{ type: 'average', name: 'Avg' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -297,6 +303,9 @@ async function handleSelectFloor(selectedKeys, info) {
|
||||
{ type: 'min', name: 'Min' },
|
||||
],
|
||||
},
|
||||
markLine: {
|
||||
data: [{ type: 'average', name: 'Avg' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '当月',
|
||||
@@ -308,6 +317,9 @@ async function handleSelectFloor(selectedKeys, info) {
|
||||
{ type: 'min', name: 'Min' },
|
||||
],
|
||||
},
|
||||
markLine: {
|
||||
data: [{ type: 'average', name: 'Avg' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -328,6 +340,9 @@ async function handleSelectFloor(selectedKeys, info) {
|
||||
{ type: 'min', name: 'Min' },
|
||||
],
|
||||
},
|
||||
markLine: {
|
||||
data: [{ type: 'average', name: 'Avg' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '当年',
|
||||
@@ -339,6 +354,9 @@ async function handleSelectFloor(selectedKeys, info) {
|
||||
{ type: 'min', name: 'Min' },
|
||||
],
|
||||
},
|
||||
markLine: {
|
||||
data: [{ type: 'average', name: 'Avg' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@@ -81,7 +81,7 @@ import { Page } from '@vben/common-ui'
|
||||
import { ref, onMounted, onBeforeUnmount, reactive } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import type { ECharts, EChartsOption } from 'echarts'
|
||||
import FloorTree from "../../electricEnergy/components/floor-tree.vue"
|
||||
import FloorTree from "../components/floor-tree.vue"
|
||||
|
||||
const chainData = reactive({
|
||||
currentMonthEnergy: '9',
|
||||
|
@@ -13,15 +13,23 @@ export const querySchema: FormSchemaGetter = () => [
|
||||
// export const columns: () => VxeGridProps['columns'] = () => [
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
title: '园区名称',
|
||||
field: 'communityText',
|
||||
},
|
||||
{
|
||||
title: '建筑名称',
|
||||
field: 'buildingText',
|
||||
},
|
||||
{
|
||||
title: '楼层名称',
|
||||
field: 'floorName',
|
||||
},
|
||||
{
|
||||
title: '楼层号',
|
||||
field: 'floorNumber',
|
||||
},
|
||||
/* {
|
||||
// {
|
||||
// title: '楼层号',
|
||||
// field: 'floorNumber',
|
||||
// },
|
||||
/* {
|
||||
title: '楼层类型',
|
||||
field: 'floorType',
|
||||
},*/
|
||||
@@ -33,6 +41,18 @@ export const columns: VxeGridProps['columns'] = [
|
||||
title: '层高',
|
||||
field: 'floorHeight',
|
||||
},
|
||||
{
|
||||
title: '建筑面积(㎡)',
|
||||
field: 'area',
|
||||
},
|
||||
{
|
||||
title: '套内面积(㎡)',
|
||||
field: 'insideInArea',
|
||||
},
|
||||
{
|
||||
title: '公摊面积(㎡)',
|
||||
field: 'sharedArea',
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
@@ -56,34 +76,23 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
component: 'TreeSelect',
|
||||
fieldName: 'buildingId',
|
||||
defaultValue: undefined,
|
||||
label: '社区建筑',
|
||||
label: '建筑名称',
|
||||
rules: 'selectRequired',
|
||||
formItemClass: 'col-span-2',
|
||||
},
|
||||
{
|
||||
label: '楼层数名称',
|
||||
label: '楼层名称',
|
||||
fieldName: 'floorName',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '楼层号',
|
||||
fieldName: 'floorNumber',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
/*{
|
||||
label: '楼层类型',
|
||||
fieldName: 'floorType',
|
||||
component: 'Select',
|
||||
componentProps: {},
|
||||
},*/
|
||||
{
|
||||
label: '房间数量',
|
||||
fieldName: 'roomCount',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min:0,
|
||||
precision:0,
|
||||
min: 0,
|
||||
precision: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -91,8 +100,35 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
fieldName: 'floorHeight',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min:0,
|
||||
precision:0,
|
||||
min: 0,
|
||||
precision: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '建筑面积(㎡)',
|
||||
fieldName: 'area',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '套内面积(㎡)',
|
||||
fieldName: 'insideInArea',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '公摊面积(㎡)',
|
||||
fieldName: 'sharedArea',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@@ -22,9 +22,9 @@ const title = computed(() => {
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
formItemClass: 'col-span-1',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 80,
|
||||
labelWidth: 90,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
@@ -44,7 +44,7 @@ const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
// 在这里更改宽度
|
||||
class: 'w-[550px]',
|
||||
class: 'w-[60%]',
|
||||
fullscreenButton: false,
|
||||
onBeforeClose,
|
||||
onClosed: handleClosed,
|
||||
|
@@ -162,7 +162,7 @@ export const columns: VxeGridProps['columns'] = [
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
width: 180,
|
||||
width: 240,
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -139,6 +139,20 @@ function handleMultiDelete() {
|
||||
>
|
||||
{{ $t('pages.common.edit') }}
|
||||
</ghost-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认退订吗?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<ghost-button
|
||||
danger
|
||||
v-access:code="['property:orderCharge:remove']"
|
||||
@click.stop=""
|
||||
>
|
||||
{{ '退订' }}
|
||||
</ghost-button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
|
@@ -46,6 +46,7 @@ export const columns: VxeGridProps['columns'] = [
|
||||
{
|
||||
title: '图片',
|
||||
field: 'imgPath',
|
||||
slots: { default: 'imgPath' },
|
||||
},
|
||||
{
|
||||
title: '规格',
|
||||
@@ -121,9 +122,9 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
fieldName: 'rent',
|
||||
component: 'InputNumber',
|
||||
rules: 'required',
|
||||
componentProps:{
|
||||
componentProps: {
|
||||
precision: 2,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '库存数量',
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import {
|
||||
useVbenVxeGrid,
|
||||
vxeCheckboxChecked,
|
||||
type VxeGridProps
|
||||
type VxeGridProps,
|
||||
} from '#/adapter/vxe-table';
|
||||
import {
|
||||
plantsProductList,
|
||||
@@ -15,6 +15,7 @@ import type { PropertyForm } from '#/api/property/productManagement/model';
|
||||
import PlantsProductModal from './plantsProduct-modal.vue';
|
||||
import PlantsProductDetail from './plantsProduct-detail.vue';
|
||||
import { columns, querySchema } from './data';
|
||||
import { ossInfo } from '#/api/system/oss';
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
@@ -38,18 +39,48 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
return await plantsProductList({
|
||||
const result = await plantsProductList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
|
||||
// 预处理图片路径
|
||||
if (result.rows && result.rows.length > 0) {
|
||||
// 收集所有图片路径
|
||||
const imgPaths = result.rows
|
||||
.filter((row: any) => row.imgPath)
|
||||
.map((row: any) => row.imgPath);
|
||||
|
||||
if (imgPaths.length > 0) {
|
||||
try {
|
||||
// 批量获取图片URL
|
||||
const imgUrls = await ossInfo(imgPaths);
|
||||
|
||||
const urlMap = new Map(
|
||||
imgUrls.map((item) => [item.ossId, item.url]),
|
||||
);
|
||||
|
||||
// 将URL映射回数据行
|
||||
result.rows.forEach((row: any) => {
|
||||
if (row.imgPath && urlMap.has(row.imgPath)) {
|
||||
row._imgUrl = urlMap.get(row.imgPath);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取图片URL失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
id: 'property-property-index'
|
||||
id: 'property-property-index',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
@@ -98,6 +129,9 @@ function handleMultiDelete() {
|
||||
},
|
||||
});
|
||||
}
|
||||
function getImgUrl(row: any) {
|
||||
return row._imgUrl || '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -110,7 +144,8 @@ function handleMultiDelete() {
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['property:plantsProduct:remove']"
|
||||
@click="handleMultiDelete">
|
||||
@click="handleMultiDelete"
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -152,8 +187,16 @@ function handleMultiDelete() {
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
<template #imgPath="{ row }">
|
||||
<img
|
||||
v-if="row.imgPath"
|
||||
:src="getImgUrl(row)"
|
||||
alt=""
|
||||
class="h-[50px] w-[70px]"
|
||||
/>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<PlantsProduct @reload="tableApi.query()" />
|
||||
<PlantsProductDetailModal/>
|
||||
<PlantsProductDetailModal />
|
||||
</Page>
|
||||
</template>
|
||||
|
@@ -112,6 +112,12 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
fieldName: 'nfcCode',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '巡检位置',
|
||||
fieldName: 'inspectionLocation',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
fieldName: 'remark',
|
||||
|
@@ -5,11 +5,6 @@ import { getDictOptions } from '#/utils/dict';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { renderDict } from '#/utils/render';
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'communityName',
|
||||
label: '社区',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
@@ -44,11 +39,11 @@ export const columns: VxeGridProps['columns'] = [
|
||||
{
|
||||
title: '是否重要',
|
||||
field: 'isMatter',
|
||||
slots:{
|
||||
default:({row})=>{
|
||||
return renderDict(row.isMatter,'wy_fjzydj')
|
||||
}
|
||||
}
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.isMatter, 'wy_fjzydj');
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
@@ -96,25 +91,25 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
getPopupContainer,
|
||||
options: getDictOptions(DictEnum.wy_room_type),
|
||||
},
|
||||
rules:'selectRequired'
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
label: '建筑面积',
|
||||
fieldName: 'area',
|
||||
component: 'InputNumber',
|
||||
componentProps:{
|
||||
min:0,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
},
|
||||
rules:'required'
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '使用面积',
|
||||
fieldName: 'insideInArea',
|
||||
component: 'InputNumber',
|
||||
componentProps:{
|
||||
min:0,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
},
|
||||
rules:'required'
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '是否重要',
|
||||
@@ -123,7 +118,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
componentProps: {
|
||||
options: getDictOptions('wy_fjzydj'),
|
||||
},
|
||||
rules:'selectRequired'
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
@@ -133,7 +128,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
getPopupContainer,
|
||||
options: getDictOptions(DictEnum.wy_fjzt),
|
||||
},
|
||||
rules:'selectRequired'
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
label: '房间图片',
|
||||
|
125
apps/web-antd/src/views/property/room/floor-tree.vue
Normal file
125
apps/web-antd/src/views/property/room/floor-tree.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import type { DeptTree } from '#/api/system/user/model';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { SyncOutlined } from '@ant-design/icons-vue';
|
||||
import { Empty, InputSearch, Skeleton, Tree } from 'ant-design-vue';
|
||||
import { communityTree } from '#/api/property/community';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
withDefaults(defineProps<{ showSearch?: boolean }>(), { showSearch: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* 点击刷新按钮的事件
|
||||
*/
|
||||
reload: [];
|
||||
/**
|
||||
* 点击节点的事件
|
||||
*/
|
||||
select: [];
|
||||
}>();
|
||||
|
||||
const selectFloorId = defineModel('selectFloorId', {
|
||||
default: '',
|
||||
type: Array as PropType<string[]>,
|
||||
});
|
||||
|
||||
const searchValue = defineModel('searchValue', {
|
||||
type: String,
|
||||
default: '',
|
||||
});
|
||||
|
||||
/** 部门数据源 */
|
||||
type DeptTreeArray = DeptTree[];
|
||||
const deptTreeArray = ref<DeptTreeArray>([]);
|
||||
/** 骨架屏加载 */
|
||||
const showTreeSkeleton = ref<boolean>(true);
|
||||
|
||||
async function loadTree() {
|
||||
showTreeSkeleton.value = true;
|
||||
searchValue.value = '';
|
||||
selectFloorId.value = [];
|
||||
const ret = await communityTree(3);
|
||||
deptTreeArray.value = ret;
|
||||
showTreeSkeleton.value = false;
|
||||
}
|
||||
|
||||
async function handleReload() {
|
||||
await loadTree();
|
||||
emit('reload');
|
||||
}
|
||||
function selectNode(selectedKeys, e) {
|
||||
emit('select',e.node.level);
|
||||
}
|
||||
|
||||
onMounted(loadTree);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$attrs.class">
|
||||
<Skeleton
|
||||
:loading="showTreeSkeleton"
|
||||
:paragraph="{ rows: 8 }"
|
||||
active
|
||||
class="p-[8px]"
|
||||
>
|
||||
<div
|
||||
class="bg-background flex h-full flex-col overflow-y-auto rounded-lg"
|
||||
>
|
||||
<!-- 固定在顶部 必须加上bg-background背景色 否则会产生'穿透'效果 -->
|
||||
<div
|
||||
v-if="showSearch"
|
||||
class="bg-background z-100 sticky left-0 top-0 p-[8px]"
|
||||
>
|
||||
<InputSearch
|
||||
v-model:value="searchValue"
|
||||
:placeholder="$t('pages.common.search')"
|
||||
size="small"
|
||||
>
|
||||
<template #enterButton>
|
||||
<a-button @click="handleReload">
|
||||
<SyncOutlined class="text-primary" />
|
||||
</a-button>
|
||||
</template>
|
||||
</InputSearch>
|
||||
</div>
|
||||
<div class="h-full overflow-x-hidden px-[8px]">
|
||||
<Tree
|
||||
v-bind="$attrs"
|
||||
v-if="deptTreeArray.length > 0"
|
||||
v-model:selected-keys="selectFloorId"
|
||||
:class="$attrs.class"
|
||||
:field-names="{ title: 'label', key: 'id' }"
|
||||
:show-line="{ showLeafIcon: false }"
|
||||
:tree-data="deptTreeArray"
|
||||
:virtual="false"
|
||||
default-expand-all
|
||||
@select="selectNode"
|
||||
>
|
||||
<template #title="{ label }">
|
||||
<span v-if="label.indexOf(searchValue) > -1">
|
||||
{{ label.substring(0, label.indexOf(searchValue)) }}
|
||||
<span style="color: #f50">{{ searchValue }}</span>
|
||||
{{
|
||||
label.substring(
|
||||
label.indexOf(searchValue) + searchValue.length,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>{{ label }}</span>
|
||||
</template>
|
||||
</Tree>
|
||||
<!-- 仅本人数据权限 可以考虑直接不显示 -->
|
||||
<div v-else class="mt-5">
|
||||
<Empty
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="无部门数据"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</template>
|
@@ -1,18 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { Recordable } from '@vben/types';
|
||||
import {Page, useVbenModal, type VbenFormProps} from '@vben/common-ui';
|
||||
import {getVxePopupContainer} from '@vben/utils';
|
||||
import {Modal, Popconfirm, Space} from 'ant-design-vue';
|
||||
import FloorTree from "./floor-tree.vue"
|
||||
import detailModal from "./room-detail.vue";
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
import {
|
||||
useVbenVxeGrid,
|
||||
vxeCheckboxChecked,
|
||||
type VxeGridProps
|
||||
type VxeGridProps
|
||||
} from '#/adapter/vxe-table';
|
||||
|
||||
import {
|
||||
@@ -20,11 +16,12 @@ import {
|
||||
roomList,
|
||||
roomRemove,
|
||||
} from '#/api/property/room';
|
||||
import type { RoomForm } from '#/api/property/room/model';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
import type {RoomForm} from '#/api/property/room/model';
|
||||
import {commonDownloadExcel} from '#/utils/file/download';
|
||||
|
||||
import roomModal from './room-modal.vue';
|
||||
import { columns, querySchema } from './data';
|
||||
import {columns, querySchema} from './data';
|
||||
import {ref} from "vue";
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
@@ -63,7 +60,19 @@ const gridOptions: VxeGridProps = {
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
query: async ({page}, formValues = {}) => {
|
||||
Reflect.deleteProperty(formValues, 'communityId');
|
||||
Reflect.deleteProperty(formValues, 'buildingId');
|
||||
Reflect.deleteProperty(formValues, 'floorId');
|
||||
if (selectFloorId.value.length === 1) {
|
||||
if (level.value == 1) {
|
||||
formValues.communityId = selectFloorId.value[0];
|
||||
} else if (level.value == 2) {
|
||||
formValues.buildingId = selectFloorId.value[0];
|
||||
} else {
|
||||
formValues.floorId = selectFloorId.value[0];
|
||||
}
|
||||
}
|
||||
return await roomList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
@@ -87,6 +96,9 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
const [RoomModal, modalApi] = useVbenModal({
|
||||
connectedComponent: roomModal,
|
||||
});
|
||||
const [RoomDetail, detailApi] = useVbenModal({
|
||||
connectedComponent: detailModal,
|
||||
});
|
||||
|
||||
function handleAdd() {
|
||||
modalApi.setData({});
|
||||
@@ -94,10 +106,15 @@ function handleAdd() {
|
||||
}
|
||||
|
||||
async function handleEdit(row: Required<RoomForm>) {
|
||||
modalApi.setData({ id: row.id });
|
||||
modalApi.setData({id: row.id});
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
async function handleInfo(row: Required<RoomForm>) {
|
||||
detailApi.setData({id: row.id});
|
||||
detailApi.open();
|
||||
}
|
||||
|
||||
async function handleDelete(row: Required<RoomForm>) {
|
||||
await roomRemove(row.id);
|
||||
await tableApi.query();
|
||||
@@ -122,61 +139,82 @@ function handleDownloadExcel() {
|
||||
fieldMappingTime: formOptions.fieldMappingTime,
|
||||
});
|
||||
}
|
||||
|
||||
const selectFloorId = ref<string[]>([]);
|
||||
const level = ref<number>();
|
||||
|
||||
function handleSelectFloor(nodeLevel: number) {
|
||||
level.value = nodeLevel;
|
||||
tableApi.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<BasicTable table-title="房间信息列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['property:room:export']"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['property:room:remove']"
|
||||
@click="handleMultiDelete">
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
v-access:code="['property:room:add']"
|
||||
@click="handleAdd"
|
||||
>
|
||||
{{ $t('pages.common.add') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Space>
|
||||
<ghost-button
|
||||
v-access:code="['property:room:edit']"
|
||||
@click.stop="handleEdit(row)"
|
||||
>
|
||||
{{ $t('pages.common.edit') }}
|
||||
</ghost-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<ghost-button
|
||||
danger
|
||||
v-access:code="['property:room:remove']"
|
||||
@click.stop=""
|
||||
<Page :auto-content-height="true" style="width: 100%">
|
||||
<div class="flex h-full gap-[15px]">
|
||||
<FloorTree v-model:select-floor-id="selectFloorId"
|
||||
class="w-[260px]"
|
||||
@reload="() => tableApi.reload()"
|
||||
@select="handleSelectFloor"></FloorTree>
|
||||
<BasicTable class="flex-1 overflow-hidden" table-title="房间信息列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['property:room:export']"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['property:room:remove']"
|
||||
@click="handleMultiDelete">
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
v-access:code="['property:room:add']"
|
||||
@click="handleAdd"
|
||||
>
|
||||
{{ $t('pages.common.add') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Space>
|
||||
<ghost-button
|
||||
v-access:code="['property:room:query']"
|
||||
@click.stop="handleInfo(row)"
|
||||
>
|
||||
{{ $t('pages.common.info') }}
|
||||
</ghost-button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<RoomModal @reload="tableApi.query()" />
|
||||
<ghost-button
|
||||
v-access:code="['property:room:edit']"
|
||||
@click.stop="handleEdit(row)"
|
||||
>
|
||||
{{ $t('pages.common.edit') }}
|
||||
</ghost-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<ghost-button
|
||||
danger
|
||||
v-access:code="['property:room:remove']"
|
||||
@click.stop=""
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</ghost-button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
<RoomModal @reload="tableApi.query()"/>
|
||||
<RoomDetail/>
|
||||
</Page>
|
||||
</template>
|
||||
|
79
apps/web-antd/src/views/property/room/room-detail.vue
Normal file
79
apps/web-antd/src/views/property/room/room-detail.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import {shallowRef} from 'vue';
|
||||
import {useVbenModal} from '@vben/common-ui';
|
||||
import {Descriptions, DescriptionsItem} from 'ant-design-vue';
|
||||
import {ossInfo} from "#/api/system/oss";
|
||||
import {roomInfo} from "#/api/property/room";
|
||||
import type {RoomVO} from "#/api/property/room/model";
|
||||
import {renderDict} from "#/utils/render";
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
onOpenChange: handleOpenChange,
|
||||
onClosed() {
|
||||
roomDetail.value = null;
|
||||
},
|
||||
});
|
||||
|
||||
const roomDetail = shallowRef<null | RoomVO>(null);
|
||||
|
||||
async function handleOpenChange(open: boolean) {
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
const {id} = modalApi.getData() as { id: number | string };
|
||||
roomDetail.value = await roomInfo(id);
|
||||
try {
|
||||
if (roomDetail.value.imgUrl) {
|
||||
const res = await ossInfo([roomDetail.value.imgUrl]);
|
||||
roomDetail.value.imgPath = res?.[0]?.url
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal :footer="false" :fullscreen-button="false" title="房间详情" class="w-[70%]">
|
||||
<Descriptions v-if="roomDetail" size="small" :column="2" bordered
|
||||
:labelStyle="{width:'120px'}">
|
||||
<DescriptionsItem label="社区">
|
||||
{{ roomDetail.communityText }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="建筑名称">
|
||||
{{ roomDetail.buildingText }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="楼栋号">
|
||||
{{ roomDetail.floorText }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="房间号">
|
||||
{{ roomDetail.roomNumber }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="房间类型">
|
||||
{{ roomDetail.roomTypeName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="建筑面积">
|
||||
{{ roomDetail.area }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="使用面积">
|
||||
{{ roomDetail.insideInArea }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="是否重要">
|
||||
<component v-if="roomDetail.isMatter != null"
|
||||
:is="renderDict(roomDetail.isMatter,'wy_fjzydj')"
|
||||
/>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="状态">
|
||||
{{ roomDetail.statusName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="所属单位">
|
||||
{{ roomDetail.residentUnitText ? roomDetail.residentUnitText : '-'}}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="房间图片" :span="2">
|
||||
<img v-if="roomDetail.imgPath" :src="roomDetail.imgPath" alt="图片加载失败" class="w-[100px] h-[100px]"/>
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</BasicModal>
|
||||
</template>
|
@@ -54,10 +54,15 @@ async function handleOpenChange(open: boolean) {
|
||||
{{ conferenceSettingsDetail.name }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="会议室类型">
|
||||
<component
|
||||
<component v-if="conferenceSettingsDetail.meetingRoomType!=null"
|
||||
:is="renderDict(conferenceSettingsDetail.meetingRoomType,'meeting_room_type')"
|
||||
/>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="保密等级">
|
||||
<component v-if="conferenceSettingsDetail.secrecyGrade!=null"
|
||||
:is="renderDict(conferenceSettingsDetail.secrecyGrade,'wy_hysbmdj')"
|
||||
/>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="可容纳人数">
|
||||
{{ conferenceSettingsDetail.personNumber }}
|
||||
</DescriptionsItem>
|
||||
|
@@ -42,7 +42,16 @@ export const columns: VxeGridProps['columns'] = [
|
||||
field: 'meetingRoomType',
|
||||
slots: {
|
||||
default: ({row}) => {
|
||||
return row.meetingRoomType!=null?renderDict(row.meetingRoomType, 'meeting_room_type'):'';
|
||||
return row.meetingRoomType!=null?renderDict(row.meetingRoomType, 'meeting_room_type'):'-';
|
||||
},
|
||||
},
|
||||
minWidth:100,
|
||||
},
|
||||
{
|
||||
title: '保密等级',
|
||||
slots: {
|
||||
default: ({row}) => {
|
||||
return row.secrecyGrade!=null?renderDict(row.secrecyGrade, 'wy_hysbmdj'):'-';
|
||||
},
|
||||
},
|
||||
minWidth:100,
|
||||
@@ -124,6 +133,16 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
label: '保密等级',
|
||||
fieldName: 'secrecyGrade',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions('wy_hysbmdj'),
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
formItemClass: 'col-span-1',
|
||||
},
|
||||
{
|
||||
label: '可容纳人数',
|
||||
fieldName: 'personNumber',
|
||||
|
@@ -0,0 +1,88 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import { personList } from '#/api/property/resident/person';
|
||||
import {participantsList} from '#/api/property/roomBooking/participants';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
label: '会议室名称',
|
||||
fieldName: 'meetBookId',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const rows = await participantsList({ pageSize: 1000000000, pageNum: 1 });
|
||||
return rows;
|
||||
},
|
||||
resultField: 'rows',
|
||||
labelField: 'meetBookingVo.name',
|
||||
valueField: 'meetBookId',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '发起人',
|
||||
fieldName: 'residentPersonId',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const rows = await personList({ pageSize: 1000000000, pageNum: 1 });
|
||||
return rows;
|
||||
},
|
||||
resultField: 'rows',
|
||||
labelField: 'userName',
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{
|
||||
title: '会议室名称',
|
||||
field: 'meetBookingVo.name',
|
||||
},
|
||||
{
|
||||
title: '会议主题',
|
||||
field: 'meetBookingVo.meetTheme',
|
||||
},
|
||||
{
|
||||
title: '会议时间',
|
||||
field: 'meetBookingVo.meetingTime',
|
||||
slots: {
|
||||
default: ({row}) => {
|
||||
return row.meetBookingVo.scheduledStarttime+'~'+row.meetBookingVo.scheduledEndtime;
|
||||
},
|
||||
},
|
||||
width:300
|
||||
},
|
||||
{
|
||||
title: '发起人',
|
||||
field: 'meetBookingVo.personName',
|
||||
},
|
||||
{
|
||||
title: '发起单位',
|
||||
field: 'meetBookingVo.unitName',
|
||||
},
|
||||
{
|
||||
title: '会议室地点',
|
||||
field: 'meetBookingVo.meetLocation',
|
||||
},
|
||||
{
|
||||
title: '签到状态',
|
||||
field: 'signState',
|
||||
slots: {
|
||||
default: ({row}) => {
|
||||
return row.signState === '0' ? '未签到' : '已签到';
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '签到时间',
|
||||
field: 'signTime',
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
width: 180,
|
||||
},
|
||||
];
|
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
|
||||
import { Space } from 'ant-design-vue';
|
||||
import {
|
||||
useVbenVxeGrid,
|
||||
type VxeGridProps
|
||||
} from '#/adapter/vxe-table';
|
||||
import {
|
||||
participantsExport,
|
||||
participantsList,
|
||||
} from '#/api/property/roomBooking/participants';
|
||||
import type { ParticipantsForm } from '#/api/property/roomBooking/participants/model';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
import participantsDetail from './participants-detail.vue';
|
||||
import { columns, querySchema } from './data';
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
reserve: true,
|
||||
},
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
return await participantsList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
// 表格全局唯一表示 保存列配置需要用到
|
||||
id: 'property-participants-index'
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const [ParticipantsDetail, participantsDetailApi] = useVbenModal({
|
||||
connectedComponent: participantsDetail,
|
||||
});
|
||||
|
||||
async function handleInfo(row: Required<ParticipantsForm>) {
|
||||
participantsDetailApi.setData({ id: row.id });
|
||||
participantsDetailApi.open();
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(participantsExport, '会议室参会记录数据', tableApi.formApi.form.values, {
|
||||
fieldMappingTime: formOptions.fieldMappingTime,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<BasicTable table-title="会议室参会记录列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['property:participants:export']"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Space>
|
||||
<ghost-button
|
||||
@click.stop="handleInfo(row)"
|
||||
>
|
||||
{{ $t('pages.common.info') }}
|
||||
</ghost-button>
|
||||
</Space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<ParticipantsDetail/>
|
||||
</Page>
|
||||
</template>
|
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import type {ParticipantsVO} from '#/api/property/roomBooking/participants/model';
|
||||
import {shallowRef} from 'vue';
|
||||
import {useVbenModal} from '@vben/common-ui';
|
||||
import {Descriptions, DescriptionsItem} from 'ant-design-vue';
|
||||
import {participantsInfo} from '#/api/property/roomBooking/participants';
|
||||
import {ossInfo} from "#/api/system/oss";
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
onOpenChange: handleOpenChange,
|
||||
onClosed() {
|
||||
ParticipantsDetail.value = null;
|
||||
},
|
||||
});
|
||||
|
||||
const ParticipantsDetail = shallowRef<null | ParticipantsVO>(null);
|
||||
|
||||
async function handleOpenChange(open: boolean) {
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
const {id} = modalApi.getData() as { id: number | string };
|
||||
ParticipantsDetail.value = await participantsInfo(id);
|
||||
try {
|
||||
if (ParticipantsDetail.value.residentPersonVo.img) {
|
||||
const res = await ossInfo([ParticipantsDetail.value.residentPersonVo.img]);
|
||||
ParticipantsDetail.value.residentPersonVo.img = res?.[0]?.url
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal :footer="false" :fullscreen-button="false" title="会议室详情" class="w-[70%]">
|
||||
<Descriptions v-if="ParticipantsDetail" size="small" :column="2" bordered
|
||||
:labelStyle="{width:'120px'}">
|
||||
<DescriptionsItem label="会议室名称">
|
||||
{{ ParticipantsDetail.meetBookingVo.name }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="会议主题">
|
||||
{{ ParticipantsDetail.meetBookingVo.meetTheme }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="会议时间">
|
||||
{{ ParticipantsDetail.meetBookingVo.scheduledStarttime }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="发起人">
|
||||
{{ ParticipantsDetail.meetBookingVo.personName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="发起单位">
|
||||
{{ ParticipantsDetail.meetBookingVo.unitName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="会议室地点">
|
||||
{{ ParticipantsDetail.meetBookingVo.meetLocation }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="参会姓名">
|
||||
{{ ParticipantsDetail.residentPersonVo.userName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="证件号">
|
||||
{{ ParticipantsDetail.residentPersonVo.idCard }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="所属公司">
|
||||
{{ ParticipantsDetail.residentPersonVo.unitName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="联系电话">
|
||||
{{ ParticipantsDetail.residentPersonVo.phone }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="人脸图片" v-if="ParticipantsDetail.residentPersonVo.img" :span="2">
|
||||
<img :src="ParticipantsDetail.residentPersonVo.img" alt="图片加载失败" class="w-[100px] h-[100px]"/>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="签到状态">
|
||||
{{ ParticipantsDetail.signState === '0' ? '未签到' : '已签到'}}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="签到时间">
|
||||
{{ ParticipantsDetail.signTime }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</BasicModal>
|
||||
</template>
|
@@ -2,7 +2,7 @@ import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import {getDictOptions} from "#/utils/dict";
|
||||
import {renderDict} from "#/utils/render";
|
||||
import {personList} from '#/api/property/resident/person'
|
||||
import {resident_unitList,} from '#/api/property/resident/unit';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
@@ -138,7 +138,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '所属公司',
|
||||
label: '所属单位',
|
||||
fieldName: 'visitorUnit',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
@@ -160,10 +160,10 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
fieldName: 'interviewedUnit',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: personList,
|
||||
api: resident_unitList,
|
||||
resultField: 'rows',
|
||||
labelField: 'unitName',
|
||||
valueField: 'unitName',
|
||||
labelField: 'name',
|
||||
valueField: 'name',
|
||||
placeholder: '请选择邀约单位',
|
||||
},
|
||||
rules: 'required',
|
||||
|
@@ -7,17 +7,20 @@ import type { TreeNode } from '#/api/common';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
withDefaults(defineProps<{ showSearch?: boolean }>(), { showSearch: true });
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
showSearch?: boolean;
|
||||
currentSelectKey?: string;
|
||||
selectKeys?: string[];
|
||||
}>(),
|
||||
{
|
||||
showSearch: true,
|
||||
currentSelectKey: '',
|
||||
selectKeys: [],
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
checked: [];
|
||||
/**
|
||||
* 点击节点的事件
|
||||
*/
|
||||
reload: [];
|
||||
select: [];
|
||||
}>();
|
||||
const emit = defineEmits(['selected', 'reload', 'checked']);
|
||||
|
||||
const searchValue = defineModel('searchValue', {
|
||||
type: String,
|
||||
@@ -37,6 +40,10 @@ async function loadChannelTree() {
|
||||
showTreeSkeleton.value = false;
|
||||
}
|
||||
|
||||
function onSelect(key: any, selectNode: any) {
|
||||
emit('selected', key, selectNode);
|
||||
}
|
||||
|
||||
async function handleReload() {
|
||||
await loadChannelTree();
|
||||
emit('reload');
|
||||
@@ -46,21 +53,11 @@ onMounted(loadChannelTree);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[800px]" :class="$attrs.class">
|
||||
<Skeleton
|
||||
:loading="showTreeSkeleton"
|
||||
:paragraph="{ rows: 8 }"
|
||||
active
|
||||
class="p-[8px]"
|
||||
>
|
||||
<div
|
||||
class="flex h-full flex-col overflow-y-auto rounded-lg"
|
||||
>
|
||||
<div :class="$attrs.class">
|
||||
<Skeleton :loading="showTreeSkeleton" :paragraph="{ rows: 8 }" active>
|
||||
<div class="h-full">
|
||||
<!-- 固定在顶部 必须加上bg-background背景色 否则会产生'穿透'效果 -->
|
||||
<divx
|
||||
v-if="showSearch"
|
||||
class="z-100 sticky left-0 top-0 p-[8px]"
|
||||
>
|
||||
<div v-if="showSearch" class="z-100 sticky left-0 top-0 p-[8px]">
|
||||
<InputSearch
|
||||
v-model:value="searchValue"
|
||||
:placeholder="$t('pages.common.search')"
|
||||
@@ -72,31 +69,45 @@ onMounted(loadChannelTree);
|
||||
</a-button>
|
||||
</template>
|
||||
</InputSearch>
|
||||
</divx>
|
||||
<div class="h-full overflow-x-hidden px-[8px]">
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="h-[calc(100%-40px)] overflow-y-auto overflow-x-hidden px-[8px]"
|
||||
>
|
||||
<Tree
|
||||
v-bind="$attrs"
|
||||
v-if="channelTree.length > 0"
|
||||
:class="$attrs.class"
|
||||
:show-line="{ showLeafIcon: false }"
|
||||
:tree-data="channelTree"
|
||||
:virtual="false"
|
||||
default-expand-all
|
||||
checkable
|
||||
@select="$emit('select')"
|
||||
@check="$emit('checked')"
|
||||
@select="onSelect"
|
||||
>
|
||||
<template #title="{ label }">
|
||||
<span v-if="label.indexOf(searchValue) > -1">
|
||||
{{ label.substring(0, label.indexOf(searchValue)) }}
|
||||
<span style="color: #f50">{{ searchValue }}</span>
|
||||
{{
|
||||
label.substring(
|
||||
label.indexOf(searchValue) + searchValue.length,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>{{ label }}</span>
|
||||
<template #title="{ label, level, key }">
|
||||
<div class="flex">
|
||||
<div v-if="level == 2" class="tree-icon">
|
||||
<div
|
||||
v-if="selectKeys.indexOf(key) > -1"
|
||||
class="icon playing"
|
||||
></div>
|
||||
<div v-else class="icon unplay"></div>
|
||||
</div>
|
||||
<span :style="currentSelectKey == key?'color:blue':''">
|
||||
<span v-if="label.indexOf(searchValue) > -1">
|
||||
{{ label.substring(0, label.indexOf(searchValue)) }}
|
||||
<span style="color: #f50">{{ searchValue }}</span>
|
||||
{{
|
||||
label.substring(
|
||||
label.indexOf(searchValue) + searchValue.length,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
<span>{{ label }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</Tree>
|
||||
</div>
|
||||
@@ -104,3 +115,25 @@ onMounted(loadChannelTree);
|
||||
</Skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tree-icon {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
.unplay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('/src/assets/tree/unplayer.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.playing {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('/src/assets/tree/playering.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,9 +1,15 @@
|
||||
<template>
|
||||
<Page class="h-full w-full">
|
||||
<Page style="height: calc(100vh - 88px)" class="video-page h-full w-full">
|
||||
<!-- 设备分组区域 -->
|
||||
<div class="flex h-full gap-[8px]">
|
||||
<div class="c-tree bg-background h-full pb-[5px]">
|
||||
<ChannelTree class="w-[300px]" @check="onNodeChecked" />
|
||||
<div class="c-tree bg-background h-full overflow-hidden pb-[5px]">
|
||||
<ChannelTree
|
||||
class="h-full w-[300px]"
|
||||
:currentSelectKey="currentSelectKey"
|
||||
:selectKeys="selectKeys"
|
||||
@check="onNodeChecked"
|
||||
@select="onTreeSelect"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 设备分组区域 -->
|
||||
@@ -13,29 +19,43 @@
|
||||
v-for="i in playerNum"
|
||||
:style="playerStyle"
|
||||
class="player"
|
||||
:class="`layer-${i} ${currentSelectPlayerIndex == i ? selected : ''}`"
|
||||
@click="playerSelect(i)"
|
||||
:class="`layer-${i - 1} ${currentSelectPlayerIndex == i - 1 ? selected : ''}`"
|
||||
@click="playerSelect(i - 1)"
|
||||
>
|
||||
<video
|
||||
style="width: 100%; height: 100%"
|
||||
:ref="setItemRef"
|
||||
muted
|
||||
autoplay
|
||||
></video>
|
||||
<Loading
|
||||
:spinning="playerLoading[i - 1]"
|
||||
text="加载中..."
|
||||
class="flex h-full w-full items-center justify-center"
|
||||
>
|
||||
<video
|
||||
style="width: 100%; height: 100%"
|
||||
:ref="(el) => setItemRef(i - 1, el)"
|
||||
muted
|
||||
autoplay
|
||||
></video>
|
||||
</Loading>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player-area flex h-[30px] gap-[5px]">
|
||||
<div @click="onPlayerNumChanged(1)" class="h-[20px] w-[20px]">
|
||||
<Svg1FrameIcon style="width: 100%; height: 100%" />
|
||||
</div>
|
||||
<div @click="onPlayerNumChanged(2)" class="h-[20px] w-[20px]">
|
||||
<Svg4FrameIcon style="width: 100%; height: 100%" />
|
||||
</div>
|
||||
<div @click="onPlayerNumChanged(3)" class="h-[20px] w-[20px]">
|
||||
<Svg9FrameIcon style="width: 100%; height: 100%" />
|
||||
</div>
|
||||
<div @click="onPlayerNumChanged(4)" class="h-[20px] w-[20px]">
|
||||
<Svg16FrameIcon style="width: 100%; height: 100%" />
|
||||
<div
|
||||
v-for="key in 4"
|
||||
@click="onPlayerNumChanged(key)"
|
||||
:class="playerSelectItemIndex == key ? selected : ''"
|
||||
class="btn-item h-[20px] w-[20px]"
|
||||
>
|
||||
<Svg1FrameIcon v-if="key == 1" style="width: 100%; height: 100%" />
|
||||
<Svg4FrameIcon
|
||||
v-else-if="key == 2"
|
||||
style="width: 100%; height: 100%"
|
||||
/>
|
||||
<Svg9FrameIcon
|
||||
v-else-if="key == 3"
|
||||
style="width: 100%; height: 100%"
|
||||
/>
|
||||
<Svg16FrameIcon
|
||||
v-else-if="key == 4"
|
||||
style="width: 100%; height: 100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,7 +65,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, toRaw } from 'vue';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Loading, Page } from '@vben/common-ui';
|
||||
import ChannelTree from './channel-tree.vue';
|
||||
import mpegts from 'mpegts.js';
|
||||
import { message } from 'ant-design-vue';
|
||||
@@ -61,32 +81,46 @@ import type { AddStreamProxyResult } from '#/api/sis/stream/model';
|
||||
|
||||
const selected = 'selected';
|
||||
|
||||
const itemRefs = ref<HTMLVideoElement[]>([]);
|
||||
const setItemRef = (el: any) => {
|
||||
if (el) {
|
||||
itemRefs.value.push(el);
|
||||
}
|
||||
const itemRefs: any[] = [];
|
||||
const setItemRef = (index: number, el: any) => {
|
||||
itemRefs[index] = el;
|
||||
};
|
||||
|
||||
/**
|
||||
* 屏幕播放器数量
|
||||
*/
|
||||
const playerNum = ref(1);
|
||||
/**
|
||||
* 屏幕播放器样式
|
||||
*/
|
||||
//屏幕播放器样式
|
||||
const playerStyle = ref({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
});
|
||||
const currentSelectPlayerIndex = ref(-1);
|
||||
// 播放器配置
|
||||
const playerConfig = {
|
||||
enableErrorRecover: true, // 启用错误恢复
|
||||
autoCleanupMaxBackwardDuration: 30,
|
||||
autoCleanupMinBackwardDuration: 10,
|
||||
};
|
||||
//屏幕播放器数量
|
||||
const playerNum = ref(1);
|
||||
// 播放器数量控制按钮组索引
|
||||
const playerSelectItemIndex = ref(1);
|
||||
// 当前选择的播放器索引,默认选中第一个
|
||||
const currentSelectPlayerIndex = ref(0);
|
||||
// 播放器数据, 每一个位置代表页面上行的一个矩形
|
||||
const playerList: any[] = [];
|
||||
// 播放器loading
|
||||
const playerLoading = ref<boolean[]>([]);
|
||||
|
||||
// 当前选择播放器的key
|
||||
const currentSelectKey = ref('');
|
||||
// 当前所有的播放设备key
|
||||
const selectKeys = ref<string[]>([]);
|
||||
|
||||
function playerSelect(index: number) {
|
||||
if (index === currentSelectPlayerIndex.value) {
|
||||
currentSelectPlayerIndex.value = -1;
|
||||
return;
|
||||
}
|
||||
currentSelectPlayerIndex.value = index;
|
||||
const player = playerList[index];
|
||||
if (player) {
|
||||
currentSelectKey.value = player.key;
|
||||
} else {
|
||||
currentSelectKey.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,8 +129,8 @@ function playerSelect(index: number) {
|
||||
*/
|
||||
function onPlayerNumChanged(val: number) {
|
||||
// 1个屏幕
|
||||
const changeBeforeNum = playerNum.value;
|
||||
let changeNum = 1;
|
||||
playerSelectItemIndex.value = val;
|
||||
if (val === 1) {
|
||||
playerStyle.value = {
|
||||
width: '100%',
|
||||
@@ -128,26 +162,18 @@ function onPlayerNumChanged(val: number) {
|
||||
changeNum = 16;
|
||||
}
|
||||
playerNum.value = changeNum;
|
||||
// 缩小布局
|
||||
if (changeBeforeNum > changeNum) {
|
||||
const playerArr = [];
|
||||
for (let i = 0; i < playerList.length; i++) {
|
||||
const playerBox = playerList[i];
|
||||
if (playerBox) {
|
||||
playerArr.push(playerBox);
|
||||
}
|
||||
playerList[i] = null;
|
||||
}
|
||||
for (let i = 0; i < playerArr.length; i++) {
|
||||
const play = playerArr[i];
|
||||
if (i < changeNum) {
|
||||
// 获取播放元素
|
||||
changeElPlayer(play, i);
|
||||
} else {
|
||||
closePlayVieo(play.player);
|
||||
// 缩小布局,超过当前播放器的都关闭
|
||||
for (let i = 0; i < playerList.length; i++) {
|
||||
const playInfo = playerList[i];
|
||||
if (i >= changeNum) {
|
||||
if (playInfo) {
|
||||
closePlayVieo(playInfo.player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理树节点状态
|
||||
treeSelectHandle();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,8 +194,43 @@ function handleParentNoe(node: any, newNode: any[] = []) {
|
||||
}
|
||||
}
|
||||
|
||||
// 播放器数据, 每一个位置代表页面上行的一个矩形
|
||||
const playerList: any[] = [];
|
||||
function onTreeSelect(_key: any, selectNode: any) {
|
||||
const {
|
||||
selected,
|
||||
node: { level, data },
|
||||
} = selectNode;
|
||||
// 代表点击的是摄像头
|
||||
if (level === 2) {
|
||||
// 播放
|
||||
if (selected) {
|
||||
doPlayer(data, currentSelectPlayerIndex.value);
|
||||
}
|
||||
// 取消播放
|
||||
else {
|
||||
for (let i = 0; i < playerNum.value; i++) {
|
||||
const player = playerList[i];
|
||||
if (player && player.data.id === data.id) {
|
||||
closePlayer(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error('请选择摄像头');
|
||||
}
|
||||
}
|
||||
|
||||
function treeSelectHandle() {
|
||||
// 此处player可能已经释放所以不可能在取到只
|
||||
const player = playerList[currentSelectPlayerIndex.value];
|
||||
currentSelectKey.value = player ? player.key : '';
|
||||
const arr: string[] = [];
|
||||
playerList.forEach((item: any) => {
|
||||
if (item && item.key) {
|
||||
arr.push(item.key);
|
||||
}
|
||||
});
|
||||
selectKeys.value = arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点选中时间处理
|
||||
@@ -194,7 +255,7 @@ function onNodeChecked(
|
||||
* 如果当前页面有选择播放未知,并且播放视频只有一个,则播放到制定位置
|
||||
*/
|
||||
if (currentSelectPlayerIndex.value !== -1 && checkNode.length == 1) {
|
||||
doPlayer(checkNode[0], currentSelectPlayerIndex.value - 1);
|
||||
doPlayer(checkNode[0], currentSelectPlayerIndex.value);
|
||||
}
|
||||
// 批量播放 currentSelectPlayerIndex 将不再生效
|
||||
else {
|
||||
@@ -233,40 +294,33 @@ function onNodeChecked(
|
||||
}
|
||||
}
|
||||
|
||||
function changeElPlayer(playerInfo: any, index: number) {
|
||||
const playerData = playerInfo.data;
|
||||
const oldPlayer = playerInfo.player;
|
||||
if (oldPlayer) {
|
||||
closePlayVieo(oldPlayer);
|
||||
}
|
||||
const videoConfig = {
|
||||
type: 'flv',
|
||||
url: playerData.url,
|
||||
isLive: true,
|
||||
hasAudio: false,
|
||||
hasVideo: true,
|
||||
enableWorker: true, // 启用分离的线程进行转码
|
||||
enableStashBuffer: false, // 关闭IO隐藏缓冲区
|
||||
stashInitialSize: 256, // 减少首帧显示等待时长
|
||||
};
|
||||
const playerConfig = {
|
||||
enableErrorRecover: true, // 启用错误恢复
|
||||
autoCleanupMaxBackwardDuration: 30,
|
||||
autoCleanupMinBackwardDuration: 10,
|
||||
};
|
||||
const player = mpegts.createPlayer(videoConfig, playerConfig);
|
||||
const videoElement = itemRefs.value[index];
|
||||
if (videoElement) {
|
||||
player.attachMediaElement(videoElement);
|
||||
player.load();
|
||||
player.play();
|
||||
playerList[index] = {
|
||||
player,
|
||||
data: playerData,
|
||||
};
|
||||
} else {
|
||||
console.log('视频播放元素获取异常');
|
||||
}
|
||||
/**
|
||||
* 添加媒体播放器事件监听
|
||||
* @param index 媒体索引
|
||||
* @param player 播放器对象
|
||||
*/
|
||||
function addPlayerListener(index: number, player: any) {
|
||||
// 播放结束。
|
||||
player.on(mpegts.Events.LOADING_COMPLETE, (...args: any[]) => {
|
||||
console.log('LOADING_COMPLETE', args);
|
||||
});
|
||||
// 播放媒体数据
|
||||
player.on(mpegts.Events.MEDIA_INFO, (...args: any[]) => {
|
||||
console.log('MEDIA_INFO', args);
|
||||
loading(index, 1);
|
||||
});
|
||||
// 播放媒体元数据到到达
|
||||
player.on(mpegts.Events.METADATA_ARRIVED, (...args: any[]) => {
|
||||
console.log('METADATA_ARRIVED', args);
|
||||
});
|
||||
// 统计播放数据
|
||||
// player.on(mpegts.Events.STATISTICS_INFO, (...args: any[]) => {
|
||||
// console.log('STATISTICS_INFO', args);
|
||||
// });
|
||||
// 播放异常
|
||||
player.on(mpegts.Events.ERROR, (...args: any[]) => {
|
||||
console.log('ERROR', args);
|
||||
});
|
||||
}
|
||||
|
||||
function streamProxy(nodeData: any, cb: Function) {
|
||||
@@ -309,6 +363,7 @@ function doPlayer(nodeData: any, index: number = 0) {
|
||||
streamProxy(nodeData, (res: AddStreamProxyResult) => {
|
||||
const host = window.location.host;
|
||||
const url = `ws://${host}/${res.app}/${res.streamId}.live.flv`;
|
||||
// const url = `ws://183.230.235.66:11010/${res.app}/${res.streamId}.live.flv`;
|
||||
// 将url 绑定到 nodeData
|
||||
nodeData.url = url;
|
||||
closePlayer(index);
|
||||
@@ -322,13 +377,11 @@ function doPlayer(nodeData: any, index: number = 0) {
|
||||
enableStashBuffer: false, // 关闭IO隐藏缓冲区
|
||||
stashInitialSize: 256, // 减少首帧显示等待时长
|
||||
};
|
||||
const playerConfig = {
|
||||
enableErrorRecover: true, // 启用错误恢复
|
||||
autoCleanupMaxBackwardDuration: 30,
|
||||
autoCleanupMinBackwardDuration: 10,
|
||||
};
|
||||
// 开启loading
|
||||
loading(index);
|
||||
const player = mpegts.createPlayer(videoConfig, playerConfig);
|
||||
const videoElement = itemRefs.value[index];
|
||||
addPlayerListener(index, player);
|
||||
const videoElement = itemRefs[index];
|
||||
if (videoElement) {
|
||||
player.attachMediaElement(videoElement);
|
||||
player.load();
|
||||
@@ -338,6 +391,8 @@ function doPlayer(nodeData: any, index: number = 0) {
|
||||
key: nodeData.id,
|
||||
data: nodeData,
|
||||
};
|
||||
// 播放完成后, 需要处理树组件的状态
|
||||
treeSelectHandle();
|
||||
} else {
|
||||
console.log('视频播放元素获取异常');
|
||||
}
|
||||
@@ -352,7 +407,9 @@ function closePlayVieo(plInfo: any) {
|
||||
try {
|
||||
plInfo.pause(); // 暂停
|
||||
plInfo.unload(); // 卸载
|
||||
plInfo.detachMediaElement();
|
||||
plInfo.destroy(); // 销毁
|
||||
treeSelectHandle();
|
||||
} catch (e) {
|
||||
console.log('播放器关闭失败,e=', e);
|
||||
}
|
||||
@@ -363,30 +420,19 @@ function closePlayer(index: number) {
|
||||
// 如果播放器存在,尝试关闭
|
||||
const pData = playerList[index];
|
||||
if (pData) {
|
||||
try {
|
||||
const player = pData.player;
|
||||
player.pause(); // 暂停
|
||||
player.unload(); // 卸载
|
||||
player.destroy(); // 销毁
|
||||
playerList[index] = null;
|
||||
} catch (e) {
|
||||
console.log('播放器关闭失败,e=', e);
|
||||
}
|
||||
closePlayVieo(pData.player);
|
||||
}
|
||||
}
|
||||
|
||||
function catchUp() {
|
||||
playerList.forEach((playerData) => {
|
||||
if (playerData) {
|
||||
const { player, el } = playerData;
|
||||
const end = player.buffered.end(player.buffered.length - 1);
|
||||
const diff = end - el.currentTime;
|
||||
if (diff > 2) {
|
||||
// 如果延迟超过2秒
|
||||
el.currentTime = end - 0.5; // 跳转到接近直播点
|
||||
}
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 开启或关闭播放器loading
|
||||
* @param index 播放器索引
|
||||
* @param cmd 0:开启,1:关闭
|
||||
*/
|
||||
function loading(index: number, cmd: Number = 0) {
|
||||
const loadinArr = [...playerLoading.value];
|
||||
loadinArr[index] = cmd === 0;
|
||||
playerLoading.value = loadinArr;
|
||||
}
|
||||
|
||||
let isSupportH265 = false;
|
||||
@@ -401,6 +447,20 @@ onUnmounted(() => {
|
||||
closePlayer(i);
|
||||
}
|
||||
});
|
||||
|
||||
function catchUp() {
|
||||
playerList.forEach((playerData) => {
|
||||
if (playerData) {
|
||||
const { player, el } = playerData;
|
||||
const end = player.buffered.end(player.buffered.length - 1);
|
||||
const diff = end - el.currentTime;
|
||||
if (diff > 2) {
|
||||
// 如果延迟超过2秒
|
||||
el.currentTime = end - 0.5; // 跳转到接近直播点
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -415,6 +475,10 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.c-tree {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.player.selected {
|
||||
border: 2px solid deepskyblue;
|
||||
}
|
||||
@@ -423,5 +487,9 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.btn-item.selected {
|
||||
color: #00a8ff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -21,7 +21,7 @@ export const querySchema: FormSchemaGetter = () => [
|
||||
options: getDictOptions(DictEnum.alarm_level, true),
|
||||
},
|
||||
fieldName: 'level',
|
||||
label: '级别',
|
||||
label: '预警级别',
|
||||
},
|
||||
/*{
|
||||
component: 'Select',
|
||||
@@ -45,7 +45,7 @@ export const columns: VxeGridProps['columns'] = [
|
||||
field: 'reportTime',
|
||||
},
|
||||
{
|
||||
title: '设备ip',
|
||||
title: '设备IP',
|
||||
field: 'deviceIp',
|
||||
},
|
||||
{
|
||||
@@ -53,7 +53,7 @@ export const columns: VxeGridProps['columns'] = [
|
||||
field: 'deviceName',
|
||||
},
|
||||
{
|
||||
title: '级别',
|
||||
title: '预警级别',
|
||||
field: 'level',
|
||||
slots: {
|
||||
default: ({ row }: any) => {
|
||||
@@ -138,7 +138,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: '设备名称',
|
||||
label: '设备IP',
|
||||
fieldName: 'deviceIp',
|
||||
component: 'Input',
|
||||
disabled: true,
|
||||
@@ -152,7 +152,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '重要级别',
|
||||
label: '预警级别',
|
||||
fieldName: 'level',
|
||||
component: 'Select',
|
||||
disabled: true,
|
||||
@@ -185,7 +185,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
label: '描述',
|
||||
label: '预警描述',
|
||||
disabled: true,
|
||||
fieldName: 'description',
|
||||
component: 'Textarea',
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { Descriptions, DescriptionsItem, Image, Tag } from 'ant-design-vue';
|
||||
import { Descriptions, DescriptionsItem, Image, Tag,Divider } from 'ant-design-vue';
|
||||
import { queryAlarmEventAttachmentsList } from '#/api/sis/alarmEventAttachments';
|
||||
import type { AlarmEventAttachmentsVO } from '#/api/sis/alarmEventAttachments/model';
|
||||
import { fallImg } from './data';
|
||||
@@ -72,15 +72,24 @@ function loadProcessList() {
|
||||
:labelStyle="{ width: '120px' }"
|
||||
style="margin-bottom: 30px"
|
||||
>
|
||||
<DescriptionsItem label="预警编号">
|
||||
<DescriptionsItem label="预警编码">
|
||||
{{ warningDetail.id }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="预警时间">
|
||||
{{ warningDetail.reportTime }}
|
||||
<DescriptionsItem label="预警类型">
|
||||
{{ warningDetail.bigTypeName + ' - ' + warningDetail.smallTypeName }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="级别">
|
||||
<DescriptionsItem label="设备IP"
|
||||
>{{ warningDetail.deviceIp }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="设备名称"
|
||||
>{{ warningDetail.deviceName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="预警位置" :span="2">
|
||||
{{ warningDetail.deviceName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="预警级别">
|
||||
<Tag
|
||||
:color="
|
||||
warningDetail.level === '特大'
|
||||
@@ -93,42 +102,15 @@ function loadProcessList() {
|
||||
{{ warningDetail.levelName }}
|
||||
</Tag>
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="预警类型">
|
||||
{{ warningDetail.bigTypeName + ' - ' + warningDetail.smallTypeName }}
|
||||
<DescriptionsItem label="预警时间">
|
||||
{{ warningDetail.reportTime }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="设备ip"
|
||||
>{{ warningDetail.deviceIp }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="设备ip"
|
||||
>{{ warningDetail.deviceName }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="描述" :span="2">
|
||||
<DescriptionsItem label="预警描述" :span="2">
|
||||
{{ warningDetail.description }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="所在位置">
|
||||
{{ warningDetail.deviceName }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="处理状态">
|
||||
<Tag>
|
||||
{{ warningDetail.stateName }}
|
||||
</Tag>
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="处理情况" :span="2">
|
||||
{{ warningDetail.processingDetails || '-' }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="处理时间" :span="2">
|
||||
{{ warningDetail.solveTime || '-' }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem :span="1" label="附件信息">
|
||||
<DescriptionsItem :span="2" label="相关图片">
|
||||
<div class="file-box">
|
||||
<div class="img-box" v-for="item in currFiles">
|
||||
<Image
|
||||
@@ -140,7 +122,33 @@ function loadProcessList() {
|
||||
</div>
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem :span="1" label="报警视频"></DescriptionsItem>
|
||||
<DescriptionsItem :span="2" label="报警视频"></DescriptionsItem>
|
||||
</Descriptions>
|
||||
<Divider orientation="left">处理</Divider>
|
||||
<Descriptions
|
||||
v-if="warningDetail"
|
||||
size="small"
|
||||
:column="2"
|
||||
bordered
|
||||
:labelStyle="{ width: '120px' }"
|
||||
style="margin-bottom: 30px">
|
||||
<DescriptionsItem label="处理状态">
|
||||
<Tag>
|
||||
{{ warningDetail.stateName }}
|
||||
</Tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="处理人">
|
||||
{{ warningDetail.solveName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="处理人电话">
|
||||
{{ warningDetail.solvePhone }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="处理时间">
|
||||
{{ warningDetail.solveTime || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="处理情况" :span="2">
|
||||
{{ warningDetail.processingDetails || '-' }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
@@ -21,7 +21,7 @@ export const querySchema: FormSchemaGetter = () => [
|
||||
options: getDictOptions(DictEnum.alarm_level, true),
|
||||
},
|
||||
fieldName: 'level',
|
||||
label: '级别',
|
||||
label: '预警级别',
|
||||
},
|
||||
/*{
|
||||
component: 'Select',
|
||||
@@ -45,7 +45,7 @@ export const columns: VxeGridProps['columns'] = [
|
||||
field: 'reportTime',
|
||||
},
|
||||
{
|
||||
title: '设备ip',
|
||||
title: '设备IP',
|
||||
field: 'deviceIp',
|
||||
},
|
||||
{
|
||||
@@ -53,7 +53,7 @@ export const columns: VxeGridProps['columns'] = [
|
||||
field: 'deviceName',
|
||||
},
|
||||
{
|
||||
title: '级别',
|
||||
title: '预警级别',
|
||||
field: 'level',
|
||||
slots: {
|
||||
default: ({ row }: any) => {
|
||||
@@ -138,7 +138,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: '设备名称',
|
||||
label: '设备IP',
|
||||
fieldName: 'deviceIp',
|
||||
component: 'Input',
|
||||
disabled: true,
|
||||
@@ -152,7 +152,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '重要级别',
|
||||
label: '预警级别',
|
||||
fieldName: 'level',
|
||||
component: 'Select',
|
||||
disabled: true,
|
||||
@@ -185,7 +185,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
label: '描述',
|
||||
label: '预警描述',
|
||||
disabled: true,
|
||||
fieldName: 'description',
|
||||
component: 'Textarea',
|
||||
|
@@ -70,15 +70,23 @@ function loadProcessList() {
|
||||
:labelStyle="{ width: '120px' }"
|
||||
style="margin-bottom: 30px"
|
||||
>
|
||||
<DescriptionsItem label="预警编号">
|
||||
<DescriptionsItem label="预警编码">
|
||||
{{ warningDetail.id }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="预警时间">
|
||||
{{ warningDetail.reportTime }}
|
||||
<DescriptionsItem label="预警类型">
|
||||
{{ warningDetail.bigTypeName + ' - ' + warningDetail.smallTypeName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="设备IP"
|
||||
>{{ warningDetail.deviceIp }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="级别">
|
||||
<DescriptionsItem label="设备名称"
|
||||
>{{ warningDetail.deviceName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="预警位置" :span="2">
|
||||
{{ warningDetail.deviceName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="预警级别">
|
||||
<Tag
|
||||
:color="
|
||||
warningDetail.level === '特大'
|
||||
@@ -91,28 +99,16 @@ function loadProcessList() {
|
||||
{{ warningDetail.levelName }}
|
||||
</Tag>
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="预警类型">
|
||||
{{ warningDetail.bigTypeName + ' - ' + warningDetail.smallTypeName }}
|
||||
<DescriptionsItem label="预警时间">
|
||||
{{ warningDetail.reportTime }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="设备ip"
|
||||
>{{ warningDetail.deviceIp }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="设备ip"
|
||||
>{{ warningDetail.deviceName }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="描述" :span="2">
|
||||
<DescriptionsItem label="预警描述" :span="2">
|
||||
{{ warningDetail.description }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="所在位置">
|
||||
{{ warningDetail.deviceName }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="处理状态">
|
||||
|
||||
<DescriptionsItem label="处理状态" :span="2">
|
||||
<Tag color="processing">
|
||||
{{ warningDetail.stateName }}
|
||||
</Tag>
|
||||
@@ -126,7 +122,7 @@ function loadProcessList() {
|
||||
{{ warningDetail.processingTime || '-' }}
|
||||
</DescriptionsItem>-->
|
||||
|
||||
<DescriptionsItem :span="1" label="附件信息">
|
||||
<DescriptionsItem :span="2" label="相关图片">
|
||||
<div class="file-box">
|
||||
<div class="img-box" v-for="item in currFiles">
|
||||
<Image
|
||||
@@ -138,7 +134,7 @@ function loadProcessList() {
|
||||
</div>
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem :span="1" label="报警视频"></DescriptionsItem>
|
||||
<DescriptionsItem :span="2" label="报警视频"></DescriptionsItem>
|
||||
</Descriptions>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
@@ -22,7 +22,7 @@ export const querySchema: FormSchemaGetter = () => [
|
||||
options: getDictOptions(DictEnum.alarm_level, true),
|
||||
},
|
||||
fieldName: 'level',
|
||||
label: '级别',
|
||||
label: '预警级别',
|
||||
},
|
||||
/*{
|
||||
component: 'Select',
|
||||
@@ -46,7 +46,7 @@ export const columns: VxeGridProps['columns'] = [
|
||||
field: 'reportTime',
|
||||
},
|
||||
{
|
||||
title: '设备ip',
|
||||
title: '设备IP',
|
||||
field: 'deviceIp',
|
||||
},
|
||||
{
|
||||
@@ -54,7 +54,7 @@ export const columns: VxeGridProps['columns'] = [
|
||||
field: 'deviceName',
|
||||
},
|
||||
{
|
||||
title: '级别',
|
||||
title: '预警级别',
|
||||
field: 'level',
|
||||
slots: {
|
||||
default: ({ row }: any) => {
|
||||
@@ -139,7 +139,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: '设备名称',
|
||||
label: '设备IP',
|
||||
fieldName: 'deviceIp',
|
||||
component: 'Input',
|
||||
disabled: true,
|
||||
@@ -153,7 +153,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '重要级别',
|
||||
label: '预警级别',
|
||||
fieldName: 'level',
|
||||
component: 'Select',
|
||||
disabled: true,
|
||||
@@ -186,7 +186,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
label: '描述',
|
||||
label: '预警描述',
|
||||
disabled: true,
|
||||
fieldName: 'description',
|
||||
component: 'Textarea',
|
||||
@@ -196,6 +196,19 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// 插入分割线
|
||||
{
|
||||
component: 'Divider',
|
||||
fieldName: '_divider',
|
||||
formItemClass: 'col-span-2',
|
||||
hideLabel: true,
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => h('div', '处理'),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '处理人',
|
||||
fieldName: 'solveName',
|
||||
@@ -212,19 +225,6 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
fieldName: 'solveEmail',
|
||||
component: 'Input',
|
||||
},
|
||||
// 插入分割线
|
||||
{
|
||||
component: 'Divider',
|
||||
fieldName: '_divider',
|
||||
formItemClass: 'col-span-2',
|
||||
hideLabel: true,
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => h('div', '处理'),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
label: '备注',
|
||||
fieldName: 'remark',
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { Descriptions, DescriptionsItem, Image, Tag } from 'ant-design-vue';
|
||||
import {Descriptions, DescriptionsItem, Divider, Image, Tag} from 'ant-design-vue';
|
||||
import { queryAlarmEventAttachmentsList } from '#/api/sis/alarmEventAttachments';
|
||||
import type { AlarmEventAttachmentsVO } from '#/api/sis/alarmEventAttachments/model';
|
||||
import { fallImg } from './data';
|
||||
@@ -70,15 +70,23 @@ function loadProcessList() {
|
||||
:labelStyle="{ width: '120px' }"
|
||||
style="margin-bottom: 30px"
|
||||
>
|
||||
<DescriptionsItem label="预警编号">
|
||||
<DescriptionsItem label="预警编码">
|
||||
{{ warningDetail.id }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="预警时间">
|
||||
{{ warningDetail.reportTime }}
|
||||
<DescriptionsItem label="预警类型">
|
||||
{{ warningDetail.bigTypeName + ' - ' + warningDetail.smallTypeName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="设备IP"
|
||||
>{{ warningDetail.deviceIp }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="级别">
|
||||
<DescriptionsItem label="设备名称"
|
||||
>{{ warningDetail.deviceName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="预警位置" :span="2">
|
||||
{{ warningDetail.deviceName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="预警级别">
|
||||
<Tag
|
||||
:color="
|
||||
warningDetail.level === '特大'
|
||||
@@ -91,34 +99,13 @@ function loadProcessList() {
|
||||
{{ warningDetail.levelName }}
|
||||
</Tag>
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="预警类型">
|
||||
{{ warningDetail.bigTypeName + ' - ' + warningDetail.smallTypeName }}
|
||||
<DescriptionsItem label="预警时间">
|
||||
{{ warningDetail.reportTime }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="设备ip"
|
||||
>{{ warningDetail.deviceIp }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="设备ip"
|
||||
>{{ warningDetail.deviceName }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="描述" :span="2">
|
||||
<DescriptionsItem label="预警描述" :span="2">
|
||||
{{ warningDetail.description }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="所在位置">
|
||||
{{ warningDetail.deviceName }}
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem label="处理状态">
|
||||
<Tag color="success">
|
||||
{{ warningDetail.stateName }}
|
||||
</Tag>
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem :span="1" label="附件信息">
|
||||
<DescriptionsItem label="相关图片" :span="2">
|
||||
<div class="file-box">
|
||||
<div class="img-box" v-for="item in currFiles">
|
||||
<Image
|
||||
@@ -130,7 +117,27 @@ function loadProcessList() {
|
||||
</div>
|
||||
</DescriptionsItem>
|
||||
|
||||
<DescriptionsItem :span="1" label="报警视频"></DescriptionsItem>
|
||||
<DescriptionsItem :span="2" label="报警视频"></DescriptionsItem>
|
||||
</Descriptions>
|
||||
<Divider orientation="left">处理</Divider>
|
||||
<Descriptions
|
||||
v-if="warningDetail"
|
||||
size="small"
|
||||
:column="2"
|
||||
bordered
|
||||
:labelStyle="{ width: '120px' }"
|
||||
style="margin-bottom: 30px">
|
||||
<DescriptionsItem label="处理状态" :span="2">
|
||||
<Tag>
|
||||
{{ warningDetail.stateName }}
|
||||
</Tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="处理人">
|
||||
{{ warningDetail.solveName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="处理人电话">
|
||||
{{ warningDetail.solvePhone }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
Reference in New Issue
Block a user