Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/api-proxy/src/platform/api/camera/index.ios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import CreateCamera from './rnCamera'

function createCameraContext () {
return new CreateCamera()
}

export {
createCameraContext
}
7 changes: 7 additions & 0 deletions packages/api-proxy/src/platform/api/camera/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ENV_OBJ, envError } from '../../../common/js'

const createCameraContext = ENV_OBJ.createCameraContext || envError('createCameraContext')

export {
createCameraContext
}
111 changes: 111 additions & 0 deletions packages/api-proxy/src/platform/api/camera/rnCamera.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { noop } from '@mpxjs/utils'

const qualityValue = {
high: 90,
normal: 75,
low: 50,
original: 100
}
export default class CreateCamera {
constructor () {
const navigation = Object.values(global.__mpxPagesMap || {})[0]?.[1]
this.camera = navigation?.camera || {}
}

setZoom (options = {}) {
const { zoom, success = noop, fail = noop, complete = noop } = options
if (this.camera.setZoom) {
this.camera.setZoom(zoom)
}
}
takePhoto (options = {}) {
const { success = noop, fail = noop, complete = noop } = options
const takePhoto = this.camera.getTakePhoto?.()
if (takePhoto) {
takePhoto({
quality: qualityValue[options.quality || 'normal']
}).then((res) => {
const result = {
errMsg: 'takePhoto:ok',
tempImagePath: res.path
}
success(result)
complete(result)
}).catch(() => {
const result = {
errMsg: 'takePhoto:fail'
}
fail(result)
complete(result)
})
}
}
startRecord (options = {}) {
let { timeout = 30, success = noop, fail = noop, complete = noop, timeoutCallback = noop } = options
timeout = timeout > 300 ? 300 : timeout
let recordTimer = null
let isTimeout = false
const startRecord = this.camera.getStartRecord?.()
if (startRecord) {
const result = {
errMsg: 'startRecord:ok'
}
success(result)
complete(result)
startRecord({
onRecordingError: (res) => {
clearTimeout(recordTimer)
timeoutCallback()
},
onRecordingFinished: (res) => {
if (isTimeout) {
console.log('record timeout, ignore', res)
}
clearTimeout(recordTimer)
console.log('record finished', res)
}
})
recordTimer = setTimeout(() => { // 超时自动停止
if (this.camera.stopRecord) {
this.camera.stopRecord().catch(() => {})
}
}, timeout * 1000)
} else {
const result = {
errMsg: 'startRecord:fail to initialize the camera'
}
fail(result)
complete(result)
}
}
stopRecord(options = {}) {
const { success = noop, fail = noop, complete = noop } = options
const stopRecord = this.camera.getStopRecord?.()
if (stopRecord) {
stopRecord().then((res) => {
console.log('stopRecord res', res)
const result = {
errMsg: 'stopRecord:ok',
tempVideoPath: res.path,
duration: res.duration * 1000, // 转成ms
size: res.fileSize
}
success(result)
complete(result)
}).catch((e) => {
console.log('stopRecord error', e)
const result = {
errMsg: 'stopRecord:fail'
}
fail(result)
complete(result)
})
} else {
const result = {
errMsg: 'stopRecord:fail to initialize the camera'
}
fail(result)
complete(result)
}
}
}
6 changes: 6 additions & 0 deletions packages/api-proxy/src/platform/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export * from './api/create-selector-query'
// getNetworkType, onNetworkStatusChange, offNetworkStatusChange
export * from './api/device/network'

// startWifi, stopWifi, getWifiList, onGetWifiList, offGetWifiList, getConnectedWifi
export * from './api/device/wifi'

// downloadFile, uploadFile
export * from './api/file'

Expand Down Expand Up @@ -119,3 +122,6 @@ export * from './api/keyboard'

// getSetting, openSetting, enableAlertBeforeUnload, disableAlertBeforeUnload, getMenuButtonBoundingClientRect
export * from './api/setting'

// createCameraContext
export * from './api/camera'
8 changes: 6 additions & 2 deletions packages/core/src/platform/patch/getDefaultOptions.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as ReactNative from 'react-native'
import { ReactiveEffect } from '../../observer/effect'
import { watch } from '../../observer/watch'
import { del, reactive, set } from '../../observer/reactive'
import { hasOwn, isFunction, noop, isObject, isArray, getByPath, collectDataset, hump2dash, dash2hump, callWithErrorHandling, wrapMethodsWithErrorHandling, error, setFocusedNavigation } from '@mpxjs/utils'
import { hasOwn, isFunction, noop, isObject, isArray, getByPath, collectDataset, hump2dash, dash2hump, callWithErrorHandling, wrapMethodsWithErrorHandling, error, warn, setFocusedNavigation } from '@mpxjs/utils'
import MpxProxy from '../../core/proxy'
import { BEFOREUPDATE, ONLOAD, UPDATED, ONSHOW, ONHIDE, ONRESIZE, REACTHOOKSEXEC } from '../../core/innerLifecycle'
import mergeOptions from '../../core/mergeOptions'
Expand Down Expand Up @@ -593,7 +593,7 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
const instanceRef = useRef(null)
const propsRef = useRef(null)
const intersectionCtx = useContext(IntersectionObserverContext)
const { pageId } = useContext(RouteContext) || {}
const { pageId, navigation } = useContext(RouteContext) || {}
const parentProvides = useContext(ProviderContext)
let relation = null
if (hasDescendantRelation || hasAncestorRelation) {
Expand Down Expand Up @@ -652,6 +652,10 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
usePageEffect(proxy, pageId)
useEffect(() => {
proxy.mounted()
if (navigation.camera?.multi) {
navigation.camera.multi = false
warn('<camera>: 一个页面只能插入一个')
}
return () => {
proxy.unmounted()
proxy.target.__resetInstance()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ module.exports = function ({ print }) {
const qaEventLog = print({ platform: 'qa', tag: TAG_NAME, isError: false, type: 'event' })
return {
test: TAG_NAME,
ios (tag, { el }) {
el.isBuiltIn = true
return 'mpx-camera'
},
android (tag, { el }) {
el.isBuiltIn = true
return 'mpx-camera'
},
harmony (tag, { el }) {
el.isBuiltIn = true
return 'mpx-camera'
},
props: [
{
test: 'mode',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const JD_UNSUPPORTED_TAG_NAME_ARR = ['functional-page-navigator', 'live-pusher',
// 快应用不支持的标签集合
const QA_UNSUPPORTED_TAG_NAME_ARR = ['movable-view', 'movable-area', 'open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'cover-image']
// RN不支持的标签集合
const RN_UNSUPPORTED_TAG_NAME_ARR = ['open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'audio', 'camera', 'match-media', 'page-container', 'editor', 'keyboard-accessory', 'map']
const RN_UNSUPPORTED_TAG_NAME_ARR = ['open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'audio', 'match-media', 'page-container', 'editor', 'keyboard-accessory', 'map']

/**
* @param {function(object): function} print
Expand Down
167 changes: 167 additions & 0 deletions packages/webpack-plugin/lib/runtime/components/react/mpx-camera.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { forwardRef, useRef, useCallback, useContext, useState, useEffect } from 'react'
import { Camera, useCameraDevice, useCodeScanner, useCameraFormat, useFrameProcessor } from 'react-native-vision-camera'
import { getCustomEvent } from './getInnerListeners'
import { RouteContext } from './context'

interface CameraProps {
mode?: 'normal' | 'scanCode'
resolution?: 'low' | 'medium' | 'high'
devicePosition?: 'front' | 'back'
flash?: 'auto' | 'on' | 'off'
frameSize?: 'small' | 'medium' | 'large'
style?: Record<string, any>
bindstop?: () => void
binderror?: (error: { message: string }) => void
bindinitdone?: (result: { type: string, data: string }) => void
bindscancode?: (result: { type: string, data: string }) => void
}

interface CameraRef {
setZoom: (zoom: number) => void
getTakePhoto: () => (() => Promise<any>) | undefined
getStartRecord: () => ((options: any) => void) | undefined
getStopRecord: () => (() => void) | undefined
}

type HandlerRef<T, P> = {
// 根据实际的 HandlerRef 类型定义调整
current: T | null
}

const _camera = forwardRef<HandlerRef<Camera, CameraProps>, CameraProps>((props: CameraProps, ref): JSX.Element | null => {
const cameraRef = useRef<Camera>(null)
const {
mode = 'normal',
resolution = 'medium',
devicePosition = 'back',
flash = 'auto',
frameSize = 'medium',
bindinitdone,
bindstop,
bindscancode
} = props

const isPhoto = mode === 'normal'
const device = useCameraDevice(devicePosition || 'back')
const { navigation } = useContext(RouteContext) || {}
const [zoomValue, setZoomValue] = useState<number>(1)
const [hasPermission, setHasPermission] = useState<boolean | null>(null)

// 先定义常量,避免在条件判断后使用
const maxZoom = device?.maxZoom || 1
const RESOLUTION_MAPPING: Record<string, { width: number, height: number }> = {
low: { width: 640, height: 480 },
medium: { width: 1280, height: 720 },
high: { width: 1920, height: 1080 }
}
const FRAME_SIZE_MAPPING: Record<string, { width: number, height: number }> = {
small: { width: 480, height: 360 },
medium: { width: 720, height: 540 },
large: { width: 1080, height: 810 }
}

// 所有 Hooks 必须在条件判断之前调用
const format = useCameraFormat(device, [
{
photoResolution: RESOLUTION_MAPPING[resolution],
videoResolution: FRAME_SIZE_MAPPING[frameSize] || RESOLUTION_MAPPING[resolution]
}
])

const codeScanner = useCodeScanner({
codeTypes: ['qr', 'ean-13'],
onCodeScanned: (codes) => {
const result = codes.map(code => code.value).join(',')
bindscancode && bindscancode(getCustomEvent('scancode', {}, {
detail: {
result: codes.map(code => code.value).join(',')
}
}))
}
})

const onInitialized = useCallback(() => {
bindinitdone && bindinitdone(getCustomEvent('initdone', {}, {
detail: {
maxZoom
}
}))
}, [bindinitdone, maxZoom])

const onStopped = useCallback(() => {
bindstop && bindstop()
}, [bindstop])

// 检查相机权限
useEffect(() => {
const checkCameraPermission = async () => {
try {
const cameraPermission = global?.__mpx?.config?.rnConfig?.cameraPermission
if (typeof cameraPermission === 'function') {
const permissionResult = await cameraPermission()
setHasPermission(permissionResult === true)
} else {
setHasPermission(true)
}
} catch (error) {
setHasPermission(false)
}
}

checkCameraPermission()
}, [])

const camera: CameraRef = {
setZoom: (zoom: number) => {
setZoomValue(zoom)
},
getTakePhoto: () => {
return cameraRef.current?.takePhoto
},
getStartRecord: () => {
return cameraRef.current?.startRecording
},
getStopRecord: () => {
return cameraRef.current?.stopRecording
}
}

if (navigation) {
navigation.camera = camera
}

// 所有 Hooks 调用完成后再进行条件判断
if (hasPermission === null) {
return null
}

if (!hasPermission) {
return null
}

if (!device) {
return null
}

return (
<Camera
ref={cameraRef}
isActive={true}
photo={isPhoto}
video={true}
onInitialized={onInitialized}
onStopped={onStopped}
device={device}
flash={flash}
format={format}
codeScanner={!isPhoto ? codeScanner : undefined}
style={{ flex: 1 }}
zoom={zoomValue}
{...props}
/>
)
})

_camera.displayName = 'MpxCamera'

export default _camera
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ const _WebView = forwardRef<HandlerRef<WebView, WebViewProps>, WebViewProps>((pr
}
break
case 'postMessage':
bindmessage && bindmessage(getCustomEvent('messsage', {}, { // RN组件销毁顺序与小程序不一致,所以改成和支付宝消息一致
bindmessage && bindmessage(getCustomEvent('message', {}, { // RN组件销毁顺序与小程序不一致,所以改成和支付宝消息一致
detail: {
data: params[0]?.data
}
Expand Down
2 changes: 1 addition & 1 deletion packages/webpack-plugin/lib/utils/dom-tag-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const isBuildInReactTag = makeMap(
'mpx-movable-area,mpx-label,mpx-keyboard-avoiding-view,mpx-input,mpx-inline-text,' +
'mpx-image,mpx-form,mpx-checkbox,mpx-checkbox-group,mpx-button,' +
'mpx-rich-text,mpx-portal,mpx-popup,mpx-picker-view-column,mpx-picker-view,mpx-picker,' +
'mpx-icon,mpx-canvas'
'mpx-icon,mpx-canvas,mpx-camera'
)

const isSpace = makeMap('ensp,emsp,nbsp')
Expand Down
1 change: 1 addition & 0 deletions packages/webpack-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"react-native-safe-area-context": "^4.12.0",
"react-native-svg": "^15.8.0",
"react-native-video": "^6.9.0",
"react-native-vision-camera": "^4.7.2",
"react-native-webview": "^13.12.2",
"rimraf": "^6.0.1"
},
Expand Down
Loading