根据fullcalendar实现企业微信的拖动式预约会议
先上效果图
这边是根据日历从当天开始,生成7个Calendar实例,并且根据数据数据进行回显,并且禁用当前时间段之前的操作,和在拖动后进行鼠标附近的modal展示,并且按照第几周来进行筛选和获取数据,直接上关键代码⬇️
import { Calendar } from '@fullcalendar/core';
import resourceTimelinePlugin from '@fullcalendar/resource-timeline';
import interactionPlugin from '@fullcalendar/interaction';
import ProFormDatePickerWeek from "@ant-design/pro-form/es/components/DatePicker/WeekPicker"
import dayjs from "dayjs"const [modalPosition, setModalPosition] = useState({ top: 0, left: 0 });const [modalVisible, setModalVisible] = useState(false);const [filterTimeValue,setFilterTimeValue]=useState(dayjs())const [currentWeek, setCurrentWeek] = useState(dayjs().weekday(1));const [condition, setCondition] = useState([{condition: {field: MeetConfig.f_1744783951543,op: "between",value:[dayjs().startOf('week').valueOf(),dayjs().endOf('week').valueOf()],},nextop: "and",},{condition: {field: MeetConfig.f_1744783955010,op: "between",value:[dayjs().startOf('week').valueOf(),dayjs().endOf('week').valueOf()],},nextop: "and",},])const modalRef =useRef()
const getWeekDays = () => {// 确保始终从周一开始计算let startOfWeek = currentWeek;if(startOfWeek.day()!==1){startOfWeek= startOfWeek.day(1)}console.log(startOfWeek.format('YYYY-MM-DD'),startOfWeek.day(),'currentWeek');// 验证是否为周一// console.assert(startOfWeek.day() === 1, 'Start day should be Monday');const days = [];for (let i = 0; i < 7; i++) {// if (isWorkday(startOfWeek.add(i, 'day'))) {console.log(startOfWeek.add(i, 'day'),'startOfWeek.add');days.push(startOfWeek.add(i, 'day'));// }}return days;
};
// 导航函数
const prevWeek = () => {const weekStart = dayjs(currentWeek.subtract(1, 'week')).startOf('week'); // 强制转为周一const weekEnd = weekStart.add(6, 'day').endOf('day');let submitCondition = condition.filter((item)=>item.condition.field!==MeetConfig.f_1744783951543&&item.condition.field!==MeetConfig.f_1744783955010).concat({condition: {field: MeetConfig.f_1744783951543,op: "between",value: [weekStart.valueOf(), weekEnd.valueOf()]},nextop: "and",},).concat({condition: {field: MeetConfig.f_1744783955010,op: "between",value: [weekStart.valueOf(), weekEnd.valueOf()]},nextop: "and",},).filter((item2) => !isEmptyValues(item2?.condition?.value))setCondition(submitCondition)setFilterTimeValue(currentWeek.subtract(1, 'week'))setCurrentWeek(prev => prev.subtract(1, 'week'));
};const nextWeek = () => {console.log(dayjs(currentWeek.add(1, 'week')).format('YYYY-MM-DD'),'lkkl');const weekStart = dayjs(currentWeek.add(1, 'week')).startOf('week'); // 强制转为周一const weekEnd = weekStart.add(6, 'day').endOf('day');let submitCondition = condition.filter((item)=>item.condition.field!==MeetConfig.f_1744783951543&&item.condition.field!==MeetConfig.f_1744783955010).concat({condition: {field: MeetConfig.f_1744783951543,op: "between",value: [weekStart.valueOf(), weekEnd.valueOf()]},nextop: "and",},).concat({condition: {field: MeetConfig.f_1744783955010,op: "between",value: [weekStart.valueOf(), weekEnd.valueOf()]},nextop: "and",},).filter((item2) => !isEmptyValues(item2?.condition?.value))setCondition(submitCondition)setFilterTimeValue(currentWeek.add(1, 'week'))setCurrentWeek(prev => prev.add(1, 'week'));
};const goToToday = () => {const weekStart = dayjs().startOf('week'); // 强制转为周一const weekEnd = weekStart.add(6, 'day').endOf('day');let submitCondition = condition.filter((item)=>item.condition.field!==MeetConfig.f_1744783951543&&item.condition.field!==MeetConfig.f_1744783955010).concat({condition: {field: MeetConfig.f_1744783951543,op: "between",value: [weekStart.valueOf(), weekEnd.valueOf()]},nextop: "and",},).concat({condition: {field: MeetConfig.f_1744783955010,op: "between",value: [weekStart.valueOf(), weekEnd.valueOf()]},nextop: "and",},).filter((item2) => !isEmptyValues(item2?.condition?.value))setCondition(submitCondition)setCurrentWeek(dayjs());setFilterTimeValue(dayjs().startOf('week'));
};useEffect(() => {function handleClickOutside(event) {if (modalRef.current && !modalRef.current.contains(event.target)) {setModalVisible(false)}}document.addEventListener('mousedown', handleClickOutside);return () => {document.removeEventListener('mousedown', handleClickOutside);};
}, []);useEffect(() => {// 清空之前的日历实例calendarApis.current.forEach(api => api && api.destroy());calendarApis.current = [];// 为每一天初始化日历getWeekDays().forEach((day, index) => {if (!calendarRefs.current[index]) return;const calendar = new Calendar(calendarRefs.current[index], {plugins: [resourceTimelinePlugin, interactionPlugin],initialView: 'resourceTimelineDay',initialDate: day.toDate(),headerToolbar: false,resourceAreaWidth: '0',selectAllow: (selectInfo) => {console.log(selectInfo,'selet');const now = new Date(); // 当前时间return selectInfo.start >= now; // 只允许选择当前或未来的时间},slotLabelFormat: {hour: '2-digit',minute: '2-digit',hour12: false},timeline: {slotHeight: 30 // 设置与CSS一致的行高},headerToolbar:false,//设置时间间隔slotDuration: '00:30:00',slotMinTime: '08:00:00',slotMaxTime: '18:00:00',height: 'auto',editable: false,selectable: true,selectMirror: true,resources: resources,events: events.filter(event =>dayjs(event.start).isSame(day, 'day')),select:(selectInfo) => {const newEvent = {id: String(Math.random()),start: selectInfo.start,end: selectInfo.end,resourceId: selectInfo.resource.id};if (hasTimeConflict(newEvent, events)) {message.warning('该时间段已被占用');calendar.unselect();return;}// 计算点击位置(相对于日历容器)const position = {top: selectInfo.jsEvent.clientY - 50,left: selectInfo.jsEvent.clientX>1600? selectInfo.jsEvent.clientX-500:selectInfo.jsEvent.clientX-200};setModalPosition(position);setConfirmInfo(newEvent);setModalVisible(true);},eventClick: (info) => {setRecord({[MeetConfig.f_1744783951543]: dayjs(info.event.start).valueOf(),[MeetConfig.f_1744783955010]: dayjs(info.event.end).valueOf(),[MeetConfig.f_1745485256233]: info.event.title,[MeetConfig.f_1745485270386]: initialState?.currentUser?.name,id: info.event.id});setOpen(true);},dateClick: (selectInfo) => {console.log(selectInfo,22,'kkk');const calendarEl = calendarRef.current;if (dayjs(selectInfo?.date).valueOf()<dayjs().valueOf()) {message.warning('该时间段已禁用')return;}// 计算点击位置(相对于日历容器)const position = {top: selectInfo.jsEvent.clientY - 50,left: selectInfo.jsEvent.clientX>1600? selectInfo.jsEvent.clientX-500:selectInfo.jsEvent.clientX-200};const newEvent = {id: String(Math.random()),start: selectInfo.date,end:dayjs(selectInfo.date).add(30, 'minute'),resourceId: selectInfo.resource.id};setConfirmInfo(newEvent)setModalPosition(position);setModalVisible(true);},});calendar.render();calendarApis.current[index] = calendar;});return () => {calendarApis.current.forEach(api => api && api.destroy());};
}, [events, resources, currentWeek]);// 获取数据const getList = async () => {const res = await getFormData({constant: MeetConfig,filter_cond: { conditions: [...condition,] },page: 1,page_size: 999,})console.log("获取数据", res)if (res.ret === 0) {console.log(res?.msg?.data?.map((e)=>{return {...e,id:e.id,resourceId: 'room',title: e?.[MeetConfig.f_1745485256233],start: new Date(e?.['f_1744783951543']),end: new Date(e?.['f_1744783955010']),backgroundColor: '#3788d8'}}),'123');setEvents(res?.msg?.data?.map((e)=>{return {...e,id:e.id,resourceId: 'room',title: e?.[MeetConfig.f_1745485256233],start: new Date(e?.['f_1744783951543']),end: new Date(e?.['f_1744783955010']),backgroundColor: '#3788d8'}}))}}useEffect(() => {getList()}, [condition])//切换日期和筛选<ProFormtitle={null}layout="horizontal"onReset={() => {setCondition([])}}submitter={{render: (props, doms) => {return []},}}onFinish={(values) => {console.log(values,'123');let submitCondition = condition.filter((e)=>e.condition.field!==MeetConfig.f_1744783951543&&e.condition.field!==MeetConfig.f_1744783955010).concat({condition: {field: MeetConfig.f_1744783951543,op: "between",value:[dayjs( values['time']).startOf('day').valueOf(),dayjs( values['time']).endOf('day').valueOf()],},nextop: "and",},).concat({condition: {field: MeetConfig.f_1744783955010,op: "between",value:[dayjs( values['time']).startOf('day').valueOf(),dayjs( values['time']).endOf('day').valueOf()],},nextop: "and",},).filter((item) => !isEmptyValues(item?.condition?.value))setPage(1)setCondition(submitCondition)}}><ProFormGroup size={8}><ProFormDatePickerWeekname="time"fieldProps={{disabledDate: (current) => current && current < dayjs().startOf('day'),// 显式设置周一开始(兼容不同地区设置)value: filterTimeValue,onChange: (e) => {let date if(e){date = e}else{date = dayjs()}// 统一按周一开始计算(ISO标准周)const weekStart = dayjs(date).startOf('week'); // 强制转为周一const weekEnd = weekStart.add(6, 'day').endOf('day');// 更新状态(保持同步)setCurrentWeek(dayjs(date).startOf('week'));setFilterTimeValue(date.startOf('week'));let submitCondition = condition.filter((item)=>item.condition.field!==MeetConfig.f_1744783951543&&item.condition.field!==MeetConfig.f_1744783955010).concat({condition: {field: MeetConfig.f_1744783951543,op: "between",value: [weekStart.valueOf(), weekEnd.valueOf()]},nextop: "and",},).concat({condition: {field: MeetConfig.f_1744783955010,op: "between",value: [weekStart.valueOf(), weekEnd.valueOf()]},nextop: "and",},).filter((item2) => !isEmptyValues(item2?.condition?.value))setCondition(submitCondition)}}}allowClear// 清除时重置逻辑onClear={() => {const now = dayjs();setCurrentWeek(now.startOf('week').add(1, 'day'));setFilterTimeValue(now);setCondition([]);}}
/><Col><Button.Group><Button icon={<LeftOutlined />} onClick={prevWeek} disabled={filterTimeValue<=dayjs()}/><Button onClick={goToToday}>本周</Button><Button icon={<RightOutlined />} onClick={nextWeek} /></Button.Group></Col><Col></Col></ProFormGroup></ProForm>{getWeekDays().map((day, index) => (<div key={day.format('YYYY-MM-DD')}><div style={{marginBottom:2,marginTop:8,fontSize:14,fontWeight:700,display:'flex'}}> <div style={{marginRight:10}}>{dayjs(day).format('YYYY-MM-DD')}</div><div>{dayjs(day).format('dddd')=='Monday'?'周一':dayjs(day).format('dddd')=='Tuesday'?'周二':dayjs(day).format('dddd')=='Wednesday'?'周三':dayjs(day).format('dddd')=='Thursday'?'周四':dayjs(day).format('dddd')=='Friday'?'周五':dayjs(day).format('dddd')=='Saturday'?'周六':dayjs(day).format('dddd')=='Sunday'?'周日':dayjs(day).format('dddd')||'周日'}</div></div><div ref={el => calendarRefs.current[index] = el}style={{ height: '500px' }}/></div>))}{modalVisible && (<div ref={modalRef}style={{position: 'absolute',top: `${modalPosition.top}px`,left: `${modalPosition.left}px`,zIndex: 1001,background: 'white',padding: '16px',borderRadius: '4px',boxShadow: '0 2px 8px rgba(0,0,0,0.15)',minWidth: '300px'}}>{/* <h4>选择时间段</h4><p>{clickedTime.date.toLocaleString()}</p><p>资源: {clickedTime.resourceId || '无'}</p> */}<div style={{fontSize:20,fontWeight:700}}>会议室申请</div><div style={{display:'flex',fontSize:16,fontWeight:500}}><div style={{marginRight:10}}> {dayjs(confirmInfo?.start).format('YYYY-MM-DD')}</div><div style={{marginRight:10}}> {dayjs(confirmInfo?.start).format('dddd')}</div><div>{dayjs(confirmInfo?.start).format('HH:mm')+'-'+dayjs(confirmInfo?.end).format('HH:mm')}</div></div><div style={{ marginTop: '16px', display: 'flex', gap: '8px'}}><Button type="primary"style={{width:'100%'}}onClick={() => {// 创建新事件逻辑}}>预定</Button>{/* <Button onClick={() => setModalVisible(false)}>取消</Button> */}</div></div>)}
如果想只创建一个实例代码,并且按照天筛选
//修改创建实例代码useEffect(() => {const calendar = new Calendar(calendarRef.current, {....同上}, [events, resources,condition]);<div ref={calendarRef} />