收口个人中心弹层组件
- 新增 PlatformProfileModalShell 统一个人中心主弹层与副弹层壳层 - 抽离 PlatformProfilePlayedWorksModal 与 PlatformProfileReferralModal 并移除首页内联历史与邀请弹层实现 - 让昵称充值任务兑换码账单等弹层复用共享壳层并补齐测试和文档
This commit is contained in:
303
src/components/platform-entry/PlatformProfileReferralModal.tsx
Normal file
303
src/components/platform-entry/PlatformProfileReferralModal.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
import { Copy } from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import communityQqQrImage from '../../../media/social-media-group/qq.png';
|
||||
import communityWechatQrImage from '../../../media/social-media-group/wechat.png';
|
||||
import type { ProfileReferralInviteCenterResponse } from '../../../packages/shared/src/contracts/runtime';
|
||||
import { CopyFeedbackButton } from '../common/CopyFeedbackButton';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import type { CopyFeedbackState } from '../common/useCopyFeedback';
|
||||
import { PlatformProfileSecondaryModalShell } from './PlatformProfileModalShell';
|
||||
import type { ProfileReferralPanel } from './usePlatformProfileCenterController';
|
||||
|
||||
type PlatformProfileReferralModalProps = {
|
||||
panel: ProfileReferralPanel;
|
||||
center: ProfileReferralInviteCenterResponse | null;
|
||||
isLoading: boolean;
|
||||
isSubmittingRedeem: boolean;
|
||||
redeemCode: string;
|
||||
copyInviteState: CopyFeedbackState;
|
||||
error: string | null;
|
||||
success: string | null;
|
||||
onClose: () => void;
|
||||
onCopyInvite: () => void;
|
||||
onRedeemCodeChange: (value: string) => void;
|
||||
onSubmitRedeemCode: () => void;
|
||||
};
|
||||
|
||||
const COMMUNITY_QR_CODES = [
|
||||
{
|
||||
label: '微信群',
|
||||
src: communityWechatQrImage,
|
||||
alt: '玩家社区微信群二维码',
|
||||
},
|
||||
{
|
||||
label: 'QQ群',
|
||||
src: communityQqQrImage,
|
||||
alt: '玩家社区 QQ 群二维码',
|
||||
},
|
||||
] as const;
|
||||
|
||||
function ProfileReferralUserAvatar({
|
||||
name,
|
||||
avatarUrl,
|
||||
}: {
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
}) {
|
||||
const avatarLabel = (name.trim() || '玩').slice(0, 1).toUpperCase();
|
||||
|
||||
return (
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full bg-[#ff4056] text-xs font-black text-white">
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
) : (
|
||||
avatarLabel
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePanelTitle(panel: ProfileReferralPanel) {
|
||||
if (panel === 'invite') {
|
||||
return '邀请好友';
|
||||
}
|
||||
if (panel === 'redeem') {
|
||||
return '填邀请码';
|
||||
}
|
||||
return '玩家社区';
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人中心邀请能力统一弹层。
|
||||
* 承接邀请码、填码和社区二维码三种 profile panel,避免首页继续内联重复白底浮层实现。
|
||||
*/
|
||||
export function PlatformProfileReferralModal({
|
||||
panel,
|
||||
center,
|
||||
isLoading,
|
||||
isSubmittingRedeem,
|
||||
redeemCode,
|
||||
copyInviteState,
|
||||
error,
|
||||
success,
|
||||
onClose,
|
||||
onCopyInvite,
|
||||
onRedeemCodeChange,
|
||||
onSubmitRedeemCode,
|
||||
}: PlatformProfileReferralModalProps) {
|
||||
const title = resolvePanelTitle(panel);
|
||||
const normalizedRedeemCode = redeemCode
|
||||
.trim()
|
||||
.replace(/[^0-9a-z]/gi, '')
|
||||
.toUpperCase();
|
||||
|
||||
let content: ReactNode;
|
||||
|
||||
if (panel === 'community') {
|
||||
content = (
|
||||
<div className="mt-5 grid grid-cols-2 gap-3">
|
||||
{COMMUNITY_QR_CODES.map((qrCode) => (
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
key={qrCode.label}
|
||||
surface="flat"
|
||||
radius="xs"
|
||||
padding="xs"
|
||||
className="text-center"
|
||||
>
|
||||
<div className="aspect-square overflow-hidden rounded-lg border border-zinc-200 bg-white p-1.5">
|
||||
<img
|
||||
src={qrCode.src}
|
||||
alt={qrCode.alt}
|
||||
className="h-full w-full object-contain"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 text-sm font-bold text-zinc-700">
|
||||
{qrCode.label}
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else if (panel === 'redeem') {
|
||||
content = isLoading ? (
|
||||
<div className="mt-5 space-y-3">
|
||||
<div className="h-12 animate-pulse rounded-xl bg-zinc-100" />
|
||||
<div className="h-11 animate-pulse rounded-xl bg-zinc-100" />
|
||||
</div>
|
||||
) : center?.hasRedeemedCode ? (
|
||||
<PlatformEmptyState
|
||||
surface="subpanel"
|
||||
size="inline"
|
||||
tone="base"
|
||||
className="mt-5"
|
||||
>
|
||||
已填写邀请码
|
||||
</PlatformEmptyState>
|
||||
) : (
|
||||
<form
|
||||
className="mt-5 space-y-3"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
onSubmitRedeemCode();
|
||||
}}
|
||||
>
|
||||
<PlatformTextField
|
||||
value={redeemCode}
|
||||
onChange={(event) => onRedeemCodeChange(event.target.value)}
|
||||
size="lg"
|
||||
density="roomy"
|
||||
tone="rose"
|
||||
className="rounded-xl text-center font-black uppercase tracking-[0.16em]"
|
||||
placeholder="邀请码"
|
||||
aria-label="邀请码"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
surface="profile"
|
||||
fullWidth
|
||||
size="md"
|
||||
className="rounded-xl"
|
||||
disabled={isSubmittingRedeem || !normalizedRedeemCode}
|
||||
>
|
||||
{isSubmittingRedeem ? '提交中' : '提交'}
|
||||
</PlatformActionButton>
|
||||
</form>
|
||||
);
|
||||
} else if (isLoading) {
|
||||
content = (
|
||||
<div className="mt-5 space-y-3">
|
||||
<div className="h-20 animate-pulse rounded-xl bg-zinc-100" />
|
||||
<div className="h-10 animate-pulse rounded-xl bg-zinc-100" />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<div className="mt-5 space-y-3">
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
surface="flat"
|
||||
radius="xs"
|
||||
padding="md"
|
||||
className="text-center"
|
||||
>
|
||||
<PlatformFieldLabel
|
||||
variant="section"
|
||||
className="block text-[11px] text-zinc-500"
|
||||
>
|
||||
邀请码
|
||||
</PlatformFieldLabel>
|
||||
<div className="mt-1 text-3xl font-black tracking-[0.16em] text-[#ff4056]">
|
||||
{center?.inviteCode ?? '--------'}
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
<PlatformStatusMessage
|
||||
tone="warning"
|
||||
surface="profile"
|
||||
size="md"
|
||||
className="space-y-0.5 px-3.5 font-semibold"
|
||||
>
|
||||
<div>
|
||||
{`邀请一个用户注册,双方都可以获得${center?.rewardPoints ?? 30}泥点。`}
|
||||
</div>
|
||||
<div>每日最多获得十次邀请奖励。</div>
|
||||
</PlatformStatusMessage>
|
||||
<CopyFeedbackButton
|
||||
state={copyInviteState}
|
||||
onClick={onCopyInvite}
|
||||
disabled={!center?.inviteCode}
|
||||
idleLabel="复制邀请"
|
||||
copiedLabel="已复制"
|
||||
failedLabel="复制失败"
|
||||
idleIcon={<Copy className="h-4 w-4" />}
|
||||
actionSurface="profile"
|
||||
actionSize="md"
|
||||
actionFullWidth
|
||||
className="gap-2 rounded-xl"
|
||||
/>
|
||||
<PlatformSubpanel as="div" surface="flat" radius="xs" padding="sm">
|
||||
<PlatformFieldLabel
|
||||
variant="section"
|
||||
className="block text-zinc-900"
|
||||
>
|
||||
成功邀请
|
||||
</PlatformFieldLabel>
|
||||
{center?.invitedUsers?.length ? (
|
||||
<div className="mt-3 max-h-44 space-y-2 overflow-y-auto pr-1">
|
||||
{center.invitedUsers.map((user) => (
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
key={`${user.userId}-${user.boundAt}`}
|
||||
surface="soft"
|
||||
radius="xs"
|
||||
padding="row"
|
||||
className="flex items-center gap-3"
|
||||
>
|
||||
<ProfileReferralUserAvatar
|
||||
name={user.displayName}
|
||||
avatarUrl={user.avatarUrl}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-bold text-zinc-900">
|
||||
{user.displayName || '玩家'}
|
||||
</div>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<PlatformEmptyState
|
||||
surface="subpanel"
|
||||
size="compact"
|
||||
className="mt-3 text-center text-xs font-semibold leading-normal"
|
||||
>
|
||||
暂无成功邀请
|
||||
</PlatformEmptyState>
|
||||
)}
|
||||
</PlatformSubpanel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PlatformProfileSecondaryModalShell
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
closeVariant="floatingPlain"
|
||||
closeIcon="×"
|
||||
overlayTone="soft"
|
||||
panelClassName="relative !max-w-[24rem] bg-white text-zinc-950 shadow-2xl !rounded-[1.35rem] sm:!rounded-[1.35rem]"
|
||||
contentClassName="relative px-5 pb-5 pt-4"
|
||||
>
|
||||
<div className="text-center text-xl font-black">{title}</div>
|
||||
{content}
|
||||
|
||||
{error ? (
|
||||
<PlatformStatusMessage tone="error" className="mt-4">
|
||||
{error}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
{success ? (
|
||||
<PlatformStatusMessage tone="success" className="mt-4">
|
||||
{success}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
</PlatformProfileSecondaryModalShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user