收口个人中心状态弹层与扫码组件

新增 PlatformStatusDialog 统一支付结果与确认中状态弹层
新增 PlatformProfileQrScannerModal 统一个人中心扫码面板
RpgEntryHomeView 改用共享组件并删除内联支付与扫码实现
更新 PlatformUiKit 收口文档与团队决策记录
This commit is contained in:
2026-06-10 20:24:09 +08:00
parent 914b74ce8e
commit f54c3ee936
7 changed files with 583 additions and 260 deletions

View File

@@ -1,9 +1,7 @@
import {
AlertCircle,
ArrowRight,
BookOpen,
Camera,
CheckCircle2,
ChevronDown,
ChevronRight,
Clock3,
@@ -13,7 +11,6 @@ import {
Gamepad2,
GitFork,
Heart,
Loader2,
LogIn,
MessageCircle,
Palette,
@@ -28,7 +25,6 @@ import {
ThumbsUp,
Ticket,
UserRound,
XCircle,
} from 'lucide-react';
import {
type ComponentType,
@@ -90,6 +86,7 @@ import { PlatformIconBadge } from '../common/PlatformIconBadge';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformStatusDialog } from '../common/PlatformStatusDialog';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { PlatformTextField } from '../common/PlatformTextField';
@@ -122,6 +119,7 @@ import {
} from '../platform-entry/PlatformProfilePrimitives';
import { PlatformProfileModalShell } from '../platform-entry/PlatformProfileModalShell';
import { PlatformProfilePlayedWorksModal } from '../platform-entry/PlatformProfilePlayedWorksModal';
import { PlatformProfileQrScannerModal } from '../platform-entry/PlatformProfileQrScannerModal';
import { PlatformProfileRechargeModal } from '../platform-entry/PlatformProfileRechargeModal';
import { PlatformProfileReferralModal } from '../platform-entry/PlatformProfileReferralModal';
import { PlatformProfileRewardCodeRedeemModal } from '../platform-entry/PlatformProfileRewardCodeRedeemModal';
@@ -866,22 +864,6 @@ function readyRecommendRuntime(
scanResources();
});
}
const PROFILE_QR_SCAN_INTERVAL_MS = 360;
type BarcodeDetectorLike = {
detect: (source: CanvasImageSource) => Promise<Array<{ rawValue?: string }>>;
};
type BarcodeDetectorConstructorLike = new (options?: {
formats?: string[];
}) => BarcodeDetectorLike;
function getBarcodeDetectorConstructor(): BarcodeDetectorConstructorLike | null {
const maybeDetector = (
globalThis as unknown as {
BarcodeDetector?: BarcodeDetectorConstructorLike;
}
).BarcodeDetector;
return typeof maybeDetector === 'function' ? maybeDetector : null;
}
type DiscoverChannel =
| 'recommend'
| 'today'
@@ -2484,261 +2466,39 @@ function RechargePaymentResultModal({
result: RechargePaymentResult;
onClose: () => void;
}) {
const Icon =
const status =
result.kind === 'success'
? CheckCircle2
: result.kind === 'cancel'
? XCircle
: AlertCircle;
const iconClass =
result.kind === 'success'
? 'text-[var(--platform-success-text)]'
: result.kind === 'cancel'
? 'text-[var(--platform-text-soft)]'
: 'text-[var(--platform-button-danger-text)]';
? 'success'
: result.kind === 'pending'
? 'loading'
: result.kind === 'cancel'
? 'cancel'
: 'error';
return (
<UnifiedModal
open
<PlatformStatusDialog
status={status}
title={result.title}
description={result.message}
onClose={onClose}
showHeader={false}
showCloseButton={false}
closeOnBackdrop={false}
closeOnEscape={false}
portal={false}
size="sm"
action={{ label: '知道了', onClick: onClose }}
zIndexClassName="z-[90]"
overlayClassName={PROFILE_MODAL_OVERLAY_CLASS}
panelClassName="platform-remap-surface !max-w-sm rounded-[1.4rem]"
bodyClassName="px-5 pb-5 pt-6 text-center"
>
<PlatformIconBadge
icon={<Icon className="h-8 w-8" aria-hidden="true" />}
size="xl"
tone="neutral"
className={`mx-auto bg-white/10 ${iconClass}`}
/>
<div className="mt-4 text-xl font-black text-[var(--platform-text-strong)]">
{result.title}
</div>
<div className="mt-3 text-sm font-semibold leading-6 text-[var(--platform-text-soft)]">
{result.message}
</div>
<PlatformActionButton
surface="profile"
fullWidth
size="md"
className="mt-5"
onClick={onClose}
>
</PlatformActionButton>
</UnifiedModal>
/>
);
}
function RechargePaymentConfirmationMask({ orderId }: { orderId: string }) {
return (
<UnifiedModal
open
<PlatformStatusDialog
status="confirming"
title="正在确认支付"
description={`订单 ${orderId} 正在同步到账状态,请先停留在当前页面。`}
onClose={() => undefined}
showHeader={false}
showCloseButton={false}
closeOnBackdrop={false}
closeOnEscape={false}
portal={false}
size="sm"
closeDisabled
zIndexClassName="z-[95]"
overlayClassName={PROFILE_MODAL_OVERLAY_CLASS}
panelClassName="platform-remap-surface !max-w-sm rounded-[1.4rem]"
bodyClassName="px-5 pb-5 pt-6 text-center"
>
<PlatformIconBadge
icon={<Loader2 className="h-8 w-8 animate-spin" aria-hidden="true" />}
size="xl"
tone="neutral"
className="mx-auto bg-white/10 text-[var(--platform-accent)]"
/>
<div className="mt-4 text-xl font-black text-[var(--platform-text-strong)]">
</div>
<div className="mt-3 text-sm font-semibold leading-6 text-[var(--platform-text-soft)]">
{orderId}
</div>
</UnifiedModal>
);
}
function ProfileQrScannerModal({
error,
result,
onClose,
onError,
onResult,
}: {
error: string | null;
result: string | null;
onClose: () => void;
onError: (message: string) => void;
onResult: (value: string) => void;
}) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const streamRef = useRef<MediaStream | null>(null);
useEffect(() => {
const videoElement = videoRef.current;
if (!videoElement) {
return;
}
let isMounted = true;
let scanTimer: number | null = null;
const detectorCtor = getBarcodeDetectorConstructor();
const detector = detectorCtor
? new detectorCtor({ formats: ['qr_code'] })
: null;
const clearScanTimer = () => {
if (scanTimer !== null) {
window.clearTimeout(scanTimer);
scanTimer = null;
}
};
const stopCamera = () => {
const stream = streamRef.current;
streamRef.current = null;
if (stream) {
stream.getTracks().forEach((track) => track.stop());
}
videoElement.srcObject = null;
};
const scanVideo = async () => {
if (!isMounted || !detector || videoElement.readyState < 2) {
if (isMounted && detector) {
scanTimer = window.setTimeout(scanVideo, PROFILE_QR_SCAN_INTERVAL_MS);
}
return;
}
try {
const codes = await detector.detect(videoElement);
const rawValue = codes[0]?.rawValue?.trim();
if (rawValue) {
clearScanTimer();
stopCamera();
onResult(rawValue);
return;
}
} catch {
onError('扫码识别失败,请调整二维码位置');
}
if (isMounted) {
scanTimer = window.setTimeout(scanVideo, PROFILE_QR_SCAN_INTERVAL_MS);
}
};
const startCamera = async () => {
if (
typeof navigator === 'undefined' ||
!navigator.mediaDevices?.getUserMedia
) {
onError('当前浏览器不支持摄像头扫码');
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: { facingMode: { ideal: 'environment' } },
});
if (!isMounted) {
stream.getTracks().forEach((track) => track.stop());
return;
}
streamRef.current?.getTracks().forEach((track) => track.stop());
streamRef.current = stream;
videoElement.srcObject = stream;
await videoElement.play();
if (!detector) {
onError('当前浏览器暂不支持二维码识别');
return;
}
scanTimer = window.setTimeout(scanVideo, PROFILE_QR_SCAN_INTERVAL_MS);
} catch {
onError('无法打开摄像头,请检查权限');
}
};
void startCamera();
return () => {
isMounted = false;
clearScanTimer();
stopCamera();
};
}, [onError, onResult]);
return (
<UnifiedModal
open
title="扫码"
onClose={onClose}
showHeader={false}
showCloseButton={false}
closeOnBackdrop={false}
closeOnEscape={false}
portal={false}
size="sm"
zIndexClassName="z-[80]"
overlayClassName={PROFILE_MODAL_OVERLAY_CLASS}
panelClassName="platform-qr-scanner-modal !max-w-sm rounded-[1.4rem]"
bodyClassName="!p-0"
>
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
<div className="text-base font-black"></div>
<PlatformModalCloseButton
label="关闭扫码"
onClick={onClose}
icon="×"
/>
</div>
<div className="space-y-3 px-5 py-5">
<div className="platform-qr-scanner-modal__viewport">
<video
ref={videoRef}
className="h-full w-full object-cover"
playsInline
muted
/>
<span className="platform-qr-scanner-modal__frame" />
</div>
{result ? (
<PlatformStatusMessage
tone="success"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
{result}
</PlatformStatusMessage>
) : error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
{error}
</PlatformStatusMessage>
) : null}
</div>
</UnifiedModal>
/>
);
}
@@ -5145,7 +4905,7 @@ export function RpgEntryHomeView({
/>
) : null;
const qrScannerModal: ReactNode = isQrScannerOpen ? (
<ProfileQrScannerModal
<PlatformProfileQrScannerModal
error={qrScannerError}
result={qrScannerResult}
onClose={() => {