解决Vue(Nuxt)项目中FullCalendar(v5)跨周期事件绑定Bootstrap Vue Popover的唯一ID冲突问题
这个问题我之前也遇到过!核心原因是FullCalendar对于跨多个视图块(比如月视图里跨几周的事件)会多次调用eventContent方法,每个调用对应一个独立的渲染块,但事件本身的id是相同的,导致你生成的DOM元素ID重复。而Bootstrap Vue的b-popover是通过ID绑定目标元素的,同一个ID只会关联到第一个匹配的元素,所以后面的事件块就无法触发弹窗了。
下面提供两个可行的解决方案,推荐第二种更简洁的实现方式:
方案一:生成唯一ID并动态渲染对应Popover组件
这个方案需要给每个渲染块生成唯一ID,同时跟踪所有渲染的事件块来动态生成b-popover:
修改
eventContent生成唯一ID
利用eventContent参数里的renderRange属性(当前渲染块的时间范围),结合事件ID生成唯一标识:eventContent(args) { if (args.event.extendedProps) { const props = args.event.extendedProps const startTime = new Date(args.event.start) const startHours = startTime.getHours().toString().padStart(2, '0') const startMinutes = startTime.getMinutes().toString().padStart(2, '0') const endTime = new Date(args.event.end) const endHours = endTime.getHours().toString().padStart(2, '0') const endMinutes = endTime.getMinutes().toString().padStart(2, '0') // 用事件ID + 渲染块开始时间戳生成唯一ID const uniqueItemId = `item_${args.event.id}_${args.renderRange.start.valueOf()}` // 同时把唯一ID和事件数据存到一个新数组里 const existingItem = this.renderedEventBlocks.find(block => block.id === uniqueItemId) if (!existingItem) { this.renderedEventBlocks.push({ id: uniqueItemId, eventData: args.event.extendedProps }) } let titleHtml = document.createElement('div') if (props.title) { titleHtml.innerHTML = `<p class="calendar-custom-text mb-0" id="${uniqueItemId}"><span class="calendar-item-type-dot" style="background-color: ${props.borderColor}"></span> <span class="calendar-item-time">${startHours}:${startMinutes}</span> <span class="calendar-item-title font-weight-bold">${props.title}</span></p>` } else { titleHtml.innerHTML = `<p class="calendar-custom-text mb-0" id="${uniqueItemId}"><span class="calendar-item-type-dot" style="background-color: ${props.borderColor}"></span> <span class="calendar-item-time">${startHours}:${startMinutes} - ${endHours}:${endMinutes}</span></p>` } return { domNodes: [titleHtml] } } }在Vue组件中维护渲染块数组并渲染Popover
在data里添加renderedEventBlocks: [],然后在视图切换时清空数组(避免重复):<template> <div> <FullCalendar v-if="mounted" :options="calendarOptions" /> <b-popover v-for="block of renderedEventBlocks" :key="block.id" :target="block.id" triggers="hover" container="page-container" placement="bottomleft" > <calendar-item-popover-content :calendar-item="block.eventData" /> </b-popover> </div> </template> <script> export default { data() { return { mounted: false, renderedEventBlocks: [], calendarOptions: { // 其他配置... viewDidMount: () => { this.renderedEventBlocks = [] }, viewDidUpdate: () => { this.renderedEventBlocks = [] } } } } } </script>
方案二:直接使用Bootstrap原生Popover初始化(推荐)
放弃使用Bootstrap Vue的b-popover组件,改用Bootstrap原生JS在eventContent里直接为每个渲染块初始化Popover,这样每个元素都能独立绑定弹窗:
修改
eventContent方法eventContent(args) { if (args.event.extendedProps) { const props = args.event.extendedProps const startTime = new Date(args.event.start) const startHours = startTime.getHours().toString().padStart(2, '0') const startMinutes = startTime.getMinutes().toString().padStart(2, '0') const endTime = new Date(args.event.end) const endHours = endTime.getHours().toString().padStart(2, '0') const endMinutes = endTime.getMinutes().toString().padStart(2, '0') // 生成唯一ID const uniqueId = `item_${args.event.id}_${args.renderRange.start.valueOf()}` const titleHtml = document.createElement('div') titleHtml.id = uniqueId titleHtml.className = 'calendar-custom-text mb-0' // 构造事件内容HTML let innerHtml = `<span class="calendar-item-type-dot" style="background-color: ${props.borderColor}"></span>` if (props.title) { innerHtml += `<span class="calendar-item-time">${startHours}:${startMinutes}</span> <span class="calendar-item-title font-weight-bold">${props.title}</span>` } else { innerHtml += `<span class="calendar-item-time">${startHours}:${startMinutes} - ${endHours}:${endMinutes}</span>` } titleHtml.innerHTML = innerHtml // 初始化Bootstrap原生Popover new bootstrap.Popover(titleHtml, { trigger: 'hover', placement: 'bottomleft', container: 'page-container', html: true, // 复用你的弹窗内容组件:通过Vue实例生成HTML content: () => { const popoverContainer = document.createElement('div') // 这里需要确保CalendarItemPopoverContent组件已导入 createApp(CalendarItemPopoverContent, { calendarItem: props }).mount(popoverContainer) return popoverContainer.innerHTML } }) return { domNodes: [titleHtml] } } }清理Popover实例(避免内存泄漏)
在组件销毁前,手动销毁所有Popover实例:beforeUnmount() { document.querySelectorAll('.calendar-custom-text').forEach(el => { const popoverInstance = bootstrap.Popover.getInstance(el) if (popoverInstance) { popoverInstance.dispose() } }) }移除模板中的
b-popover循环
现在不需要提前渲染Popover组件了,直接移除原来的v-for循环即可。
这个方案的优势是不需要维护额外的数组,每个渲染块独立初始化弹窗,完美解决ID重复的问题。
内容的提问来源于stack exchange,提问作者Dennis

