feat(core): 增强核心事件通讯总线,增强跨组件交互能力

This commit is contained in:
2025-10-30 16:57:33 +08:00
parent 66bf2504e0
commit 0fabacd71c
8 changed files with 1191 additions and 750 deletions

View File

@@ -5,6 +5,7 @@ let systemInfo = uni.getSystemInfoSync();
export default { export default {
data() { data() {
return { return {
// 自定义页面的数据,数据来源于后台配置
diyData: { diyData: {
global: { global: {
title: '', title: '',

View File

@@ -0,0 +1,133 @@
/**
* Uniapp DOM 事件桥接类,用于将 DOM 事件桥接到 EventBus
* 支持将 DOM 事件转换为 EventBus 事件,可配置是否阻止默认行为和停止传播
*
* @author ZFSun <lauer3912@gmail.com>
* @version 1.0.0
* @createTime 2023-08-10
*/
class DomEventBridge {
constructor() {
this.eventBus = null
this.domListeners = new Map()
}
// 设置 EventBus 实例
setEventBus(eventBus) {
this.eventBus = eventBus
}
// 桥接 DOM 事件到 EventBus
bridgeDomEvent(domEventName, eventBusEventName, options = {}) {
const handler = (domEvent) => {
if (!this.eventBus) return
// 创建 EventBus 兼容的事件对象
const eventBusEvent = this.createEventBusEvent(domEvent, eventBusEventName)
// 触发 EventBus 事件
const shouldContinue = this.eventBus.emit(eventBusEventName, eventBusEvent)
// 根据 EventBus 处理结果决定是否阻止 DOM 事件默认行为
if (!shouldContinue && options.preventDefault) {
domEvent.preventDefault()
}
// 根据 EventBus 处理结果决定是否停止 DOM 事件传播
if (!shouldContinue && options.stopPropagation) {
domEvent.stopPropagation()
}
}
// 保存监听器引用以便清理
const listenerKey = `${domEventName}-${eventBusEventName}`
this.domListeners.set(listenerKey, handler)
// 添加 DOM 事件监听
// #ifdef H5
document.addEventListener(domEventName, handler, options)
// #endif
return () => this.unbridgeDomEvent(domEventName, eventBusEventName)
}
// 创建 EventBus 兼容的事件对象
createEventBusEvent(domEvent, eventBusEventName) {
return {
type: eventBusEventName,
domEvent: domEvent,
originalEvent: domEvent,
target: domEvent.target,
currentTarget: domEvent.currentTarget,
timestamp: Date.now(),
// EventBus 的取消方法
preventDefault: function () {
this._preventDefault = true
},
stopPropagation: function () {
this._stopPropagation = true
},
// DOM 事件的代理方法
stopImmediatePropagation: function () {
domEvent.stopImmediatePropagation()
},
get isDefaultPrevented() {
return this._preventDefault || false
},
get isPropagationStopped() {
return this._stopPropagation || false
}
}
}
// 取消桥接
unbridgeDomEvent(domEventName, eventBusEventName) {
const listenerKey = `${domEventName}-${eventBusEventName}`
const handler = this.domListeners.get(listenerKey)
if (handler) {
// #ifdef H5
document.removeEventListener(domEventName, handler)
// #endif
this.domListeners.delete(listenerKey)
}
}
// 从 EventBus 触发 DOM 事件
triggerDomEventFromEventBus(domEventName, detail = {}) {
// #ifdef H5
const event = new CustomEvent(domEventName, {
bubbles: true,
cancelable: true,
detail: detail
})
document.dispatchEvent(event)
return !event.defaultPrevented
// #endif
// #ifdef MP - WEIXIN
// 小程序环境不支持 DOM 事件
return true
// #endif
}
// 清理所有桥接
destroy() {
for (const [listenerKey, handler] of this.domListeners) {
const [domEventName] = listenerKey.split('-')
// #ifdef H5
document.removeEventListener(domEventName, handler)
// #endif
}
this.domListeners.clear()
}
}
export default new DomEventBridge()

260
common/js/event-bus.js Normal file
View File

@@ -0,0 +1,260 @@
/**
* Uniapp 事件总线类,用于在应用内进行事件通信
* 支持同步和异步事件触发,可配置是否继续传播事件
* 可配置是否在任意监听器抛出错误后立即停止传播
* 可配置是否在每个监听器执行后调用 defaultAsyncHandler
* 如果 allowAsyncHandlerRun 为 true在每个监听器执行后调用 defaultAsyncHandler
* 如果 allowAsyncHandlerRun 为 false不调用 defaultAsyncHandler
* 注意:如果 allowAsyncHandlerRun 为 falsedefaultAsyncHandler 不会被调用,也不会等待其 resolve
*
* @author ZFSun <lauer3912@gmail.com>
* @version 1.0.0
* @createTime 2023-08-10
*/
class EventBus {
constructor() {
this.events = new Map()
this.domBridge = null
this.platform = this.detectPlatform()
}
// 检测平台
detectPlatform() {
// #ifdef H5
return 'h5'
// #endif
// #ifdef MP-WEIXIN
return 'weapp'
// #endif
return 'unknown'
}
// 设置 DOM 桥接
setDomBridge(bridge) {
this.domBridge = bridge
bridge.setEventBus(this)
}
// 监听事件(自动处理平台差异),支持取消
on(eventName, callback, options = { priority: 0 }) {
// 如果是 H5 平台且事件名是 DOM 事件,自动创建桥接
if (this.platform === 'h5' && this.isDomEvent(eventName) && this.domBridge) {
return this.domBridge.bridgeDomEvent(
this.getDomEventName(eventName),
eventName,
options
)
}
// 普通 EventBus 监听
if (!this.events.has(eventName)) {
this.events.set(eventName, new Set())
}
const handler = { callback, options }
this.events.get(eventName).add(handler)
// 返回取消函数
return () => this.off(eventName, handler)
}
// 监听多个事件,支持取消
onMany(eventNames = [], callback, options = { priority: 0 }) {
// 支持字符串数组或空格分隔的字符串,自动转换为数组
if (typeof eventNames === 'string') {
// 支持使用空格分隔事件名
// 支持使用逗号分隔事件名
// 下面代码使用正则表达式,自动处理逗号和空格分隔的字符串
eventNames = eventNames.replace(/\s+/g, ',').split(',')
}
const removeFns = eventNames.map(eventName => this.on(eventName, callback, options))
return () => removeFns.forEach(fn => fn())
}
// 异步触发事件,支持取消传播,并支持在监听处理后调用异步函数 defaultAsyncHandler
// 使用方式await eventBus.emit(eventName, data, defaultAsyncHandler)
// defaultAsyncHandler 会在每个监听器执行后被调用,签名为 defaultAsyncHandler(event, handler, handlerResult)
// 如果 defaultAsyncHandler 返回 Promise会等待其 resolve
// 如果 allowContinuePropagation 为 false事件触发后将立即返回不继续传播
// 如果 stopOnError 为 true在任意监听器抛出错误后立即停止传播
// 如果 allowAsyncHandlerRun 为 true在每个监听器执行后调用 defaultAsyncHandler
// 如果 allowAsyncHandlerRun 为 false不调用 defaultAsyncHandler
// 注意:如果 allowAsyncHandlerRun 为 falsedefaultAsyncHandler 不会被调用,也不会等待其 resolve
async emit(eventName, data, defaultAsyncHandler, { allowContinuePropagation = true, stopOnError = false, allowAsyncHandlerRun = true } = {}) {
// 先创建事件对象,方便传入 defaultAsyncHandler
const event = this.createEvent(eventName, data)
// helper to call defaultAsyncHandler only when allowed
const callDefaultAsyncHandler = async (evt, handler, handlerResult) => {
if (typeof defaultAsyncHandler !== 'function' || !allowAsyncHandlerRun) return
try {
const res = defaultAsyncHandler(evt, handler, handlerResult)
if (res instanceof Promise) {
const awaited = await res
if (awaited === false) evt.preventDefault()
} else {
if (res === false) evt.preventDefault()
}
} catch (err) {
console.error(`EventBus defaultAsyncHandler error for ${eventName}:`, err)
}
}
// 如果是 H5 平台且需要触发 DOM 事件
if (this.platform === 'h5' && data && data.triggerDomEvent) {
const domResult = this.domBridge.triggerDomEventFromEventBus(
this.getDomEventName(eventName),
data.detail
)
// 调用 defaultAsyncHandler仅当允许
await callDefaultAsyncHandler(event, null, domResult)
// 如果不允许继续传播,则直接返回(根据 defaultPrevented 判断结果)
if (!allowContinuePropagation) {
return !event.defaultPrevented
}
// 否则继续走普通 EventBus 处理fallthrough
}
// 普通 EventBus 触发
const handlers = this.events.get(eventName)
// 如果没有监听器,执行 defaultAsyncHandler如果有并返回结果
if (!handlers) {
await callDefaultAsyncHandler(event, null, true)
return !event.defaultPrevented
}
// 如果不允许继续传播,则不执行任何监听器,只调用一次 defaultAsyncHandler若提供然后返回
if (!allowContinuePropagation) {
await callDefaultAsyncHandler(event, null, true)
return !event.defaultPrevented
}
// 按优先级排序执行
const sortedHandlers = Array.from(handlers).sort((a, b) => {
return (b.options?.priority || 0) - (a.options?.priority || 0)
})
// 按优先级顺序执行每个监听器
for (const handler of sortedHandlers) {
if (event.defaultPrevented) break
try {
// 支持监听器返回 Promise 或同步值
const result = handler.callback(event)
const awaitedResult = result instanceof Promise ? await result : result
// 如果回调返回 false也停止默认行为
if (awaitedResult === false) {
event.preventDefault()
}
// 在每个监听器执行后,如果允许并且提供了 defaultAsyncHandler就调用并等待它
await callDefaultAsyncHandler(event, handler, awaitedResult)
} catch (error) {
console.error(`EventBus ${eventName} error:`, error)
// 如果全局或该 handler 指定遇错停止传播,则停止
if (stopOnError || handler.options?.stopOnError) {
break
}
// 否则继续下一个 handler
}
// 一次性监听自动移除
if (handler.options?.once) {
this.off(eventName, handler)
}
}
return !event.defaultPrevented
}
// 移除监听
off(eventName, handler) {
const handlers = this.events.get(eventName)
if (handlers) {
handlers.delete(handler)
if (handlers.size === 0) {
this.events.delete(eventName)
}
}
}
// 移除多个事件监听
offMany(eventNames, handler) {
eventNames.forEach(eventName => this.off(eventName, handler))
}
// 移除所有监听
offAll(eventName) {
this.events.delete(eventName)
}
// 一次性监听
once(eventName, callback, options = {}) {
return this.on(eventName, callback, { ...options, once: true })
}
// 前置监听(高优先级)
preOn(eventName, callback) {
return this.on(eventName, callback, { priority: 100 })
}
// 后置监听(低优先级)
postOn(eventName, callback) {
return this.on(eventName, callback, { priority: -100 })
}
// 判断是否是 DOM 事件
isDomEvent(eventName) {
const domEvents = ['click', 'touchstart', 'touchend', 'mousedown', 'mouseup']
return domEvents.includes(eventName.toLowerCase())
}
// 获取对应的 DOM 事件名
getDomEventName(eventName) {
const map = {
'tap': 'click',
'click': 'click',
'touchstart': 'touchstart',
'touchend': 'touchend'
}
return map[eventName] || eventName
}
// 创建可取消的事件对象
createEvent(eventName, data) {
const event = {
type: eventName,
data,
timestamp: Date.now(),
defaultPrevented: false,
_isPropagationStopped: false
}
// 阻止事件继续传播
event.preventDefault = function () {
this.defaultPrevented = true
}
// 停止传播
event.stopPropagation = function () {
this._isPropagationStopped = true
}
event.isPropagationStopped = function () {
return this._isPropagationStopped
}
return event
}
}
export default new EventBus()

View File

@@ -2,8 +2,8 @@
<view class="diy-picture" :style="style"> <view class="diy-picture" :style="style">
<view class="fui-picture"> <view class="fui-picture">
<view v-for="(item,index) in value.list" style="line-height: 0;"> <view v-for="(item,index) in value.list" style="line-height: 0;">
<image mode="widthFix" style="width: 100%;height:auto" :src="$util.img(item.imageUrl)" v-if="item.link.wap_url" @click="redirectTo(item.link)"></image> <image mode="widthFix" style="width: 100%;height:auto" :src="$util.img(item.imageUrl)" v-if="item.link.wap_url" @click="handlerClick(item)" @tap="handlerClick(item)"></image>
<image mode="widthFix" style="width: 100%;height:auto" :src="$util.img(item.imageUrl)" v-else @click="previewImg(item.imageUrl)"></image> <image mode="widthFix" style="width: 100%;height:auto" :src="$util.img(item.imageUrl)" v-else @click="handlerClick(item)" @tap="handlerClick(item)"></image>
</view> </view>
<!-- <view wx:if="{{!childitem.linkurl}}" bindtap="previewImg" data-src="{{childitem.imgurl}}" style="padding:{{diyitem.style.paddingtop==0?0:diyitem.style.paddingtop+'rpx'}} {{diyitem.style.paddingleft==0?0:diyitem.style.paddingleft+'rpx'}}" wx:for="{{diyitem.data}}" wx:for-index="childid" wx:for-item="childitem" wx:key="{{childid}}"> <!-- <view wx:if="{{!childitem.linkurl}}" bindtap="previewImg" data-src="{{childitem.imgurl}}" style="padding:{{diyitem.style.paddingtop==0?0:diyitem.style.paddingtop+'rpx'}} {{diyitem.style.paddingleft==0?0:diyitem.style.paddingleft+'rpx'}}" wx:for="{{diyitem.data}}" wx:for-index="childid" wx:for-item="childitem" wx:key="{{childid}}">
<image mode="widthFix" src="{{childitem.imgurl}}" style="{{bannerheight?'height:'+bannerheight+'px':'height:auto'}}"></image> <image mode="widthFix" src="{{childitem.imgurl}}" style="{{bannerheight?'height:'+bannerheight+'px':'height:auto'}}"></image>
@@ -13,6 +13,7 @@
</template> </template>
<script> <script>
import minx from './minx.js'
export default { export default {
name: 'diy-picture', name: 'diy-picture',
props: { props: {
@@ -34,6 +35,7 @@
// this.getDataList(); // this.getDataList();
} }
}, },
mixins: [minx],
computed: { computed: {
style() { style() {
var css = ''; var css = '';
@@ -68,6 +70,17 @@
} }
this.$util.diyRedirectTo(link); this.$util.diyRedirectTo(link);
}, },
async handlerClick(item) {
await this.__$emitEvent({eventName: 'pictureTap', data: item, promiseCallback: (event, handler, awaitedResult) => {
if (!awaitedResult) return;
if (item.link.wap_url) {
this.redirectTo(item.link);
} else {
this.previewImg(item.imageUrl);
}
}})
}
} }
}; };
</script> </script>

View File

@@ -0,0 +1,8 @@
export default {
methods: {
// 异步触发事件
async __$emitEvent(payload = {eventName: '__unnamedEvent', data: {}, promiseCallback: null}) {
await this.$eventBus?.emit(payload.eventName, payload.data, payload.promiseCallback)
},
}
}

View File

@@ -8,6 +8,8 @@ import Util from './common/js/util.js'
import Http from './common/js/http.js' import Http from './common/js/http.js'
import Lang from './common/js/lang.js' import Lang from './common/js/lang.js'
import Config from './common/js/config.js' import Config from './common/js/config.js'
import EventBus from './common/js/event-bus.js'
import DomEventBridge from './common/js/dom-event-bridge.js'
import globalConfig from './common/js/golbalConfig.js'; import globalConfig from './common/js/golbalConfig.js';
import { import {
uniStorage uniStorage
@@ -25,6 +27,12 @@ Vue.prototype.$lang = Lang.lang; //解析语言包
Vue.prototype.$config = Config; Vue.prototype.$config = Config;
// #ifdef H5
EventBus.setDomBridge(DomEventBridge)
// #endif
Vue.prototype.$eventBus = EventBus;
Vue.mixin(globalConfig); Vue.mixin(globalConfig);
App.mpType = 'app'; App.mpType = 'app';

View File

@@ -127,6 +127,7 @@
<privacy-popup ref="privacyPopup"></privacy-popup> <privacy-popup ref="privacyPopup"></privacy-popup>
<!-- #endif --> <!-- #endif -->
<to-top v-if="showTop" @toTop="scrollToTopNative()"></to-top> <to-top v-if="showTop" @toTop="scrollToTopNative()"></to-top>
<ns-login ref="login"></ns-login>
</view> </view>
</template> </template>
@@ -140,17 +141,12 @@
export default { export default {
data() {
return {
minScrollTop: 100, // 设置回到顶端按钮显示要求,最小滚动距离
}
},
components: { components: {
uniPopup, uniPopup,
nsNavbar, nsNavbar,
toTop toTop
}, },
mixins: [diyJs, indexJs, scroll] mixins: [diyJs, scroll, indexJs]
}; };
</script> </script>

View File

@@ -1,6 +1,7 @@
export default { export default {
data() { data() {
return { return {
minScrollTop: 100, // 设置回到顶端按钮显示要求,最小页面滚动距离
wechatQrcode: '', // 公众号二维码 wechatQrcode: '', // 公众号二维码
diyRoute: '/pages/index/index' diyRoute: '/pages/index/index'
}; };
@@ -16,8 +17,17 @@ export default {
}, },
onShow() { onShow() {
// 获取关注公众号二维码
this.getFollowQrcode(); this.getFollowQrcode();
}, },
mounted() {
// 监听图片点击事件
this.unsubscribe = this.$eventBus?.on('pictureTap', this._handleDiyGroupInteractionEvent);
},
beforeDestroy() {
// 移除图片点击事件监听
if (this.unsubscribe) this.unsubscribe();
},
methods: { methods: {
// 关注公众号 // 关注公众号
getFollowQrcode() { getFollowQrcode() {
@@ -37,5 +47,17 @@ export default {
officialAccountsClose() { officialAccountsClose() {
this.$refs.officialAccountsPopup.close(); this.$refs.officialAccountsPopup.close();
}, },
// 统一处理点击及触摸事件
_handleDiyGroupInteractionEvent(payload) {
if (this.storeToken) {
return;
}
// 打开登录弹窗
this.$refs.login?.open();
// 阻止事件继续传播
return false;
}
} }
} }