From 1584b0ac93cd9f85635c1fa6fe1d98f9083bae28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=8B=87?= Date: Wed, 25 Dec 2024 09:29:39 +0800 Subject: [PATCH] feat: add wp Integration --- apps/web/src/plugin/components/index.ts | 1 + .../components/meeting-room/config.json | 70 +++ .../meeting-room/configure/index.tsx | 27 + .../plugin/components/meeting-room/icon.png | Bin 0 -> 13525 bytes .../meeting-room/runtime/action/index.tsx | 15 + .../runtime/action/useTrigger.tsx | 29 + .../components/meeting-room/runtime/index.tsx | 18 + .../meeting-room/runtime/reducer/index.tsx | 35 ++ .../runtime/reducer/useDynamic.tsx | 79 +++ .../components/meeting-room/typings.d.ts | 7 + .../meeting-room/view/hooks/index.tsx | 1 + .../meeting-room/view/hooks/useSource.tsx | 43 ++ .../components/meeting-room/view/index.tsx | 126 +++++ .../meeting-room/view/meeting/index.tsx | 520 ++++++++++++++++++ .../meeting-room/view/meeting/style.less | 280 ++++++++++ .../components/meeting-room/view/style.less | 65 +++ apps/web/src/plugin/constant.ts | 4 + apps/web/src/services/http/index.ts | 1 + apps/web/src/services/http/meeting.ts | 62 +++ 19 files changed, 1383 insertions(+) create mode 100644 apps/web/src/plugin/components/meeting-room/config.json create mode 100644 apps/web/src/plugin/components/meeting-room/configure/index.tsx create mode 100644 apps/web/src/plugin/components/meeting-room/icon.png create mode 100644 apps/web/src/plugin/components/meeting-room/runtime/action/index.tsx create mode 100644 apps/web/src/plugin/components/meeting-room/runtime/action/useTrigger.tsx create mode 100644 apps/web/src/plugin/components/meeting-room/runtime/index.tsx create mode 100644 apps/web/src/plugin/components/meeting-room/runtime/reducer/index.tsx create mode 100644 apps/web/src/plugin/components/meeting-room/runtime/reducer/useDynamic.tsx create mode 100644 apps/web/src/plugin/components/meeting-room/typings.d.ts create mode 100644 apps/web/src/plugin/components/meeting-room/view/hooks/index.tsx create mode 100644 apps/web/src/plugin/components/meeting-room/view/hooks/useSource.tsx create mode 100644 apps/web/src/plugin/components/meeting-room/view/index.tsx create mode 100644 apps/web/src/plugin/components/meeting-room/view/meeting/index.tsx create mode 100644 apps/web/src/plugin/components/meeting-room/view/meeting/style.less create mode 100644 apps/web/src/plugin/components/meeting-room/view/style.less create mode 100644 apps/web/src/services/http/meeting.ts diff --git a/apps/web/src/plugin/components/index.ts b/apps/web/src/plugin/components/index.ts index 7ba37a68..8ee5121b 100644 --- a/apps/web/src/plugin/components/index.ts +++ b/apps/web/src/plugin/components/index.ts @@ -7,3 +7,4 @@ export { default as ChartTimeSelect } from './chart-time-select'; export { default as entitySelect } from './entity-select'; export { default as chartMetricsSelect } from './chart-metrics-select'; export { default as multiEntitySelect } from './multi-entity-select'; +export { default as meetingRoom } from './switch'; diff --git a/apps/web/src/plugin/components/meeting-room/config.json b/apps/web/src/plugin/components/meeting-room/config.json new file mode 100644 index 00000000..a0fb07b7 --- /dev/null +++ b/apps/web/src/plugin/components/meeting-room/config.json @@ -0,0 +1,70 @@ +{ + "type": "meetingRoom", + "name": "Meeting Room", + "class": "meeting_room", + "icon": "./icon.png", + "defaultRow": 3, + "defaultCol": 5, + "minRow": 2, + "minCol": 4, + "configProps": [ + { + "style": "width: 100%", + "components": [ + { + "type": "entitySelect", + "title": "Entity", + "key": "entity", + "style": "width: 100%", + "componentProps": { + "size": "small", + "entityType": ["PROPERTY"], + "entityValueTypes": ["STRING", "LONG", "DOUBLE", "BOOLEAN"] + }, + "rules": { + "required": true + } + } + ] + }, + { + "style": "width: 100%", + "components": [ + { + "type": "input", + "title": "Label", + "key": "title", + "style": "width: 100%", + "defaultValue": "Label", + "componentProps": { + "size": "small", + "inputProps": { + "maxLength": 15 + } + } + } + ] + } + ], + "view": [ + { + "tag": "div", + "themes": { + "default": { + "class": "data-view__container" + } + }, + "children": [ + { + "tag": "span", + "themes": { + "default": { + "class": "data-view__content" + } + }, + "params": ["entity"] + } + ] + } + ] +} diff --git a/apps/web/src/plugin/components/meeting-room/configure/index.tsx b/apps/web/src/plugin/components/meeting-room/configure/index.tsx new file mode 100644 index 00000000..1dc2220a --- /dev/null +++ b/apps/web/src/plugin/components/meeting-room/configure/index.tsx @@ -0,0 +1,27 @@ +import { forwardRef } from 'react'; +import { RenderConfig } from '../../../render'; +import { useConnect } from '../runtime'; +import type { ConfigureType, ViewConfigProps } from '../typings'; + +interface ConfigPluginProps { + value: ViewConfigProps; + config: ConfigureType; + onOk: (data: ViewConfigProps) => void; + onChange: (data: ViewConfigProps) => void; +} +const Plugin = forwardRef((props: ConfigPluginProps, ref: any) => { + const { value, config, onOk, onChange } = props; + const { configure, handleChange } = useConnect({ value, config, onChange }); + + return ( + + ); +}); + +export default Plugin; diff --git a/apps/web/src/plugin/components/meeting-room/icon.png b/apps/web/src/plugin/components/meeting-room/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..231d227551e20d0c49014abb44fac8ec6f31a24d GIT binary patch literal 13525 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 void; +} +export const useAction = ({ onChange }: IProps) => { + const { handleChange } = useTrigger({ onChange }); + + return { + handleChange, + }; +}; diff --git a/apps/web/src/plugin/components/meeting-room/runtime/action/useTrigger.tsx b/apps/web/src/plugin/components/meeting-room/runtime/action/useTrigger.tsx new file mode 100644 index 00000000..8a237bb8 --- /dev/null +++ b/apps/web/src/plugin/components/meeting-room/runtime/action/useTrigger.tsx @@ -0,0 +1,29 @@ +// import { useRef } from 'react'; +import type { ViewConfigProps } from '../../typings'; + +interface IProps { + onChange: (data: ViewConfigProps) => void; +} +export const useTrigger = ({ onChange }: IProps) => { + // const prevStateRef = useRef(); + + const handleChange = (value: ViewConfigProps) => { + // const { entity } = value || {}; + // const { value: entityValue, rawData } = entity || {}; + // const prevEntityValue = prevStateRef.current?.entity?.value; + + // // 如果实体变化时,更新title为当前选中实体名称 + // if (prevEntityValue !== entityValue) { + // // 当前选中实体 + // const { entityName } = rawData || {}; + // entityName && (value.title = entityName); + // } + + // prevStateRef.current = value; + onChange(value); + }; + + return { + handleChange, + }; +}; diff --git a/apps/web/src/plugin/components/meeting-room/runtime/index.tsx b/apps/web/src/plugin/components/meeting-room/runtime/index.tsx new file mode 100644 index 00000000..dc404956 --- /dev/null +++ b/apps/web/src/plugin/components/meeting-room/runtime/index.tsx @@ -0,0 +1,18 @@ +import { useAction } from './action'; +import { useReducer } from './reducer'; +import type { ConfigureType, ViewConfigProps } from '../typings'; + +interface IProps { + value: ViewConfigProps; + config: ConfigureType; + onChange: (data: ViewConfigProps) => void; +} +export const useConnect = ({ value, config, onChange }: IProps) => { + const { handleChange } = useAction({ value, config, onChange }); + const { configure } = useReducer({ value, config }); + + return { + configure, + handleChange, + }; +}; diff --git a/apps/web/src/plugin/components/meeting-room/runtime/reducer/index.tsx b/apps/web/src/plugin/components/meeting-room/runtime/reducer/index.tsx new file mode 100644 index 00000000..e0342053 --- /dev/null +++ b/apps/web/src/plugin/components/meeting-room/runtime/reducer/index.tsx @@ -0,0 +1,35 @@ +import { useMemo } from 'react'; +import { cloneDeep } from 'lodash-es'; +import { useDynamic } from './useDynamic'; +import type { ConfigureType, ViewConfigProps } from '../../typings'; + +interface IProps { + value: ViewConfigProps; + config: ConfigureType; +} +export const useReducer = ({ value, config }: IProps) => { + const { updateDynamicForm } = useDynamic(); + + // /** config实时保存value */ + // const updateConfigState = (value: ViewConfigProps, config: ConfigureType) => { + // config.config = value || {}; + + // return config; + // }; + + /** 生成新的configure */ + const configure = useMemo(() => { + // 按顺序执行回调,返回最新的configure + const ChainCallList = [updateDynamicForm]; + + const newConfig = ChainCallList.reduce((config, fn) => { + return fn(value, cloneDeep(config)); + }, config); + + return { ...newConfig }; + }, [value, config]); + + return { + configure, + }; +}; diff --git a/apps/web/src/plugin/components/meeting-room/runtime/reducer/useDynamic.tsx b/apps/web/src/plugin/components/meeting-room/runtime/reducer/useDynamic.tsx new file mode 100644 index 00000000..b0ab1189 --- /dev/null +++ b/apps/web/src/plugin/components/meeting-room/runtime/reducer/useDynamic.tsx @@ -0,0 +1,79 @@ +import type { ConfigureType, ViewConfigProps } from '../../typings'; + +export const useDynamic = () => { + /** + * 动态生成的每一项表单 + */ + const generateFormItem = (title: string, index: string, value: Record) => { + return { + $$type: 'dynamic', + title, + style: 'display: flex;margin-bottom: 20px;', + components: [ + { + type: 'iconSelect', + key: `Icon_${index}`, + style: 'flex: 1;padding-right: 12px;', + defaultValue: value[`Icon_${index}`] || undefined, + componentProps: { + size: 'small', + }, + }, + { + type: 'iconColorSelect', + key: `IconColor_${index}`, + style: 'flex: 1;', + defaultValue: value[`IconColor_${index}`] || undefined, + componentProps: { + size: 'small', + }, + }, + ], + }; + }; + + /** + * 生成动态configure逻辑 + */ + const dynamicConfigure = ( + currentEntity: Required['rawData'], + value: Record, + ) => { + const { entityValueAttribute, entityId } = currentEntity || {}; + const { enum: enumStruct } = entityValueAttribute || {}; + + // 非枚举类型 + if (!enumStruct) return [generateFormItem(`Appearance`, entityId?.toString(), value)]; + + // 枚举类型 + return Object.keys(enumStruct || {}).map(enumKey => { + const enumValue = enumStruct[enumKey]; + return generateFormItem(`Appearance of ${enumValue}`, enumKey, value); + }); + }; + + /** 动态渲染表单 */ + const updateDynamicForm = (value: ViewConfigProps, config: ConfigureType) => { + const { entity } = value || {}; + // 获取当前选中实体 + const { rawData: currentEntity } = entity || {}; + if (!currentEntity) return config; + + // 渲染动态表单 + const result = dynamicConfigure(currentEntity, value); + if (!result) return config; + + // 动态渲染表单 + const { configProps } = config || {}; + const newConfigProps = [ + ...configProps.filter((item: any) => item.$$type !== 'dynamic'), + ...result, + ]; + config.configProps = newConfigProps; + return config; + }; + + return { + updateDynamicForm, + }; +}; diff --git a/apps/web/src/plugin/components/meeting-room/typings.d.ts b/apps/web/src/plugin/components/meeting-room/typings.d.ts new file mode 100644 index 00000000..861d4c0e --- /dev/null +++ b/apps/web/src/plugin/components/meeting-room/typings.d.ts @@ -0,0 +1,7 @@ +export interface ViewConfigProps { + title: string; + entity: EntityOptionType; + [key: string]: string; +} + +export type ConfigureType = any; diff --git a/apps/web/src/plugin/components/meeting-room/view/hooks/index.tsx b/apps/web/src/plugin/components/meeting-room/view/hooks/index.tsx new file mode 100644 index 00000000..f8c59354 --- /dev/null +++ b/apps/web/src/plugin/components/meeting-room/view/hooks/index.tsx @@ -0,0 +1 @@ +export { useSource } from './useSource'; diff --git a/apps/web/src/plugin/components/meeting-room/view/hooks/useSource.tsx b/apps/web/src/plugin/components/meeting-room/view/hooks/useSource.tsx new file mode 100644 index 00000000..c2ee149a --- /dev/null +++ b/apps/web/src/plugin/components/meeting-room/view/hooks/useSource.tsx @@ -0,0 +1,43 @@ +import { useEffect, useMemo } from 'react'; +import { useRequest } from 'ahooks'; +import ws, { getExChangeTopic } from '@/services/ws'; +import { awaitWrap, entityAPI, getResponseData, isRequestSuccess } from '@/services/http'; +import type { ViewConfigProps } from '../../typings'; + +interface IProps { + entity: ViewConfigProps['entity']; +} +export const useSource = (props: IProps) => { + const { entity } = props; + + const { data: entityStatusValue, runAsync: getEntityStatusValue } = useRequest( + async () => { + if (!entity) return; + const { value } = entity || {}; + + const [error, resp] = await awaitWrap(entityAPI.getEntityStatus({ id: value })); + if (error || !isRequestSuccess(resp)) return; + + return getResponseData(resp)?.value; + }, + { manual: true }, + ); + useEffect(() => { + getEntityStatusValue(); + }, [entity]); + + const topic = useMemo(() => { + const entityKey = entity?.rawData?.entityKey?.toString(); + return entityKey && getExChangeTopic(entityKey); + }, [entity]); + // 订阅 WS 主题 + useEffect(() => { + if (!topic) return; + + return ws.subscribe(topic, getEntityStatusValue); + }, [topic]); + + return { + entityStatusValue, + }; +}; diff --git a/apps/web/src/plugin/components/meeting-room/view/index.tsx b/apps/web/src/plugin/components/meeting-room/view/index.tsx new file mode 100644 index 00000000..4dbcf9f8 --- /dev/null +++ b/apps/web/src/plugin/components/meeting-room/view/index.tsx @@ -0,0 +1,126 @@ +import { useMemo } from 'react'; +import * as Icons from '@milesight/shared/src/components/icons'; +import { Tooltip } from '@/plugin/view-components'; +import { useSource } from './hooks'; +import type { ViewConfigProps } from '../typings'; +import MeetingSchedule from './meeting'; +import './style.less'; + +interface Props { + config: ViewConfigProps; + configJson: CustomComponentProps; +} + +interface Entity { + description: string; + label: string; + rawData: RawData; + value: number; + valueType: string; +} + +interface RawData { + deviceName: string; + entityAccessMod: string; + entityId: string; + entityKey: string; +} + +const View = (props: Props) => { + const { config, configJson } = props; + const { title, entity } = config || {}; + const { entityStatusValue } = useSource({ entity }); + const { isPreview } = configJson || {}; + + const meetingData: RawData = { + deviceName: '', + entityAccessMod: 'R', + entityId: "", + entityKey: '' + }; + const entityData: Entity = { + description: '', + label: '', + rawData: { + deviceName: '', + entityAccessMod: 'R', + entityId: "", + entityKey: '' + }, + value: 1, + valueType: '' + }; + + if (config?.entity) { + const { description, label, rawData, value, valueType } = config.entity; + entity.label = label; + entity.rawData = rawData; + entity.value = value; + entity.valueType = valueType; + const { deviceName, entityAccessMod, entityId, entityKey } = entity.rawData; + meetingData.deviceName = deviceName; + meetingData.entityAccessMod = entityAccessMod; + meetingData.entityId = entityId; + meetingData.entityKey = entityKey; + console.log(config) + } + + // 当前实体实时数据 + const currentEntityData = useMemo(() => { + const { rawData: currentEntity, value: entityValue } = entity || {}; + if (!currentEntity) return; + + // 获取当前选中实体 + const { entityValueAttribute } = currentEntity || {}; + const { enum: enumStruct, unit } = entityValueAttribute || {}; + const currentEntityStatus = entityStatusValue?.toString(); + + // 枚举类型 + if (enumStruct) { + const currentKey = Object.keys(enumStruct).find(enumKey => { + return enumKey === currentEntityStatus; + }); + if (!currentKey) return; + + return { + label: enumStruct[currentKey], + value: currentKey, + }; + } + + // 非枚举类型 + return { + label: unit ? `${currentEntityStatus ?? '- '}${unit}` : `${currentEntityStatus ?? ''}`, + value: entityValue, + }; + }, [entity, entityStatusValue]); + // 当前实体图标 + const { Icon, iconColor } = useMemo(() => { + const { value } = currentEntityData || {}; + const iconType = config?.[`Icon_${value}`]; + const Icon = iconType && Icons[iconType as keyof typeof Icons]; + const iconColor = config?.[`IconColor_${value}`]; + + return { + Icon, + iconColor, + }; + }, [config, currentEntityData]); + + return ( +
+ + {/* + {Icon && ( + + )}
+ +
+ {currentEntityData?.label || '-'} +
+
*/} +
+ ); +}; + +export default View; diff --git a/apps/web/src/plugin/components/meeting-room/view/meeting/index.tsx b/apps/web/src/plugin/components/meeting-room/view/meeting/index.tsx new file mode 100644 index 00000000..346edaec --- /dev/null +++ b/apps/web/src/plugin/components/meeting-room/view/meeting/index.tsx @@ -0,0 +1,520 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, toast } from '@milesight/shared/src/components'; +import { TextField, Select, MenuItem, FormControl, InputLabel, Button } from '@mui/material'; +import { LocalizationProvider, DatePicker } from '@mui/x-date-pickers'; +import { meetingAPI, entityAPI } from '@/services/http'; +import './style.less'; +import { AxiosResponse } from 'axios'; + +const daysOfWeek = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; +const timeSlots = ['00:00', '01:00', '02:00', '03:00', '04:00', '05:00', '06:00', '07:00', '08:00', '09: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']; + +interface Entity { + description: string; + label: string; + rawData: string; + value: number; + valueType: string; +} + +interface RawData { + deviceName: string; + entityAccessMod: string; + entityId: string; + entityKey: string; +} + +interface MeetingRoomScheduleProps { + meetingData: RawData; +} + +interface Meeting { + meeting: string; + subject: string; + firstStartTime: number; + lastEndTime: number; + createTime: number; + meetingId?: string; +} + +interface ApiResponse { + data: T[]; + status: string; + message: string; +} + +const MeetingRoomSchedule: React.FC = ({ meetingData }) => { + const [meetingOffset, setMeetingOffset] = useState(0); // 86400 + const [modalIsOpen, setModalIsOpen] = useState(false); + const [selectedMeetingId, setSelectedMeetingId] = useState(''); + const [selectedDay, setSelectedDay] = useState(''); + const [selectedDate, setSelectedDate] = useState(null); + const [selectedStartTime, setSelectedStartTime] = useState(''); + const [selectedEndTime, setSelectedEndTime] = useState(''); + const [meetingId, setMeetingId] = useState(meetingData.deviceName + "预约"); + const [meetingTitle, setMeetingTitle] = useState(meetingData.deviceName + "预约"); + const [meetingRoomKey, setMeetingRoomKey] = useState(meetingData.entityKey); + const [dragging, setDragging] = useState(false); + const [allHistoryData, setAllHistoryData] = useState([]); + const [filteredHistoryData, setFilteredHistoryData] = useState([]); + const [currentWeek, setCurrentWeek] = useState(0); + const [editingMeeting, setEditingMeeting] = useState(null); + const [deleteConfirmVisible, setDeleteConfirmVisible] = useState(false); + const [meetingToDelete, setMeetingToDelete] = useState(null); + + const openModal = () => { + setModalIsOpen(true); + }; + + const closeModal = () => { + setModalIsOpen(false); + setMeetingTitle(meetingData.deviceName + "预约"); + setEditingMeeting(null); + }; + + const handleScheduleMeeting = async () => { + const sortedTimes = [selectedStartTime, selectedEndTime].sort(); + const startTime = sortedTimes[0]; + const endTime = sortedTimes[1]; + + const [startHour, startMinute] = startTime.split(':').map(Number); + const [endHour, endMinute] = endTime.split(':').map(Number); + const startTotalMinutes = startHour * 60 + startMinute; + const endTotalMinutes = endHour * 60 + endMinute; + if (endTotalMinutes - startTotalMinutes < 60) { + toast.warning('请选择至少1小时的时间段'); + return; + } + if (endTotalMinutes - startTotalMinutes > 180) { + toast.warning('请选择最多3小时的时间段'); + return; + } + const first = Math.floor(new Date(selectedDate?.toISOString().split('T')[0] + " " + selectedStartTime).getTime() / 1000); + const last = Math.floor(new Date(selectedDate?.toISOString().split('T')[0] + " " + selectedEndTime).getTime() / 1000); + const meetingDataToSend = { + type: !editingMeeting?0:1, + subject: meetingTitle, + meeting: meetingId, + id: meetingData.entityId, + key: meetingRoomKey, + time: selectedStartTime, + date: selectedDate?.toISOString().split('T')[0], + first: !editingMeeting?first:first+ meetingOffset, + last: !editingMeeting?last:last+ meetingOffset, + }; + try { + await meetingAPI.addMeeting(meetingDataToSend); + // 显示成功通知 + toast.success('Meeting scheduled successfully'); + } catch (error) { + console.error('Error scheduling meeting:', error); + toast.error('Failed to schedule meeting'); + } + closeModal(); + fetchHistoryData(currentWeek); // Refresh the history data + }; + + const handleDeleteMeeting = async () => { + console.error('Error deleting meeting:', selectedStartTime); + const meetingDataToSend = { + type: 2, + subject: meetingTitle, + meeting: meetingId, + id: meetingData.entityId, + key: meetingRoomKey, + time: selectedStartTime, + date: selectedDate?.toISOString().split('T')[0], + first: Math.floor(new Date(selectedDate?.toISOString().split('T')[0] + " " + selectedStartTime).getTime() / 1000 + meetingOffset), + last: Math.floor(new Date(selectedDate?.toISOString().split('T')[0] + " " + selectedEndTime).getTime() / 1000 + meetingOffset), + }; + try { + await meetingAPI.addMeeting(meetingDataToSend); + // 显示成功通知 + toast.success('Delete Meeting scheduled successfully'); + } catch (error) { + console.error('Error scheduling meeting:', error); + toast.error('Failed to schedule meeting'); + } + closeModal(); + fetchHistoryData(currentWeek); // Refresh the history data + }; + + const isPastTimeSlot = (day: string, time: string, weekOffset: number) => { + const now = new Date(); + const [hours, minutes] = time.split(':').map(Number); + const slotDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes); + const currentDayIndex = now.getDay() - 1; + const slotDayIndex = daysOfWeek.indexOf(day); + slotDate.setDate(now.getDate() + (slotDayIndex - currentDayIndex) + (weekOffset * 7)); + return slotDate < now; + }; + + const handleMouseDown = (day: string, time: string) => { + if (!isPastTimeSlot(day, time, currentWeek)) { + const meeting = getMeetingByTime(day, time); + if (meeting) { + // Edit existing meeting + setEditingMeeting(meeting); + setMeetingTitle(meeting.subject); + console.log("=======meeting.meeting======", meeting) + setMeetingId(meeting.meeting); + setSelectedStartTime(new Date(meeting.firstStartTime * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })); + setSelectedEndTime(new Date(meeting.lastEndTime * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })); + setSelectedDate(new Date(meeting.firstStartTime * 1000)); + openModal(); + } else { + // Schedule new meeting + setDragging(true); + // setSelectedMeetingId(); + setSelectedDay(day); + setSelectedStartTime(time); + setSelectedEndTime(time); + const weekDates = getWeekDates(currentWeek); + setSelectedDate(weekDates[daysOfWeek.indexOf(day)]); + } + } + }; + + const handleMouseUp = (day: string, time: string) => { + if (dragging) { + setDragging(false); + const sortedTimes = [selectedStartTime, time].sort(); + const startTime = sortedTimes[0]; + const endTime = sortedTimes[1]; + + const [startHour, startMinute] = startTime.split(':').map(Number); + const [endHour, endMinute] = endTime.split(':').map(Number); + const startTotalMinutes = startHour * 60 + startMinute; + const endTotalMinutes = endHour * 60 + endMinute; + + if (endTotalMinutes - startTotalMinutes < 60) { + toast.warning('请选择至少1小时的时间段'); + return; + } + if (endTotalMinutes - startTotalMinutes > 180) { + toast.warning('请选择最多3小时的时间段'); + return; + } + + setSelectedStartTime(startTime); + setSelectedEndTime(endTime); + openModal(); + } + }; + + const handleMouseEnter = (day: string, time: string) => { + if (dragging && day === selectedDay) { + setSelectedEndTime(time); + } + }; + + const getWeekStartAndEnd = (weekOffset: number) => { + const now = new Date(); + const currentDayOfWeek = now.getDay(); + const startOfWeek = new Date(now); + startOfWeek.setDate(now.getDate() - currentDayOfWeek + 1 + (weekOffset * 7)); + startOfWeek.setHours(0, 0, 0, 0); // 设置为当天的开始时间 + + const endOfWeek = new Date(startOfWeek); + endOfWeek.setDate(startOfWeek.getDate() + 6); + endOfWeek.setHours(23, 59, 59, 999); // 设置为当天的结束时间 + + return { + startOfWeek: startOfWeek.getTime(), + endOfWeek: endOfWeek.getTime(), + }; + }; + + const handleDateChange = (data: any) => { + console.log("-----------==--=", data) + const tomorrow = new Date(data); + tomorrow.setDate(tomorrow.getDate() + 1); + setSelectedDate(tomorrow) + }; + + +// 转换 response 数据 +const transformApiResponse = (response) => { + return response.data.data.content.map(item => { + const value = item.value.match(/subject=([^,]+),.*meetingId=([^,]+),.*firstStartTime=([^,]+),.*lastEndTime=([^,]+)/); + if (value) { + const subject = value[1].trim(); + const meetingId = value[2].trim(); + const startTime = value[3].trim(); + const endTime = value[4].trim(); + // const startDateTime = new Date(`${startDate}T${startTime}`).getTime(); + // const endDateTime = startDateTime + (3 * 60 * 60 * 1000); // 假设会议持续3小时 + + return { + meetingId, + meeting: meetingId, + subject, + firstStartTime: startTime, + lastEndTime: endTime, + id: item.timestamp.toString(), + }; + } + return null; + }).filter(item => item !== null); +}; + const fetchHistoryData = async (weekOffset: number) => { + const { startOfWeek, endOfWeek } = getWeekStartAndEnd(weekOffset); + try { + + const response = await entityAPI.getHistory({ + entity_id: meetingData.entityId, + start_timestamp: startOfWeek, + end_timestamp: endOfWeek, + page_number: 1, + page_size: 999, + }); + const unifiedApiData = transformApiResponse(response); + // 模拟从后端获取的数据 + const mockResponse: AxiosResponse> = { + data: { + data: unifiedApiData, + status: 'success', + message: 'Fetched successfully', + }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + }; + const result = mockResponse.data; + setAllHistoryData(result.data); + filterMeetingsForCurrentWeek(result.data); + } catch (error) { + console.error('Error fetching history data:', error); + } + }; + + const filterMeetingsForCurrentWeek = (meetings: Meeting[]) => { + const weekDates = getWeekDates(currentWeek); + const startOfWeek = weekDates[0].getTime() / 1000; + const endOfWeek = weekDates[6].getTime() / 1000 + 24 * 60 * 60 - 1; // End of the last day of the week + + // 过滤出当前周的会议 + const filteredMeetings = meetings.filter(meeting => { + return meeting.firstStartTime >= startOfWeek && meeting.lastEndTime <= endOfWeek; + }); + + console.log('filteredMeetings=================', filteredMeetings); + // 按 firstStartTime 分组,并在每个分组内根据 createTime 进行排序,取出最大的值 + const meetingMap = new Map(); + + + filteredMeetings.forEach(meeting => { + const existingMeeting = meetingMap.get(meeting.firstStartTime); + if (!existingMeeting || meeting.createTime > existingMeeting.createTime) { + meetingMap.set(meeting.firstStartTime, meeting); + } + }); + + // 将 Map 转换为数组 + const uniqueMeetings = Array.from(meetingMap.values()); + console.log('uniqueMeetings=================', uniqueMeetings); + // 过滤出当前周的会议 + const uniqueMeetingss = uniqueMeetings.filter(meeting => { + return meeting.subject !="" &&meeting.subject !="null"; + }); + + setFilteredHistoryData(uniqueMeetingss); + }; + + useEffect(() => { + fetchHistoryData(currentWeek); + const intervalId = setInterval(fetchHistoryData, 60000); + return () => clearInterval(intervalId); + }, []); + + useEffect(() => { + filterMeetingsForCurrentWeek(allHistoryData); + }, [currentWeek, allHistoryData]); + + const getWeekDates = (weekOffset: number) => { + const now = new Date(); + const currentDayOfWeek = now.getDay(); + const startOfWeek = new Date(now); + startOfWeek.setDate(now.getDate() - currentDayOfWeek + 1 + (weekOffset * 7)); + const dates = []; + for (let i = 0; i < 7; i++) { + const date = new Date(startOfWeek); + date.setDate(startOfWeek.getDate() + i); + dates.push(date); + } + return dates; + }; + + const weekDates = getWeekDates(currentWeek); + + const handleWeekChange = (weekOffset: number) => { + fetchHistoryData(weekOffset); // Fetch history data for the new week + setCurrentWeek(weekOffset); + }; + + const getTimeSlotClass = (day: string, time: string) => { + const pastTimeSlot = isPastTimeSlot(day, time, currentWeek); + const isSelected = dragging && day === selectedDay && ( + (time >= selectedStartTime && time <= selectedEndTime) || + (time >= selectedEndTime && time <= selectedStartTime) + ); + + let className = `time-slot ${pastTimeSlot ? 'past-time-slot' : ''} ${isSelected ? 'selected-time-slot' : ''}`; + return className; + }; + + const getMeetingByTime = (day: string, time: string): Meeting | null => { + for (const meeting of filteredHistoryData) { + const meetingDay = new Date(meeting.firstStartTime * 1000).toLocaleDateString(); + const meetingStartTime = new Date(meeting.firstStartTime * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const meetingEndTime = new Date(meeting.lastEndTime * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + + if (meetingDay === weekDates[daysOfWeek.indexOf(day)].toLocaleDateString() && + time >= meetingStartTime && time < meetingEndTime) { + return meeting; + } + } + return null; + }; + + const renderHistoryMeetings = () => { + console.log("=============", filteredHistoryData) + return filteredHistoryData.map((meeting, index) => { + const startDate = new Date(meeting.firstStartTime * 1000); + const endDate = new Date(meeting.lastEndTime * 1000); + const dayIndex = startDate.getDay() - 1; + const startTime = startDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const endTime = endDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + + const top = parseInt(startTime.split(':')[0], 10) * 26 + parseInt(startTime.split(':')[1], 10) * 0.5 + 46; + const height = ((parseInt(endTime.split(':')[0], 10) * 26 + parseInt(endTime.split(':')[1], 10) * 0.5) - + (parseInt(startTime.split(':')[0], 10) * 26 + parseInt(startTime.split(':')[1], 10) * 0.5)) + 23; + + const isPast = endDate < new Date(); + + return ( +
handleMouseDown(daysOfWeek[(dayIndex<0?6:dayIndex)], startTime)} + > +
+
{meeting.subject}
+
{startTime} - {endTime}
+
+
+ ); + }); + }; + + return ( +
+
+
+

{meetingData.deviceName}日程

+
+
+ + + +
+
+
+ {daysOfWeek.map((day, index) => ( +
+
{day} - {weekDates[index].toLocaleDateString()}
+ {timeSlots.map(time => ( +
handleMouseDown(day, time)} + onMouseUp={() => handleMouseUp(day, time)} + onMouseEnter={() => handleMouseEnter(day, time)} + > + +
+ ))} +
+ ))} + {renderHistoryMeetings()} +
+ + 取消, + editingMeeting && , + , + ]} + > +
+ setMeetingTitle(e.target.value)} + fullWidth + margin="normal" + /> + {/* + 会议日期 + handleDateChange(e)} + /> + */} + + 开始时间 + + + + 结束时间 + + +
+
+ + setDeleteConfirmVisible(false)} + onOk={handleDeleteMeeting} + title="确认删除" + onOkText="删除" + onCancelText="取消" + > +
+

您确定要删除这个会议吗?

+
+
+
+ ); +}; + +export default MeetingRoomSchedule; diff --git a/apps/web/src/plugin/components/meeting-room/view/meeting/style.less b/apps/web/src/plugin/components/meeting-room/view/meeting/style.less new file mode 100644 index 00000000..054e6667 --- /dev/null +++ b/apps/web/src/plugin/components/meeting-room/view/meeting/style.less @@ -0,0 +1,280 @@ + +.modal-content { + margin-bottom: 66px; +} + +.meeting-button-cancel{ + position: absolute; + right: 300px; + bottom: 12px; + width: 23%; + height: 40px; + padding: 2px 32px; /* 内边距 */ + margin: 4px 20px; /* 外边距 */ + font-size: 16px; /* 字体大小 */ + color: white; /* 字体颜色 */ + text-align: center; /* 文字居中 */ + text-decoration: none; /* 去掉下划线 */ + background-color: #3d3f4141; /* 背景颜色 */ + border: none; /* 去掉默认边框 */ + border-radius: 12px; /* 圆角半径 */ + cursor: pointer; /* 鼠标指针 */ + transition: background-color 0.3s; /* 背景颜色过渡效果 */ +} + +/* 鼠标悬停时的效果 */ +.meeting-button-cancel:hover { + background-color: #3d3f41af; /* 更深的绿色 */ +} + +.meeting-button-delete{ + position: absolute; + right: 150px; + bottom: 12px; + width: 23%; + height: 40px; + padding: 2px 32px; /* 内边距 */ + margin: 4px 20px; /* 外边距 */ + font-size: 16px; /* 字体大小 */ + color: white; /* 字体颜色 */ + text-align: center; /* 文字居中 */ + text-decoration: none; /* 去掉下划线 */ + background-color: #f80b6e80; /* 背景颜色 */ + border: none; /* 去掉默认边框 */ + border-radius: 12px; /* 圆角半径 */ + cursor: pointer; /* 鼠标指针 */ + transition: background-color 0.3s; /* 背景颜色过渡效果 */ +} + +/* 鼠标悬停时的效果 */ +.meeting-button-delete:hover { + background-color: #e9072dd3; /* 更深的绿色 */ +} + +.meeting-button-primary{ + position: absolute; + right: 0; + bottom: 12px; + width: 23%; + height: 40px; + padding: 2px 32px; /* 内边距 */ + margin: 4px 20px; /* 外边距 */ + font-size: 16px; /* 字体大小 */ + color: white; /* 字体颜色 */ + text-align: center; /* 文字居中 */ + text-decoration: none; /* 去掉下划线 */ + background-color: #0b7af880; /* 背景颜色 */ + border: none; /* 去掉默认边框 */ + border-radius: 12px; /* 圆角半径 */ + cursor: pointer; /* 鼠标指针 */ + transition: background-color 0.3s; /* 背景颜色过渡效果 */ +} + +/* 鼠标悬停时的效果 */ +.meeting-button-primary:hover { + background-color: #0770e9d3; /* 更深的绿色 */ +} + + + +.modal { + position: absolute; + inset: 50% auto auto 50%; + padding: 20px; + margin-right: -50%; + background: white; + border: 1px solid #ccc; + border-radius: 4px; + outline: none; + transform: translate(-50%, -50%); +} + +.modal-overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.75); +} + +.meeting-room-schedule { + cursor: pointer; /* 鼠标指针 */ + user-select: none; /* 禁用文本选择 */ + + + .week-top{ + display: flex; + height: 110px; + + + button.rounded-button1 { + top: 60px; + right: 20%; + } + + button.rounded-button2 { + top: 60px; + right: 13%; + } + + button.rounded-button3 { + top: 60px; + right: 6%; + } + + .week-controls-top { + display: flex; + align-items: center; /* 垂直居中 */ + justify-content: center; /* 水平居中 */ + width: 100%; + + .data-view-icon { + top: 26px; + right: 50%; + font-size: 26px; + text-align: center; + } + } + + .week-controls { + display: flex; + align-items: center; /* 垂直居中 */ + justify-content: center; /* 水平居中 */ + width: 30%; + margin-right: -2%; + + .rounded-button1, + .rounded-button2, + .rounded-button3 { + padding: 10px 20px; + margin-left: 10%; + color: white; + background-color: #007bff; + border: none; + border-radius: 5px; + cursor: pointer; + + &:hover { + background-color: #0056b3; + } + } + } + } + + .schedule-grid { + position: relative; + display: flex; + flex-direction: row; + margin-top: -20px; + + .day-column { + position: relative; + flex: 1; + border-left: 1px solid #ccc; + + &:first-child { + border-left: none; + } + + .day-header { + padding: 10px; + font-size: 14px; + font-weight: bold; + text-align: center; + background-color: #f5f5f5; + border-bottom: 1px solid #ccc; + } + + .time-slot { + position: relative; + height: 26px; + border-bottom: 1px solid #eee; + + &.past-time-slot { + background-color: #f5f5f5; + } + + &.selected-time-slot { + background-color: #d3e0ff; + } + + .meeting-room-label { + position: absolute; + top: 50%; + left: 10px; + font-size: 12px; + transform: translateY(-50%); + } + } + } + } + + .history-meeting { + position: absolute; + padding: 5px; + color: white; + background-color: rgba(0, 123, 255, 0.8); + border-left: 4px solid #007bff; + cursor: pointer; + + &.past-meeting { + margin: 2px; + background-color: rgba(108, 117, 125, 8); + border-left-color: #6c757d; + } + + .meeting-info { + .meeting-subject { + font-weight: bold; + } + + .meeting-time { + font-size: 12px; + } + } + } + + .modal-content { + display: flex; + flex-direction: column; + margin-bottom: 66px; + + .meeting-button-cancel { + position: absolute; + right: 0; + bottom: 12px; + width: 23%; + color: white; + background-color: #6c757d; + + &:hover { + background-color: #5a6268; + } + } + + .meeting-button-delete { + position: absolute; + right: 0; + bottom: 12px; + width: 23%; + color: white; + background-color: #dc3545; + + &:hover { + background-color: #c82333; + } + } + + .meeting-button-primary { + position: absolute; + right: 0; + bottom: 12px; + width: 23%; + color: white; + background-color: #007bff; + + &:hover { + background-color: #0069d9; + } + } + } +} + diff --git a/apps/web/src/plugin/components/meeting-room/view/style.less b/apps/web/src/plugin/components/meeting-room/view/style.less new file mode 100644 index 00000000..ed79389d --- /dev/null +++ b/apps/web/src/plugin/components/meeting-room/view/style.less @@ -0,0 +1,65 @@ +.meeting-data-view_icon { + height: 50px; + padding: 12px; + margin-right: 16px; + background-color: var(--gray-color-1); + border-radius: 4px; +} + +.meeting-data-view_icon svg { + display: block; + height: 100%; +} + +.data-view { + display: inline-grid; + align-items: center; + width: 100%; + min-width: 100%; + height: 100%; + padding: 1% 3%; + overflow: hidden; + background: var(--main-background); + + &-preview { + flex: none; + width: 312px; + min-width: auto; + height: 96px; + } + + &__icon { + padding: @padding-sm; + margin-right: @margin-md; + background-color: var(--gray-color-1); + border-radius: @border-radius-base; + + svg { + display: block; + height: 100%; + } + } + + &-text { + flex: 1; + } + + &__title { + margin-bottom: @margin-xxs; + font-size: @font-size-lg; + font-weight: @font-weight-bold; + line-height: 1; + color: var(--text-color-base); + + .ms-tooltip-cont { + display: inline-block; + .text-size(@font-size-sm); + } + } + + &__content { + display: inline-block; + color: var(--text-color-secondary); + .text-size(@font-size-xxl); + } +} diff --git a/apps/web/src/plugin/constant.ts b/apps/web/src/plugin/constant.ts index eee1dec4..739bb23a 100644 --- a/apps/web/src/plugin/constant.ts +++ b/apps/web/src/plugin/constant.ts @@ -12,6 +12,10 @@ export const COMPONENTCLASS = { name: 'dashboard.plugin_class_data_card', value: 'data_card', }, + meeting_room: { + name: 'Meeting Room', + value: 'meeting_room', + }, other: { name: 'dashboard.plugin_class_other', value: 'other', diff --git a/apps/web/src/services/http/index.ts b/apps/web/src/services/http/index.ts index f779281c..9780bad8 100644 --- a/apps/web/src/services/http/index.ts +++ b/apps/web/src/services/http/index.ts @@ -1,6 +1,7 @@ export { isRequestSuccess, getResponseData, awaitWrap, API_PREFIX } from './client'; export { default as deviceAPI, type DeviceDetail, type DeviceAPISchema } from './device'; +export { default as meetingAPI, type MeetingDetail, type MeetingAPISchema } from './meeting'; export { default as entityAPI, type EntityAPISchema } from './entity'; export { default as integrationAPI, type IntegrationAPISchema } from './integration'; diff --git a/apps/web/src/services/http/meeting.ts b/apps/web/src/services/http/meeting.ts new file mode 100644 index 00000000..553fccb1 --- /dev/null +++ b/apps/web/src/services/http/meeting.ts @@ -0,0 +1,62 @@ +import { client, attachAPI, API_PREFIX } from './client'; + +/** + * 会议详情定义 + */ +export interface MeetingDetail { + /** ID */ + id: ApiKey; + /** model */ + model: string; + /** 类型 */ + type: string; + /** 输入 */ + input: string; + /** 额外数据(通常为后端使用,前端暂不开放) */ + // additional_data?: Record; +} + +/** + * 会议相关接口定义 + */ +export interface MeetingAPISchema extends APISchema { + /** 保存会议 */ + addMeeting: { + request: { + id?: string; + /** 预约名称 */ + subject?: string; + /** 会议室id */ + meeting?: string; + key?: string; + /** 开始时间 */ + first?: string; + /** 结束时间 */ + last?: string; + time?: string; + date?: string; + /** 集成新增会议需要的额外信息 */ + param_entities: Record; + }; + response: unknown; + }; + + + /** 删除会议 */ + deleteMeeting: { + request: { + meeting_id: string; + }; + response: unknown; + }; +} + +/** + * 会议相关 API 服务 + */ +export default attachAPI(client, { + apis: { + addMeeting: `POST ${API_PREFIX}/meeting`, + deleteMeeting: `POST ${API_PREFIX}/meeting/delete`, + }, +});