import { useCallback, useEffect, useRef, useState } from 'react'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import PullToRefresh from '@dukang/shared-ui/PullToRefresh'; import { isIosDevice, isScanPermissionWarmupError } from '@dukang/weixin-sdk'; import { useStoreSession } from '../contexts/StoreSessionContext'; import { request } from '../lib/api'; import { formatMoney, toMoneyNumber } from '../lib/money'; import { parseRedeemTokenFromScan } from '../lib/redeem-scan'; import { authorizeShopWechat, checkNeedsWechatAuth, fetchShopAccount, } from '../lib/wechat-auth'; import { isWechatEnv, weixinSdk } from '../lib/weixin'; import { clearPendingScanAfterAuth, consumeScanWarmupAfterAuth, getPostAuthScanDelayMs, markPendingScanAfterAuth, peekPendingScanAfterAuth, } from '../lib/shop-scan-auth'; import WechatScanAuthModal from '../components/WechatScanAuthModal'; import { useStorePageView } from '../lib/usePageView'; import { trackStore } from '../lib/analytics'; function formatScanError(e: unknown, opts?: { afterAuth?: boolean }): string { const msg = e instanceof Error ? e.message : '扫码失败,请重试'; if (/invalid signature|config:fail|signature/i.test(msg)) { return '微信扫码签名校验失败,请刷新页面或重新授权微信后重试'; } if (isScanPermissionWarmupError(msg)) { if (opts?.afterAuth) { return '微信授权后扫码仍未就绪,请刷新页面或重新授权微信'; } return '微信扫码能力未就绪,请刷新页面或重新授权微信'; } return msg; } function isScanRecoverableError(msg: string): boolean { return isScanPermissionWarmupError(msg) || /签名校验失败|扫码能力未就绪|请刷新页面/i.test(msg); } export default function HomePage() { useStorePageView('store_home_view'); const navigate = useNavigate(); const { ready, authenticated } = useStoreSession(); const [searchParams] = useSearchParams(); const [dash, setDash] = useState | null>(null); const [scanMsg, setScanMsg] = useState(''); const [scanning, setScanning] = useState(false); const [authModalOpen, setAuthModalOpen] = useState(false); const [authModalMode, setAuthModalMode] = useState<'bind' | 'recover'>('bind'); const [authLoading, setAuthLoading] = useState(false); const [authError, setAuthError] = useState(''); const [showOpenModal, setShowOpenModal] = useState(false); const [opening, setOpening] = useState(false); const pendingScanStartedRef = useRef(false); const loadDashboard = useCallback(() => { return request>('SHOP_H5', '/shop/dashboard') .then((d) => { setDash(d); }) .catch(() => {}); }, []); useEffect(() => { void loadDashboard(); }, [loadDashboard]); useEffect(() => { function onResume() { setScanning(false); void loadDashboard(); } function onVisibility() { if (document.visibilityState === 'visible') onResume(); } document.addEventListener('visibilitychange', onVisibility); window.addEventListener('pageshow', onResume); window.addEventListener('focus', onResume); return () => { document.removeEventListener('visibilitychange', onVisibility); window.removeEventListener('pageshow', onResume); window.removeEventListener('focus', onResume); }; }, [loadDashboard]); const runScan = useCallback( async (opts?: { postAuthWarmup?: boolean }) => { trackStore('store_redeem_scan_start'); if (!isWechatEnv()) { setScanMsg('请在微信内打开门店端进行扫码核销'); return; } setScanning(true); if (!opts?.postAuthWarmup) { setScanMsg(''); } try { // iOS / OAuth 回跳后须重新 wx.config(签名用入场 URL) if (opts?.postAuthWarmup || isIosDevice()) { weixinSdk.reset(); } await weixinSdk.init(); const raw = await weixinSdk.scanQrCode( opts?.postAuthWarmup ? { postAuthWarmup: true } : undefined, ); if (!raw) { void loadDashboard(); return; } const token = parseRedeemTokenFromScan(raw); if (!token) { setScanMsg('无法识别核销码,请扫描用户出示的核销二维码'); return; } navigate(`/redeem?token=${encodeURIComponent(token)}`); } catch (e) { const tip = formatScanError(e, { afterAuth: opts?.postAuthWarmup }); setScanMsg(tip); if (isScanRecoverableError(tip)) { setAuthModalMode('recover'); setAuthError(tip); setAuthModalOpen(true); } } finally { setScanning(false); } }, [loadDashboard, navigate], ); // OAuth 由 StoreSessionContext 单例处理;此处仅在授权完成后续扫 useEffect(() => { if (!ready || !authenticated || !isWechatEnv()) return; if (searchParams.get('code')) return; if (!peekPendingScanAfterAuth() || pendingScanStartedRef.current) return; pendingScanStartedRef.current = true; clearPendingScanAfterAuth(); setAuthModalOpen(false); setAuthLoading(false); setAuthError(''); setScanMsg('微信授权成功,正在准备扫码…'); const timer = window.setTimeout(() => { void runScan({ postAuthWarmup: true }); }, getPostAuthScanDelayMs()); return () => window.clearTimeout(timer); }, [ready, authenticated, searchParams, runScan]); async function handleScan() { setScanMsg(''); // 门店临时闭店/休息中(非营业状态)时,点击扫码直接提示,不进入扫码流程 if (status !== 'OPEN') { setShowOpenModal(true); return; } if (!isWechatEnv()) { setScanMsg('请在微信内打开门店端进行扫码核销'); return; } try { const profile = await fetchShopAccount(); if (await checkNeedsWechatAuth(profile)) { pendingScanStartedRef.current = false; setAuthModalMode('bind'); setAuthError(''); setAuthModalOpen(true); return; } const needWarmup = consumeScanWarmupAfterAuth(); await runScan(needWarmup ? { postAuthWarmup: true } : undefined); } catch (e) { setScanMsg(e instanceof Error ? e.message : '无法发起扫码'); } } async function openStoreAndContinue() { if (opening) return; setOpening(true); try { await request('SHOP_H5', '/shop/store/status', { method: 'PUT', body: JSON.stringify({ status: 'OPEN' }), }); setShowOpenModal(false); setScanMsg(''); void loadDashboard(); // 刷新门店状态为营业中,扫码按钮可再次使用 } catch (e) { setShowOpenModal(false); setScanMsg(e instanceof Error ? e.message : '开启营业失败'); } finally { setOpening(false); } } async function startWechatAuth() { setAuthLoading(true); setAuthError(''); try { pendingScanStartedRef.current = false; markPendingScanAfterAuth(); await authorizeShopWechat(); } catch (e) { clearPendingScanAfterAuth(); 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 || ''); const open = status === 'OPEN'; const hoursParts: string[] = []; if (store?.openTime && store?.closeTime) hoursParts.push(`${store.openTime} - ${store.closeTime}`); if (store?.openTime2 && store?.closeTime2) hoursParts.push(`${store.openTime2} - ${store.closeTime2}`); const hoursText = hoursParts.length ? hoursParts.join(',') : '10:00 - 22:00'; const statusText = status === 'CLOSED' ? '永久关闭' : open ? '当前正在营业中' : '当前临时闭店'; return (

门店管理中心

store

{String(store?.name || '门店')}

今日核销笔数

{Number(dash?.todayCount || 0)}

扫码 {Number(dash?.todayScanCount || 0)} · 手机号 {Number(dash?.todayPhoneCount || 0)}

今日到账金额

¥ {formatMoney(toMoneyNumber(dash?.todayAmount))}

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

{scanMsg &&

{scanMsg}

} smartphone 手机号核销
schedule

营业状态

{statusText}

营业时间: {hoursText}

核销记录

查看全部 chevron_right
{recent.length === 0 && (

暂无核销记录

)} {recent.map((r) => (

核销时间

{new Date(String(r.createdAt)).toLocaleString('zh-CN')}

¥{formatMoney(toMoneyNumber(r.amount))}

))}
void startWechatAuth()} onRefresh={() => { window.location.reload(); }} onCancel={() => { setAuthModalOpen(false); setAuthModalMode('bind'); setAuthError(''); clearPendingScanAfterAuth(); pendingScanStartedRef.current = false; }} /> {showOpenModal && (

门店休息中

门店目前休息中无法核销,是否开启营业?

)}
); }