diff --git a/apps/h5-partner/src/components/OssUploadField.tsx b/apps/h5-partner/src/components/OssUploadField.tsx index def9c63..aeb224a 100644 --- a/apps/h5-partner/src/components/OssUploadField.tsx +++ b/apps/h5-partner/src/components/OssUploadField.tsx @@ -1,6 +1,8 @@ -import { useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { uploadFileToOss, type OssMediaType } from '../lib/upload'; import { enqueueUpload } from '../lib/upload-lock'; +import { formatChooseImageFailMessage } from '@dukang/weixin-sdk'; +import { isWechatEnv, weixinSdk } from '../lib/weixin'; import { toastError } from '../lib/toast'; type OssUploadFieldProps = { @@ -12,10 +14,27 @@ type OssUploadFieldProps = { wide?: boolean; compact?: boolean; label?: string; + /** 兼容现有调用:选图不再依赖 openId 绑定 */ + wechatReady?: boolean; + onWechatReadyChange?: (ready: boolean) => void; }; const DEFAULT_MAX_MB = 10; +function formatWechatUploadError(e: unknown): string { + const msg = e instanceof Error ? e.message : '无法打开相册'; + const formatted = formatChooseImageFailMessage(msg); + if (formatted) return formatted; + if (/invalid signature/i.test(msg)) { + return '微信 JSSDK 签名校验失败:请确认公众号已配置 JS 接口安全域名,并刷新页面后重试'; + } + return msg; +} + +function acceptsImages(accept: string) { + return accept.includes('image'); +} + export default function OssUploadField({ value, onChange, @@ -29,6 +48,7 @@ export default function OssUploadField({ const inputRef = useRef(null); const [uploading, setUploading] = useState(false); const [error, setError] = useState(''); + const [showAlbumFallback, setShowAlbumFallback] = useState(false); function showUploadError(text: string) { setError(text); @@ -37,6 +57,17 @@ export default function OssUploadField({ const resolvedAccept = accept ?? (mediaType === 'VIDEO' ? 'video/*' : mediaType === 'FILE' ? 'image/*,.pdf' : 'image/*'); + const inWechat = isWechatEnv(); + const useWechatPicker = + inWechat && (mediaType === 'IMAGE' || (mediaType === 'FILE' && acceptsImages(resolvedAccept))); + + useEffect(() => { + if (!useWechatPicker) return; + weixinSdk.reset(); + void weixinSdk.init().catch(() => { + /* 点击上传时会再次初始化 */ + }); + }, [useWechatPicker]); async function persistUpload(file: File) { if (!file.size) { @@ -64,21 +95,60 @@ export default function OssUploadField({ } } - function pickFile() { + async function pickWechatImage() { + setUploading(true); + setError(''); + try { + weixinSdk.reset(); + 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]); + }); + } catch (e) { + const msg = e instanceof Error ? e.message : '无法打开相册'; + if (/cancel/i.test(msg)) return; + throw e; + } finally { + setUploading(false); + } + } + + async function pickFile() { if (uploading) return; setError(''); + + if (useWechatPicker) { + try { + await pickWechatImage(); + } catch (e) { + const msg = e instanceof Error ? e.message : '无法打开相册'; + if (/cancel/i.test(msg)) return; + const formatted = formatWechatUploadError(e); + setError(`${formatted},可改从系统相册选择`); + setShowAlbumFallback(true); + inputRef.current?.click(); + } + return; + } + inputRef.current?.click(); } const isImage = mediaType === 'IMAGE' && value; const isFile = mediaType === 'FILE' && value; const busy = uploading; - const pickerLabel = label ?? (mediaType === 'IMAGE' ? '从系统相册选择' : '点击上传'); + const pickerLabel = label ?? (useWechatPicker ? '拍照 / 从相册选择' : '点击上传'); const triggerProps = { type: 'button' as const, disabled: busy, - onClick: pickFile, + onClick: () => void pickFile(), }; return ( @@ -92,6 +162,7 @@ export default function OssUploadField({ onChange={(e) => { const file = e.target.files?.[0]; if (file) { + setShowAlbumFallback(false); void uploadSelectedFile(file); } }} @@ -120,13 +191,24 @@ export default function OssUploadField({ className={`partner-upload-dashed${wide ? ' partner-upload-dashed--wide' : ''}${compact ? ' partner-upload-dashed--compact' : ''}`} > - {busy ? 'hourglass_top' : 'photo_library'} + {busy ? 'hourglass_top' : 'add_a_photo'} {uploading ? '上传中…' : pickerLabel} )} + {showAlbumFallback && useWechatPicker && ( + + )} {error &&

{error}

} ); diff --git a/apps/h5-partner/src/pages/StoreCreatePage.tsx b/apps/h5-partner/src/pages/StoreCreatePage.tsx index a65d70c..c06fc98 100644 --- a/apps/h5-partner/src/pages/StoreCreatePage.tsx +++ b/apps/h5-partner/src/pages/StoreCreatePage.tsx @@ -15,6 +15,8 @@ import { checkStorePhoneAvailable } from '../lib/storePhone'; import { fetchPartnerCities, type OpenCityOption } from '../lib/upload'; +import { isWechatEnv, weixinSdk } from '../lib/weixin'; + import { usePartnerSession } from '../contexts/PartnerSessionContext'; import TencentLocPickerOverlay from '../components/TencentLocPickerOverlay'; @@ -130,7 +132,13 @@ export default function StoreCreatePage() { saveStoreDraft({ step, form }, accountId); }, [step, form, accountId]); - + useEffect(() => { + if (step !== 2 || !isWechatEnv()) return; + weixinSdk.reset(); + void weixinSdk.init().catch(() => { + /* OssUploadField 点击时会再次初始化 */ + }); + }, [step]); useEffect(() => { @@ -900,7 +908,7 @@ export default function StoreCreatePage() { onChange={(coverUrl) => patchForm({ coverUrl })} - label="从系统相册选择" + label="拍照 / 从相册选择" /> @@ -946,7 +954,7 @@ export default function StoreCreatePage() {

签约合同 *

-

上传签约协议首页与盖章页

+

拍照上传签约协议首页与盖章页

)} diff --git a/apps/h5-shop/src/App.tsx b/apps/h5-shop/src/App.tsx index 58e9481..78cf4f9 100644 --- a/apps/h5-shop/src/App.tsx +++ b/apps/h5-shop/src/App.tsx @@ -5,6 +5,7 @@ import LoginPage from './pages/LoginPage'; import LegalPage from './pages/LegalPage'; import SelectStorePage from './pages/SelectStorePage'; import HomePage from './pages/HomePage'; +import RedeemConfirmPage from './pages/RedeemConfirmPage'; import PhoneRedeemPage from './pages/PhoneRedeemPage'; import RedeemSuccessPage from './pages/RedeemSuccessPage'; import RecordsPage from './pages/RecordsPage'; @@ -21,7 +22,7 @@ export default function App() { } /> } /> } /> - } /> + } /> } /> } /> }> diff --git a/apps/h5-shop/src/pages/HomePage.tsx b/apps/h5-shop/src/pages/HomePage.tsx index f6b833e..a09ca35 100644 --- a/apps/h5-shop/src/pages/HomePage.tsx +++ b/apps/h5-shop/src/pages/HomePage.tsx @@ -1,15 +1,47 @@ import { useCallback, useEffect, useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import PullToRefresh from '@dukang/shared-ui/PullToRefresh'; +import { useStoreSession } from '../contexts/StoreSessionContext'; import { request } from '../lib/api'; +import { parseRedeemTokenFromScan } from '../lib/redeem-scan'; +import { + authorizeShopWechat, + checkNeedsWechatAuth, + fetchShopAccount, + handleShopWechatCallback, + handleShopWechatLoginResult, +} from '../lib/wechat-auth'; +import { isWechatEnv, weixinSdk } from '../lib/weixin'; +import { stripOAuthParamsFromLocation } from '@dukang/weixin-sdk'; +import WechatScanAuthModal from '../components/WechatScanAuthModal'; + +const PENDING_SCAN_KEY = 'shop_pending_scan'; function formatMoney(n: number) { return n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); } +function formatScanError(e: unknown): string { + const msg = e instanceof Error ? e.message : '扫码失败,请重试'; + if (/invalid signature/i.test(msg)) { + return '微信 JSSDK 签名校验失败:请确认公众号已配置 JS 接口安全域名,并刷新页面后重试'; + } + if (/offline verifying|权限验证中|接口未就绪/i.test(msg)) { + return `${msg}(可在 URL 后加 ?wxdebug=1 开启 JSSDK 调试查看详情)`; + } + return msg; +} + export default function HomePage() { const navigate = useNavigate(); + const { applySession } = useStoreSession(); + const [searchParams, setSearchParams] = useSearchParams(); const [dash, setDash] = useState | null>(null); + const [scanMsg, setScanMsg] = useState(''); + const [scanning, setScanning] = useState(false); + const [authModalOpen, setAuthModalOpen] = useState(false); + const [authLoading, setAuthLoading] = useState(false); + const [authError, setAuthError] = useState(''); const loadDashboard = useCallback(() => { return request>('SHOP_H5', '/shop/dashboard') @@ -25,6 +57,7 @@ export default function HomePage() { useEffect(() => { function onResume() { + setScanning(false); void loadDashboard(); } function onVisibility() { @@ -40,6 +73,88 @@ export default function HomePage() { }; }, [loadDashboard]); + async function runScan() { + if (!isWechatEnv()) { + setScanMsg('请在微信内打开门店端进行扫码核销'); + return; + } + setScanning(true); + setScanMsg(''); + try { + await weixinSdk.init(); + const raw = await weixinSdk.scanQrCode(); + if (!raw) { + void loadDashboard(); + return; + } + const token = parseRedeemTokenFromScan(raw); + if (!token) { + setScanMsg('无法识别核销码,请扫描用户出示的核销二维码'); + return; + } + navigate(`/redeem?token=${encodeURIComponent(token)}`); + } catch (e) { + setScanMsg(formatScanError(e)); + } finally { + setScanning(false); + } + } + + useEffect(() => { + if (!isWechatEnv() || !searchParams.get('code')) return; + void handleShopWechatCallback() + .then((result) => { + if (!result) return; + const session = handleShopWechatLoginResult(result); + if (session) { + applySession(session); + } + setAuthModalOpen(false); + setAuthError(''); + stripOAuthParamsFromLocation(); + setSearchParams({}, { replace: true }); + const shouldScan = sessionStorage.getItem(PENDING_SCAN_KEY) === '1'; + sessionStorage.removeItem(PENDING_SCAN_KEY); + if (shouldScan) { + window.setTimeout(() => void runScan(), 0); + } + }) + .catch((e) => { + setAuthError(e instanceof Error ? e.message : '微信授权失败'); + }); + }, [searchParams, applySession, setSearchParams]); + + async function handleScan() { + setScanMsg(''); + if (!isWechatEnv()) { + setScanMsg('请在微信内打开门店端进行扫码核销'); + return; + } + try { + const profile = await fetchShopAccount(); + if (await checkNeedsWechatAuth(profile)) { + setAuthModalOpen(true); + return; + } + await runScan(); + } catch (e) { + setScanMsg(e instanceof Error ? e.message : '无法发起扫码'); + } + } + + async function startWechatAuth() { + setAuthLoading(true); + setAuthError(''); + try { + sessionStorage.setItem(PENDING_SCAN_KEY, '1'); + await authorizeShopWechat(); + } catch (e) { + sessionStorage.removeItem(PENDING_SCAN_KEY); + setAuthError(e instanceof Error ? e.message : '微信授权失败'); + setAuthLoading(false); + } + } + const store = dash?.store as Record | undefined; const recent = (dash?.recentRecords as Array>) || []; const status = String(store?.status || ''); @@ -79,13 +194,20 @@ export default function HomePage() {
- void handleScan()} > + qr_code_scanner + +

{scanning ? '正在打开相机…' : '扫码核销'}

+ {scanMsg &&

{scanMsg}

} + smartphone + 手机号核销 -

手机号核销

@@ -132,6 +254,17 @@ export default function HomePage() {
+ void startWechatAuth()} + onCancel={() => { + setAuthModalOpen(false); + setAuthError(''); + sessionStorage.removeItem(PENDING_SCAN_KEY); + }} + /> ); }