From 5deffbdbfb9a602a489d2f4c08d9658652ee6e2c Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Sun, 2 Aug 2026 17:11:19 +0800 Subject: [PATCH] fix(h5-partner): allow reopening WeChat image picker after cancel Avoid resetting JSSDK on each pick and unlock cancel paths so camera/album can be opened again. Co-authored-by: Cursor --- .../src/components/OssUploadField.tsx | 41 ++++++++++++------- apps/h5-partner/src/pages/StoreCreatePage.tsx | 1 - packages/weixin-sdk/src/chooseImage.ts | 35 +++++++++++++--- packages/weixin-sdk/src/types.ts | 1 + 4 files changed, 56 insertions(+), 22 deletions(-) diff --git a/apps/h5-partner/src/components/OssUploadField.tsx b/apps/h5-partner/src/components/OssUploadField.tsx index aeb224a..b47b639 100644 --- a/apps/h5-partner/src/components/OssUploadField.tsx +++ b/apps/h5-partner/src/components/OssUploadField.tsx @@ -21,6 +21,10 @@ type OssUploadFieldProps = { const DEFAULT_MAX_MB = 10; +function isCancelError(msg: string): boolean { + return /cancel|取消/i.test(msg); +} + function formatWechatUploadError(e: unknown): string { const msg = e instanceof Error ? e.message : '无法打开相册'; const formatted = formatChooseImageFailMessage(msg); @@ -46,6 +50,7 @@ export default function OssUploadField({ label, }: OssUploadFieldProps) { const inputRef = useRef(null); + const pickingRef = useRef(false); const [uploading, setUploading] = useState(false); const [error, setError] = useState(''); const [showAlbumFallback, setShowAlbumFallback] = useState(false); @@ -63,7 +68,7 @@ export default function OssUploadField({ useEffect(() => { if (!useWechatPicker) return; - weixinSdk.reset(); + // 仅预热,勿在每次点击时 reset,否则取消后再点常无法调起 void weixinSdk.init().catch(() => { /* 点击上传时会再次初始化 */ }); @@ -96,31 +101,37 @@ export default function OssUploadField({ } async function pickWechatImage() { - setUploading(true); + if (pickingRef.current || uploading) return; + pickingRef.current = true; setError(''); try { - weixinSdk.reset(); + // 不要每次 reset:会打断 JSSDK,取消后再点经常无法调起相机/相册 await weixinSdk.init(); - // 选图 + 上传须在同一个队列任务内完成,避免嵌套 enqueueUpload 死锁 - await enqueueUpload(async () => { - const files = await weixinSdk.chooseImages({ - count: 1, - sourceType: ['album', 'camera'], - }); - if (!files?.[0]) return; - await persistUpload(files[0]); + const files = await weixinSdk.chooseImages({ + count: 1, + sourceType: ['album', 'camera'], }); + // 取消或未选图:直接结束,允许再次点击 + if (!files?.length) return; + + setUploading(true); + try { + // 仅串行化实际上传,选图本身不占上传锁,避免取消后锁态异常 + await enqueueUpload(() => persistUpload(files[0]!)); + } finally { + setUploading(false); + } } catch (e) { const msg = e instanceof Error ? e.message : '无法打开相册'; - if (/cancel/i.test(msg)) return; + if (isCancelError(msg)) return; throw e; } finally { - setUploading(false); + pickingRef.current = false; } } async function pickFile() { - if (uploading) return; + if (uploading || pickingRef.current) return; setError(''); if (useWechatPicker) { @@ -128,7 +139,7 @@ export default function OssUploadField({ await pickWechatImage(); } catch (e) { const msg = e instanceof Error ? e.message : '无法打开相册'; - if (/cancel/i.test(msg)) return; + if (isCancelError(msg)) return; const formatted = formatWechatUploadError(e); setError(`${formatted},可改从系统相册选择`); setShowAlbumFallback(true); diff --git a/apps/h5-partner/src/pages/StoreCreatePage.tsx b/apps/h5-partner/src/pages/StoreCreatePage.tsx index c06fc98..1f0f190 100644 --- a/apps/h5-partner/src/pages/StoreCreatePage.tsx +++ b/apps/h5-partner/src/pages/StoreCreatePage.tsx @@ -134,7 +134,6 @@ export default function StoreCreatePage() { useEffect(() => { if (step !== 2 || !isWechatEnv()) return; - weixinSdk.reset(); void weixinSdk.init().catch(() => { /* OssUploadField 点击时会再次初始化 */ }); diff --git a/packages/weixin-sdk/src/chooseImage.ts b/packages/weixin-sdk/src/chooseImage.ts index fa13151..765a7ed 100644 --- a/packages/weixin-sdk/src/chooseImage.ts +++ b/packages/weixin-sdk/src/chooseImage.ts @@ -17,10 +17,14 @@ function delay(ms: number): Promise { return new Promise((resolve) => window.setTimeout(resolve, ms)); } +function isChooseImageCancelMessage(msg: string): boolean { + return /cancel|取消/i.test(msg.trim()); +} + /** 将微信 chooseImage / getLocalImgData fail 的 errMsg 转为用户可读文案 */ export function formatChooseImageFailMessage(errMsg: string): string { const msg = errMsg.trim() || '无法打开相册'; - if (/cancel/i.test(msg)) return ''; + if (isChooseImageCancelMessage(msg)) return ''; if (/offline verifying|permission value is offline/i.test(msg)) { return '微信权限验证中,请稍候再试或刷新页面后重新选择图片'; @@ -54,7 +58,7 @@ async function reportChooseImageEvent( stage: 'jssdk' | 'choose' | 'read' | 'empty'; }, ) { - if (/cancel/i.test(payload.errMsg)) return; + if (isChooseImageCancelMessage(payload.errMsg)) return; try { const headers: Record = { 'Content-Type': 'application/json', @@ -159,19 +163,38 @@ function invokeChooseImage( sourceType: Array<'album' | 'camera'>, ): Promise { return new Promise((resolve, reject) => { + let settled = false; + const finish = (fn: () => void) => { + if (settled) return; + settled = true; + window.clearTimeout(hangTimer); + fn(); + }; + + // 极端机型取消后无回调:最长等待后按取消解锁,避免业务侧 uploading 永久卡住 + const hangTimer = window.setTimeout(() => { + finish(() => resolve([])); + }, 180_000); + window.wx!.chooseImage!({ count, sizeType: ['compressed'], sourceType, - success: (res) => resolve(res.localIds ?? []), + success: (res) => finish(() => resolve(res.localIds ?? [])), fail: (err) => { const raw = err.errMsg || '无法打开相册'; - if (/cancel/i.test(raw)) { - resolve([]); + if (isChooseImageCancelMessage(raw)) { + finish(() => resolve([])); return; } const formatted = formatChooseImageFailMessage(raw); - reject(new Error(formatted || raw)); + finish(() => reject(new Error(formatted || raw))); + }, + // 部分微信版本取消只走 complete + complete: () => { + window.setTimeout(() => { + finish(() => resolve([])); + }, 300); }, }); }); diff --git a/packages/weixin-sdk/src/types.ts b/packages/weixin-sdk/src/types.ts index ee1336d..28647f6 100644 --- a/packages/weixin-sdk/src/types.ts +++ b/packages/weixin-sdk/src/types.ts @@ -51,6 +51,7 @@ export type WxApi = { sourceType?: Array<'album' | 'camera'>; success?: (res: { localIds: string[] }) => void; fail?: (res: { errMsg: string }) => void; + complete?: (res?: { errMsg?: string }) => void; }) => void; getLocalImgData: (options: { localId: string;