From 57cdec432bda02d299e8dab0a0368a33fbca4f90 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Mon, 27 Jul 2026 14:59:11 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20RN=20runtime=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../references/rn-template-reference.md | 18 +++---- docs-vitepress/guide/rn/component.md | 54 +++++++++++++++---- .../runtime/components/react/mpx-camera.tsx | 4 +- .../components/react/mpx-canvas/Image.ts | 20 +++---- .../components/react/mpx-canvas/index.tsx | 33 ++++++------ .../components/react/mpx-checkbox-group.tsx | 2 +- .../lib/runtime/components/react/mpx-form.tsx | 4 +- .../components/react/mpx-picker/date.tsx | 2 +- .../components/react/mpx-picker/index.tsx | 2 +- .../components/react/mpx-radio-group.tsx | 2 +- .../components/react/mpx-sticky-header.tsx | 2 +- .../runtime/components/react/mpx-swiper.tsx | 17 ++++-- .../runtime/components/react/mpx-video.tsx | 6 +-- 13 files changed, 104 insertions(+), 62 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-template-reference.md b/.agents/skills/mpx2rn/references/rn-template-reference.md index 509787cb90..2f711770f4 100644 --- a/.agents/skills/mpx2rn/references/rn-template-reference.md +++ b/.agents/skills/mpx2rn/references/rn-template-reference.md @@ -822,7 +822,7 @@ Mpx 输出 RN 内置支持了大部分常用的基础组件,详情见下方文 | 事件名 | 说明 | | --- | --- | -| change | current 改变时会触发 change 事件,`event.detail = {current, source}` | +| change | current 改变时会触发 change 事件,`event.detail = {current, source}`;`source` 为 `touch`、`autoplay` 或空字符串,分别表示用户滑动、自动播放或其他原因导致的切换 | ### swiper-item @@ -963,7 +963,7 @@ movable-view 的可移动区域。 | 事件名 | 说明 | | --- | --- | -| change | checkbox-group 中选中项发生改变时触发 change 事件,`detail = { value: [ 选中的 checkbox 的 value 的数组 ] } ` | +| change | checkbox-group 中选中项发生改变时触发 change 事件,`event.type = 'change'`,`event.detail = { value: [选中的 checkbox 的 value 的数组] }` | ### radio @@ -986,7 +986,7 @@ movable-view 的可移动区域。 | 事件名 | 说明 | | --- | --- | -| change | radio-group 中选中项发生改变时触发 change 事件,`detail = { value: [ 选中的 radio 的 value 的数组 ] }` | +| change | radio-group 中选中项发生改变时触发 change 事件,`event.type = 'change'`,`event.detail = { value: 选中的 radio 的 value }` | ### form @@ -999,7 +999,7 @@ movable-view 的可移动区域。 | 事件名 | 说明 | | --- | --- | | submit | 携带 form 中的数据触发 submit 事件,`event.detail = {value : {'name': 'value'} }` | -| reset | 表单重置时会触发 reset 事件 | +| reset | 表单重置时会触发 reset 事件,`event.type = 'reset'` | ### input @@ -1162,7 +1162,7 @@ movable-view 的可移动区域。 | 事件名 | 说明 | | ---------- | ------------------------------------------------------ | -| cancel | 取消选择时触发 | +| cancel | 取消选择时触发 cancel 事件,`event.type = 'cancel'` | | change | value 改变时触发 change 事件,`event.detail = {value}` | #### 普通选择器:mode = selector @@ -1352,7 +1352,7 @@ level 有效值: | 事件名 | 说明 | | --- | --- | | initdone | 相机初始化完成时触发,`event.detail = { maxZoom }` | -| stop | 摄像头在非正常终止时触发 | +| stop | 摄像头在非正常终止时触发 stop 事件,`event.type = 'stop'` | | error | 相机发生错误时触发 | | scancode | 在 `scanCode` 模式下识别到二维码时触发,`event.detail = { result, type, scanArea }` | @@ -1398,13 +1398,13 @@ level 有效值: | --- | --- | | play | 当开始/继续播放时触发 play 事件 | | pause | 当暂停播放时触发 pause 事件 | -| ended | 当播放到末尾时触发 ended 事件 | +| ended | 当播放到末尾时触发 ended 事件,`event.type = 'ended'` | | timeupdate | 播放进度变化时触发,`event.detail = {currentTime, duration}` | | fullscreenchange | 视频进入和退出全屏时触发,`event.detail = {fullScreen` } | | waiting | 视频出现缓冲时触发 | -| error | 视频播放出错时触发 | +| error | 视频播放出错时触发 error 事件,`event.type = 'error'` | | loadedmetadata | 视频元数据加载完成时触发。`event.detail = {width, height, duration}` | -| controlstoggle | 切换 controls 显示隐藏时触发。`event.detail = {show}` | +| controlstoggle | 切换 controls 显示隐藏时触发 controlstoggle 事件,`event.type = 'controlstoggle'`,`event.detail = {show}` | | seekcomplete | seek 完成时触发 | #### 注意事项 diff --git a/docs-vitepress/guide/rn/component.md b/docs-vitepress/guide/rn/component.md index 6a56a3f781..c9c201ed96 100644 --- a/docs-vitepress/guide/rn/component.md +++ b/docs-vitepress/guide/rn/component.md @@ -7,7 +7,7 @@ - #### 基础组件 **容器组件**:[view](#view) · [scroll-view](#scroll-view) · [swiper](#swiper) · [swiper-item](#swiper-item) · [movable-area](#movable-area) · [movable-view](#movable-view) · [root-portal](#root-portal) · [sticky-section](#sticky-section) · [sticky-header](#sticky-header) · [cover-view](#cover-view) -**媒体组件**:[image](#image) · [video](#video) · [canvas](#canvas) +**媒体组件**:[image](#image) · [video](#video) · [canvas](#canvas) · [camera](#camera) **表单组件**:[input](#input) · [textarea](#textarea) · [button](#button) · [checkbox](#checkbox) · [checkbox-group](#checkbox-group) · [radio](#radio) · [radio-group](#radio-group) · [switch](#switch) · [picker](#picker) · [picker-view](#picker-view) · [picker-view-column](#picker-view-column) · [form](#form) · [label](#label) @@ -181,7 +181,7 @@ | 事件名 | 说明 | | ----------------| ------------------ | -| change| current 改变时会触发 change 事件,`event.detail = {current, source}`| +| change| current 改变时会触发 change 事件,`event.detail = {current, source}`;`source` 为 `touch`、`autoplay` 或空字符串,分别表示用户滑动、自动播放或其他原因导致的切换 | ### swiper-item 仅可放置在swiper组件中,宽高自动设置为100%。 @@ -320,7 +320,7 @@ movable-view的可移动区域。 | 事件名 | 说明 | | ----------------| ------------------ | -| change | checkbox-group 中选中项发生改变时触发 change 事件,`detail = { value: [ 选中的 checkbox 的 value 的数组 ] } `| +| change | checkbox-group 中选中项发生改变时触发 change 事件,`event.type = 'change'`,`event.detail = { value: [选中的 checkbox 的 value 的数组] }` | ### radio @@ -345,7 +345,7 @@ movable-view的可移动区域。 | 事件名 | 说明 | | ----------------| ------------------ | -| change | radio-group 中选中项发生改变时触发 change 事件,`detail = { value: [ 选中的 radio 的 value 的数组 ] }` | +| change | radio-group 中选中项发生改变时触发 change 事件,`event.type = 'change'`,`event.detail = { value: 选中的 radio 的 value }` | ### form @@ -358,7 +358,7 @@ movable-view的可移动区域。 | 事件名 | 说明 | | ---------- | --------------------------------------------------- | | submit | 携带 form 中的数据触发 submit 事件,`event.detail = {value : {'name': 'value'} }` | -| reset | 表单重置时会触发 reset 事件 | +| reset | 表单重置时会触发 reset 事件,`event.type = 'reset'` | ### input @@ -517,7 +517,7 @@ movable-view的可移动区域。 | 事件名 | 说明 | | ----------------| ----------------------------------------------------| -| cancel | 取消选择时触发 | +| cancel | 取消选择时触发 cancel 事件,`event.type = 'cancel'` | | change | value 改变时触发 change 事件,`event.detail = {value}` | #### 普通选择器:mode = selector {#mode-selector} @@ -658,6 +658,42 @@ API > - canvas 的实现主要借助于 PostMessage 方式与 webview 容器通信进行绘制,所以对于严格依赖方法执行时机的场景,如调用 drawImage 绘图,再通过 getImageData 获取图片数据的场景,调用时需要使用 await 等方式来保证方法的执行时机 > - 通过 Canvas.createImage 画图,图片的链接不能有特殊字符,安卓手机可能会 load 失败 +### camera + +系统相机。 + +#### 属性 {#camera-props} + +| 属性名 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| mode | string | `normal` | 应用模式,可选值为 `normal`、`scanCode` | +| resolution | string | `medium` | 分辨率,可选值为 `low`、`medium`、`high` | +| device-position | string | `back` | 摄像头朝向,可选值为 `front`、`back` | +| flash | string | `auto` | 闪光灯,可选值为 `auto`、`on`、`off` | +| frame-size | string | `medium` | 指定期望的相机帧数据尺寸,可选值为 `small`、`medium`、`large` | + +#### 事件 {#camera-events} + +| 事件名 | 说明 | +| --- | --- | +| initdone | 相机初始化完成时触发,`event.detail = { maxZoom }` | +| stop | 摄像头在非正常终止时触发 stop 事件,`event.type = 'stop'` | +| error | 相机发生错误时触发 | +| scancode | 在 `scanCode` 模式下识别到二维码时触发,`event.detail = { result, type, scanArea }` | + +#### API + +| 方法名 | 说明 | +| --- | --- | +| setZoom | 设置缩放级别 | +| takePhoto | 拍照,仅支持在 `normal` 模式中使用 | +| startRecord | 开始录像 | +| stopRecord | 结束录像 | + +> [!tip] 注意 +> +> camera 组件基于第三方库 `react-native-vision-camera` 实现,需要容器安装该依赖并完成相机权限配置。 + ### video 视频 @@ -688,13 +724,13 @@ API | ----------------| --------------------------------------------------- | | play | 当开始/继续播放时触发play事件 | | pause | 当暂停播放时触发 pause 事件 | -| ended | 当播放到末尾时触发 ended 事件 | +| ended | 当播放到末尾时触发 ended 事件,`event.type = 'ended'` | | timeupdate | 播放进度变化时触发,`event.detail = {currentTime, duration}` | | fullscreenchange | 视频进入和退出全屏时触发,`event.detail = {fullScreen` } | | waiting | 视频出现缓冲时触发 | -| error | 视频播放出错时触发 | +| error | 视频播放出错时触发 error 事件,`event.type = 'error'` | | loadedmetadata | 视频元数据加载完成时触发。`event.detail = {width, height, duration}` | -| controlstoggle | 切换 controls 显示隐藏时触发。`event.detail = {show}` | +| controlstoggle | 切换 controls 显示隐藏时触发 controlstoggle 事件,`event.type = 'controlstoggle'`,`event.detail = {show}` | | seekcomplete | seek 完成时触发 | > [!tip] 注意 diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-camera.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-camera.tsx index 06ae4b8427..7c55c41b56 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-camera.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-camera.tsx @@ -20,7 +20,7 @@ interface CameraProps { flash?: 'auto' | 'on' | 'off' 'frame-size'?: 'small' | 'medium' | 'large' style?: Record - bindstop?: () => void + bindstop?: (event: Record) => void binderror?: (error: { message: string }) => void bindinitdone?: (result: { type: string, data: string }) => void bindscancode?: (result: { type: string, data: string }) => void @@ -177,7 +177,7 @@ const _camera = forwardRef, CameraProps>((props: Ca }, [bindinitdone, maxZoom]) const onStopped = useCallback(() => { - bindstop && bindstop() + bindstop && bindstop(getCustomEvent('stop', {}, { layoutRef })) }, [bindstop]) const camera: CameraRef = useMemo(() => ({ diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/Image.ts b/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/Image.ts index e34fa0714c..074dbfecf5 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/Image.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/Image.ts @@ -13,8 +13,8 @@ export class Image { canvas: any; width: number; height: number; - private _loadListener: any; - private _errorListener: any; + private _removeLoadListener?: () => void; + private _removeErrorListener?: () => void; private _onload: ((...args: any[]) => void); private _onerror: ((...args: any[]) => void); [key: string]: any; @@ -70,12 +70,8 @@ export class Image { set onload (callback: ((...args: any[]) => void)) { this._onload = callback - if (this._loadListener) { - this.canvas.removeMessageListener(this._loadListener) - } - if (callback) { - this._loadListener = this.addEventListener('load', callback) - } + this._removeLoadListener?.() + this._removeLoadListener = callback ? this.addEventListener('load', callback) : undefined } get onload (): ((...args: any[]) => void) { @@ -84,12 +80,8 @@ export class Image { set onerror (callback: ((...args: any[]) => void)) { this._onerror = callback - if (this._errorListener) { - this.canvas.removeMessageListener(this._errorListener) - } - if (callback) { - this._errorListener = this.addEventListener('error', callback) - } + this._removeErrorListener?.() + this._removeErrorListener = callback ? this.addEventListener('error', callback) : undefined } get onerror () : ((...args: any[]) => void) { diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/index.tsx index b2841d8834..83fca7a3ac 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/index.tsx @@ -200,7 +200,10 @@ const _Canvas = forwardRef, CanvasPr } const removeMessageListener = (listener: any) => { - canvasRef.current.listeners.splice(canvasRef.current.listeners.indexOf(listener), 1) + const index = canvasRef.current.listeners.indexOf(listener) + if (index > -1) { + canvasRef.current.listeners.splice(index, 1) + } } const onMessage = useCallback((e: { nativeEvent: { data: string } }) => { @@ -288,22 +291,22 @@ const _Canvas = forwardRef, CanvasPr allowUniversalAccessFromFileURLs: true }) ) + } else { + canvasComponent = createElement(View, innerProps, createElement(WebView, { + ref: (element: WebViewInstance | null) => { + if (canvasRef.current) { + canvasRef.current.webview = element + } + }, + style: [stylesheet.webview, { height, width }], + source: { html }, + originWhitelist: originWhitelist, + onMessage: onMessage, + onLoad: onLoad, + scrollEnabled: false + })) } - canvasComponent = createElement(View, innerProps, createElement(WebView, { - ref: (element: WebViewInstance | null) => { - if (canvasRef.current) { - canvasRef.current.webview = element - } - }, - style: [stylesheet.webview, { height, width }], - source: { html }, - originWhitelist: originWhitelist, - onMessage: onMessage, - onLoad: onLoad, - scrollEnabled: false - })) - if (hasPositionFixed) { canvasComponent = createElement(Portal, null, canvasComponent) } diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox-group.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox-group.tsx index 1e63226338..8579c41254 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox-group.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox-group.tsx @@ -138,7 +138,7 @@ const CheckboxGroup = forwardRef< bindchange && bindchange( getCustomEvent( - 'tap', + 'change', evt, { layoutRef, diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-form.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-form.tsx index 7851f6e544..12ad2865bb 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-form.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-form.tsx @@ -23,7 +23,7 @@ interface FormProps { value: any } }) => void - bindreset?: () => void + bindreset?: (evt: Record) => void } const _Form = forwardRef, FormProps>((fromProps: FormProps, ref): JSX.Element => { @@ -98,7 +98,7 @@ const _Form = forwardRef, FormProps>((fromProps: For const reset = () => { const { bindreset } = propsRef.current - bindreset && bindreset() + bindreset && bindreset(getCustomEvent('reset', {}, { layoutRef }, propsRef.current)) formValuesMap.forEach(item => item.resetValue()) } return { diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/date.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/date.tsx index 4508887a9e..643665cef2 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/date.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/date.tsx @@ -121,7 +121,7 @@ const valueChanged2Obj = (currentObj: FormatObj, value: number[], limit = 3) => const rangeArr = currentObj.rangeArr if (limit === 3 && (currentValue[0] !== value[0] || currentValue[1] !== value[1])) { - const days = daysInMonth(value[0], value[1] + 1) + const days = daysInMonth(value[0] + START_YEAR, value[1] + 1) rangeArr[2] = days const maxIndex = days.length - 1 if (value[2] > maxIndex) { diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx index 5118c965cd..10758ad863 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx @@ -250,7 +250,7 @@ const Picker = forwardRef, PickerProps>( } const onCancel = () => { - bindcancel?.() + bindcancel?.(getCustomEvent('cancel', {}, { layoutRef }, props)) hide() } diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-radio-group.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-radio-group.tsx index fc0a69ec03..903c710718 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-radio-group.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-radio-group.tsx @@ -118,7 +118,7 @@ const radioGroup = forwardRef< bindchange && bindchange( getCustomEvent( - 'tap', + 'change', evt, { layoutRef, diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-sticky-header.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-sticky-header.tsx index 86a06de3f1..b82a7dd6f0 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-sticky-header.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-sticky-header.tsx @@ -57,7 +57,7 @@ const _StickyHeader = forwardRef, StickyHead const headerTopRef = useRef(0) useEffect(() => { - registerStickyHeader({ key: id, updatePosition }) + registerStickyHeader({ id, updatePosition }) return () => { unregisterStickyHeader(id) } diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper.tsx index d8621b671e..f6c88d4f95 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper.tsx @@ -30,6 +30,7 @@ import Portal from './mpx-portal' * ✘ snap-to-edge */ type EaseType = 'default' | 'linear' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic' +type ChangeSource = '' | 'touch' | 'autoplay' type StrAbsoType = 'absoluteX' | 'absoluteY' type StrVelocityType = 'velocityX' | 'velocityY' type EventDataType = { @@ -201,6 +202,7 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr const preMarginShared = useSharedValue(preMargin) const nextMarginShared = useSharedValue(nextMargin) const autoplayShared = useSharedValue(autoplay) + const changeSource = useSharedValue('') // 默认前后补位的元素个数 const patchElmNum = circular ? (preMargin ? 2 : 1) : 0 const patchElmNumShared = useSharedValue(patchElmNum) @@ -419,6 +421,7 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr duration: easeDuration, easing: easeMap[easeingFunc] }, () => { + changeSource.value = 'autoplay' currentIndex.value = nextIndex runOnJS(runOnJSCallback)('loop') }) @@ -434,6 +437,7 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr const initOffset = -step.value * patchElmNumShared.value + preMarginShared.value // 将开始位置设置为真正的位置 offset.value = initOffset + changeSource.value = 'autoplay' currentIndex.value = nextIndex runOnJS(runOnJSCallback)('loop') }) @@ -445,6 +449,7 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr duration: easeDuration, easing: easeMap[easeingFunc] }, () => { + changeSource.value = 'autoplay' currentIndex.value = nextIndex runOnJS(runOnJSCallback)('loop') }) @@ -474,8 +479,8 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr } }, []) - function handleSwiperChange (current: number) { - const eventData = getCustomEvent('change', {}, { detail: { current, source: 'touch' }, layoutRef: layoutRef }) + function handleSwiperChange (current: number, source: ChangeSource = 'touch') { + const eventData = getCustomEvent('change', {}, { detail: { current, source }, layoutRef: layoutRef }) bindchange && bindchange(eventData) } @@ -508,6 +513,7 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr duration: easeDuration, easing: easeMap[easeingFunc] }, () => { + changeSource.value = '' currentIndex.value = propCurrent }) } else { @@ -526,7 +532,7 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr useAnimatedReaction(() => currentIndex.value, (newIndex: number, preIndex: number) => { // 这里必须传递函数名, 直接写()=> {}形式会报 访问了未sharedValue信息 if (newIndex !== preIndex && bindchange && !isFirstRef.current) { - runOnJS(runOnJSCallback)('handleSwiperChange', newIndex, propCurrent) + runOnJS(runOnJSCallback)('handleSwiperChange', newIndex, changeSource.value) } isFirstRef.current = false }) @@ -552,6 +558,7 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr childrenLength.value = children.length if (children.length - 1 < currentIndex.value) { pauseLoop() + changeSource.value = '' currentIndex.value = 0 offset.value = getOffset(0, step.value) if (autoplay && children.length > 1) { @@ -655,6 +662,7 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr easing: easeMap[easeingFunc] }, () => { if (touchfinish.value !== false) { + changeSource.value = 'touch' currentIndex.value = selectedIndex offset.value = resetOffset runOnJS(runOnJSCallback)('resumeLoop') @@ -666,6 +674,7 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr easing: easeMap[easeingFunc] }, () => { if (touchfinish.value !== false) { + changeSource.value = 'touch' currentIndex.value = selectedIndex runOnJS(runOnJSCallback)('resumeLoop') } @@ -688,6 +697,7 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr easing: easeMap[easeingFunc] }, () => { if (touchfinish.value !== false) { + changeSource.value = 'touch' currentIndex.value = moveToIndex runOnJS(runOnJSCallback)('resumeLoop') } @@ -794,6 +804,7 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr const offsetHalf = computeHalf() if (childrenLength.value > 1 && offsetHalf) { const { selectedIndex } = getTargetPosition({ transdir: moveDistance } as EventEndType) + changeSource.value = 'touch' currentIndex.value = selectedIndex } // 2. 非循环: 处理用户一直拖拽到临界点的场景,如果放到onFinalize无法阻止offset.value更新为越界的值 diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-video.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-video.tsx index 5e5d6dc312..b177b704e8 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-video.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-video.tsx @@ -213,7 +213,7 @@ const MpxVideo = forwardRef, VideoProps>((videoProp } function handleEnd () { - bindended!(getCustomEvent('end', {}, { layoutRef }, propsRef.current)) + bindended!(getCustomEvent('ended', {}, { layoutRef }, propsRef.current)) } function handleWaiting ({ isBuffering }: OnBufferData) { @@ -259,7 +259,7 @@ const MpxVideo = forwardRef, VideoProps>((videoProp function handleAndroidControlsVisibilityChange ({ isVisible }: OnControlsVisibilityChange) { bindcontrolstoggle!( - getCustomEvent('progress', + getCustomEvent('controlstoggle', {}, { detail: { @@ -292,7 +292,7 @@ const MpxVideo = forwardRef, VideoProps>((videoProp } function handleError ({ error }: OnVideoErrorData) { - binderror && binderror(getCustomEvent('play', {}, { detail: { errMsg: error.localizedFailureReason }, layoutRef }, propsRef.current)) + binderror && binderror(getCustomEvent('error', {}, { detail: { errMsg: error.localizedFailureReason }, layoutRef }, propsRef.current)) } function play () { From d4f2c3a476093f44aa912726b8aaabf3148a8603 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Mon, 27 Jul 2026 15:11:29 +0800 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20=E7=A7=BB=E9=99=A4=20RN=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=BF=AE=E5=A4=8D=E6=96=87=E6=A1=A3=E5=8F=98=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../references/rn-template-reference.md | 18 +++---- docs-vitepress/guide/rn/component.md | 54 ++++--------------- 2 files changed, 18 insertions(+), 54 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-template-reference.md b/.agents/skills/mpx2rn/references/rn-template-reference.md index 2f711770f4..509787cb90 100644 --- a/.agents/skills/mpx2rn/references/rn-template-reference.md +++ b/.agents/skills/mpx2rn/references/rn-template-reference.md @@ -822,7 +822,7 @@ Mpx 输出 RN 内置支持了大部分常用的基础组件,详情见下方文 | 事件名 | 说明 | | --- | --- | -| change | current 改变时会触发 change 事件,`event.detail = {current, source}`;`source` 为 `touch`、`autoplay` 或空字符串,分别表示用户滑动、自动播放或其他原因导致的切换 | +| change | current 改变时会触发 change 事件,`event.detail = {current, source}` | ### swiper-item @@ -963,7 +963,7 @@ movable-view 的可移动区域。 | 事件名 | 说明 | | --- | --- | -| change | checkbox-group 中选中项发生改变时触发 change 事件,`event.type = 'change'`,`event.detail = { value: [选中的 checkbox 的 value 的数组] }` | +| change | checkbox-group 中选中项发生改变时触发 change 事件,`detail = { value: [ 选中的 checkbox 的 value 的数组 ] } ` | ### radio @@ -986,7 +986,7 @@ movable-view 的可移动区域。 | 事件名 | 说明 | | --- | --- | -| change | radio-group 中选中项发生改变时触发 change 事件,`event.type = 'change'`,`event.detail = { value: 选中的 radio 的 value }` | +| change | radio-group 中选中项发生改变时触发 change 事件,`detail = { value: [ 选中的 radio 的 value 的数组 ] }` | ### form @@ -999,7 +999,7 @@ movable-view 的可移动区域。 | 事件名 | 说明 | | --- | --- | | submit | 携带 form 中的数据触发 submit 事件,`event.detail = {value : {'name': 'value'} }` | -| reset | 表单重置时会触发 reset 事件,`event.type = 'reset'` | +| reset | 表单重置时会触发 reset 事件 | ### input @@ -1162,7 +1162,7 @@ movable-view 的可移动区域。 | 事件名 | 说明 | | ---------- | ------------------------------------------------------ | -| cancel | 取消选择时触发 cancel 事件,`event.type = 'cancel'` | +| cancel | 取消选择时触发 | | change | value 改变时触发 change 事件,`event.detail = {value}` | #### 普通选择器:mode = selector @@ -1352,7 +1352,7 @@ level 有效值: | 事件名 | 说明 | | --- | --- | | initdone | 相机初始化完成时触发,`event.detail = { maxZoom }` | -| stop | 摄像头在非正常终止时触发 stop 事件,`event.type = 'stop'` | +| stop | 摄像头在非正常终止时触发 | | error | 相机发生错误时触发 | | scancode | 在 `scanCode` 模式下识别到二维码时触发,`event.detail = { result, type, scanArea }` | @@ -1398,13 +1398,13 @@ level 有效值: | --- | --- | | play | 当开始/继续播放时触发 play 事件 | | pause | 当暂停播放时触发 pause 事件 | -| ended | 当播放到末尾时触发 ended 事件,`event.type = 'ended'` | +| ended | 当播放到末尾时触发 ended 事件 | | timeupdate | 播放进度变化时触发,`event.detail = {currentTime, duration}` | | fullscreenchange | 视频进入和退出全屏时触发,`event.detail = {fullScreen` } | | waiting | 视频出现缓冲时触发 | -| error | 视频播放出错时触发 error 事件,`event.type = 'error'` | +| error | 视频播放出错时触发 | | loadedmetadata | 视频元数据加载完成时触发。`event.detail = {width, height, duration}` | -| controlstoggle | 切换 controls 显示隐藏时触发 controlstoggle 事件,`event.type = 'controlstoggle'`,`event.detail = {show}` | +| controlstoggle | 切换 controls 显示隐藏时触发。`event.detail = {show}` | | seekcomplete | seek 完成时触发 | #### 注意事项 diff --git a/docs-vitepress/guide/rn/component.md b/docs-vitepress/guide/rn/component.md index c9c201ed96..6a56a3f781 100644 --- a/docs-vitepress/guide/rn/component.md +++ b/docs-vitepress/guide/rn/component.md @@ -7,7 +7,7 @@ - #### 基础组件 **容器组件**:[view](#view) · [scroll-view](#scroll-view) · [swiper](#swiper) · [swiper-item](#swiper-item) · [movable-area](#movable-area) · [movable-view](#movable-view) · [root-portal](#root-portal) · [sticky-section](#sticky-section) · [sticky-header](#sticky-header) · [cover-view](#cover-view) -**媒体组件**:[image](#image) · [video](#video) · [canvas](#canvas) · [camera](#camera) +**媒体组件**:[image](#image) · [video](#video) · [canvas](#canvas) **表单组件**:[input](#input) · [textarea](#textarea) · [button](#button) · [checkbox](#checkbox) · [checkbox-group](#checkbox-group) · [radio](#radio) · [radio-group](#radio-group) · [switch](#switch) · [picker](#picker) · [picker-view](#picker-view) · [picker-view-column](#picker-view-column) · [form](#form) · [label](#label) @@ -181,7 +181,7 @@ | 事件名 | 说明 | | ----------------| ------------------ | -| change| current 改变时会触发 change 事件,`event.detail = {current, source}`;`source` 为 `touch`、`autoplay` 或空字符串,分别表示用户滑动、自动播放或其他原因导致的切换 | +| change| current 改变时会触发 change 事件,`event.detail = {current, source}`| ### swiper-item 仅可放置在swiper组件中,宽高自动设置为100%。 @@ -320,7 +320,7 @@ movable-view的可移动区域。 | 事件名 | 说明 | | ----------------| ------------------ | -| change | checkbox-group 中选中项发生改变时触发 change 事件,`event.type = 'change'`,`event.detail = { value: [选中的 checkbox 的 value 的数组] }` | +| change | checkbox-group 中选中项发生改变时触发 change 事件,`detail = { value: [ 选中的 checkbox 的 value 的数组 ] } `| ### radio @@ -345,7 +345,7 @@ movable-view的可移动区域。 | 事件名 | 说明 | | ----------------| ------------------ | -| change | radio-group 中选中项发生改变时触发 change 事件,`event.type = 'change'`,`event.detail = { value: 选中的 radio 的 value }` | +| change | radio-group 中选中项发生改变时触发 change 事件,`detail = { value: [ 选中的 radio 的 value 的数组 ] }` | ### form @@ -358,7 +358,7 @@ movable-view的可移动区域。 | 事件名 | 说明 | | ---------- | --------------------------------------------------- | | submit | 携带 form 中的数据触发 submit 事件,`event.detail = {value : {'name': 'value'} }` | -| reset | 表单重置时会触发 reset 事件,`event.type = 'reset'` | +| reset | 表单重置时会触发 reset 事件 | ### input @@ -517,7 +517,7 @@ movable-view的可移动区域。 | 事件名 | 说明 | | ----------------| ----------------------------------------------------| -| cancel | 取消选择时触发 cancel 事件,`event.type = 'cancel'` | +| cancel | 取消选择时触发 | | change | value 改变时触发 change 事件,`event.detail = {value}` | #### 普通选择器:mode = selector {#mode-selector} @@ -658,42 +658,6 @@ API > - canvas 的实现主要借助于 PostMessage 方式与 webview 容器通信进行绘制,所以对于严格依赖方法执行时机的场景,如调用 drawImage 绘图,再通过 getImageData 获取图片数据的场景,调用时需要使用 await 等方式来保证方法的执行时机 > - 通过 Canvas.createImage 画图,图片的链接不能有特殊字符,安卓手机可能会 load 失败 -### camera - -系统相机。 - -#### 属性 {#camera-props} - -| 属性名 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| mode | string | `normal` | 应用模式,可选值为 `normal`、`scanCode` | -| resolution | string | `medium` | 分辨率,可选值为 `low`、`medium`、`high` | -| device-position | string | `back` | 摄像头朝向,可选值为 `front`、`back` | -| flash | string | `auto` | 闪光灯,可选值为 `auto`、`on`、`off` | -| frame-size | string | `medium` | 指定期望的相机帧数据尺寸,可选值为 `small`、`medium`、`large` | - -#### 事件 {#camera-events} - -| 事件名 | 说明 | -| --- | --- | -| initdone | 相机初始化完成时触发,`event.detail = { maxZoom }` | -| stop | 摄像头在非正常终止时触发 stop 事件,`event.type = 'stop'` | -| error | 相机发生错误时触发 | -| scancode | 在 `scanCode` 模式下识别到二维码时触发,`event.detail = { result, type, scanArea }` | - -#### API - -| 方法名 | 说明 | -| --- | --- | -| setZoom | 设置缩放级别 | -| takePhoto | 拍照,仅支持在 `normal` 模式中使用 | -| startRecord | 开始录像 | -| stopRecord | 结束录像 | - -> [!tip] 注意 -> -> camera 组件基于第三方库 `react-native-vision-camera` 实现,需要容器安装该依赖并完成相机权限配置。 - ### video 视频 @@ -724,13 +688,13 @@ API | ----------------| --------------------------------------------------- | | play | 当开始/继续播放时触发play事件 | | pause | 当暂停播放时触发 pause 事件 | -| ended | 当播放到末尾时触发 ended 事件,`event.type = 'ended'` | +| ended | 当播放到末尾时触发 ended 事件 | | timeupdate | 播放进度变化时触发,`event.detail = {currentTime, duration}` | | fullscreenchange | 视频进入和退出全屏时触发,`event.detail = {fullScreen` } | | waiting | 视频出现缓冲时触发 | -| error | 视频播放出错时触发 error 事件,`event.type = 'error'` | +| error | 视频播放出错时触发 | | loadedmetadata | 视频元数据加载完成时触发。`event.detail = {width, height, duration}` | -| controlstoggle | 切换 controls 显示隐藏时触发 controlstoggle 事件,`event.type = 'controlstoggle'`,`event.detail = {show}` | +| controlstoggle | 切换 controls 显示隐藏时触发。`event.detail = {show}` | | seekcomplete | seek 完成时触发 | > [!tip] 注意 From c75eccb50f8bd61482f81b8600ce86d10da633fb Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Mon, 27 Jul 2026 15:19:25 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=20swiper=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/runtime/components/react/mpx-swiper.tsx | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper.tsx index f6c88d4f95..d8621b671e 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper.tsx @@ -30,7 +30,6 @@ import Portal from './mpx-portal' * ✘ snap-to-edge */ type EaseType = 'default' | 'linear' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic' -type ChangeSource = '' | 'touch' | 'autoplay' type StrAbsoType = 'absoluteX' | 'absoluteY' type StrVelocityType = 'velocityX' | 'velocityY' type EventDataType = { @@ -202,7 +201,6 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr const preMarginShared = useSharedValue(preMargin) const nextMarginShared = useSharedValue(nextMargin) const autoplayShared = useSharedValue(autoplay) - const changeSource = useSharedValue('') // 默认前后补位的元素个数 const patchElmNum = circular ? (preMargin ? 2 : 1) : 0 const patchElmNumShared = useSharedValue(patchElmNum) @@ -421,7 +419,6 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr duration: easeDuration, easing: easeMap[easeingFunc] }, () => { - changeSource.value = 'autoplay' currentIndex.value = nextIndex runOnJS(runOnJSCallback)('loop') }) @@ -437,7 +434,6 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr const initOffset = -step.value * patchElmNumShared.value + preMarginShared.value // 将开始位置设置为真正的位置 offset.value = initOffset - changeSource.value = 'autoplay' currentIndex.value = nextIndex runOnJS(runOnJSCallback)('loop') }) @@ -449,7 +445,6 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr duration: easeDuration, easing: easeMap[easeingFunc] }, () => { - changeSource.value = 'autoplay' currentIndex.value = nextIndex runOnJS(runOnJSCallback)('loop') }) @@ -479,8 +474,8 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr } }, []) - function handleSwiperChange (current: number, source: ChangeSource = 'touch') { - const eventData = getCustomEvent('change', {}, { detail: { current, source }, layoutRef: layoutRef }) + function handleSwiperChange (current: number) { + const eventData = getCustomEvent('change', {}, { detail: { current, source: 'touch' }, layoutRef: layoutRef }) bindchange && bindchange(eventData) } @@ -513,7 +508,6 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr duration: easeDuration, easing: easeMap[easeingFunc] }, () => { - changeSource.value = '' currentIndex.value = propCurrent }) } else { @@ -532,7 +526,7 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr useAnimatedReaction(() => currentIndex.value, (newIndex: number, preIndex: number) => { // 这里必须传递函数名, 直接写()=> {}形式会报 访问了未sharedValue信息 if (newIndex !== preIndex && bindchange && !isFirstRef.current) { - runOnJS(runOnJSCallback)('handleSwiperChange', newIndex, changeSource.value) + runOnJS(runOnJSCallback)('handleSwiperChange', newIndex, propCurrent) } isFirstRef.current = false }) @@ -558,7 +552,6 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr childrenLength.value = children.length if (children.length - 1 < currentIndex.value) { pauseLoop() - changeSource.value = '' currentIndex.value = 0 offset.value = getOffset(0, step.value) if (autoplay && children.length > 1) { @@ -662,7 +655,6 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr easing: easeMap[easeingFunc] }, () => { if (touchfinish.value !== false) { - changeSource.value = 'touch' currentIndex.value = selectedIndex offset.value = resetOffset runOnJS(runOnJSCallback)('resumeLoop') @@ -674,7 +666,6 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr easing: easeMap[easeingFunc] }, () => { if (touchfinish.value !== false) { - changeSource.value = 'touch' currentIndex.value = selectedIndex runOnJS(runOnJSCallback)('resumeLoop') } @@ -697,7 +688,6 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr easing: easeMap[easeingFunc] }, () => { if (touchfinish.value !== false) { - changeSource.value = 'touch' currentIndex.value = moveToIndex runOnJS(runOnJSCallback)('resumeLoop') } @@ -804,7 +794,6 @@ const SwiperWrapper = forwardRef, SwiperProps>((pr const offsetHalf = computeHalf() if (childrenLength.value > 1 && offsetHalf) { const { selectedIndex } = getTargetPosition({ transdir: moveDistance } as EventEndType) - changeSource.value = 'touch' currentIndex.value = selectedIndex } // 2. 非循环: 处理用户一直拖拽到临界点的场景,如果放到onFinalize无法阻止offset.value更新为越界的值 From 99178ebb4707556cc7144e5c5d802029ab4248de Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Thu, 10 Sep 2026 15:33:55 +0800 Subject: [PATCH 4/4] fix(rn): preserve camera event target props --- .../lib/runtime/components/react/mpx-camera.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-camera.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-camera.tsx index 7c55c41b56..bc44407948 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-camera.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-camera.tsx @@ -83,6 +83,8 @@ function getCameraPermissionCache (pageId: number | undefined) { const _camera = forwardRef, CameraProps>((props: CameraProps, ref): JSX.Element | null => { // eslint-disable-next-line @typescript-eslint/no-var-requires const cameraRef = useRef(null) + const propsRef = useRef(props) + propsRef.current = props const { mode = 'normal', resolution = 'medium', @@ -163,7 +165,7 @@ const _camera = forwardRef, CameraProps>((props: Ca type, scanArea: [parseInt(frame.x) || 0, parseInt(frame.y) || 0, parseInt(frame.width) || 0, parseInt(frame.height) || 0] } - })) + }, propsRef.current)) }) } }) @@ -173,11 +175,11 @@ const _camera = forwardRef, CameraProps>((props: Ca detail: { maxZoom } - })) + }, propsRef.current)) }, [bindinitdone, maxZoom]) const onStopped = useCallback(() => { - bindstop && bindstop(getCustomEvent('stop', {}, { layoutRef })) + bindstop && bindstop(getCustomEvent('stop', {}, { layoutRef }, propsRef.current)) }, [bindstop]) const camera: CameraRef = useMemo(() => ({