收口个人中心标准弹窗壳层

扩展 UnifiedModal 支持关闭按钮变体与局部交互语义覆写
将昵称修改充值任务与兑换码弹窗迁移到 UnifiedModal
更新 PlatformUiKit 收口计划和 Hermes 决策记录
This commit is contained in:
2026-06-10 17:05:05 +08:00
parent 9a04ea55dc
commit ba5f84d963
5 changed files with 400 additions and 343 deletions

View File

@@ -132,6 +132,7 @@ import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { PlatformTextField } from '../common/PlatformTextField';
import { RUNTIME_RESOURCE_PENDING_SELECTOR } from '../common/RuntimeResourcePendingMarker';
import { SquareImageCropModal } from '../common/SquareImageCropModal';
import { UnifiedModal } from '../common/UnifiedModal';
import {
buildCenteredSquareImageCropRect,
clampSquareImageCropRect,
@@ -2824,6 +2825,13 @@ function ProfileReferralUserAvatar({
);
}
const PROFILE_MODAL_OVERLAY_CLASS =
'platform-modal-backdrop !items-center !justify-center !px-4 !py-6';
const PROFILE_MODAL_HEADER_CLASS = 'border-white/10 px-5 py-4';
const PROFILE_MODAL_TITLE_CLASS = 'text-base font-black';
const PROFILE_MODAL_DESCRIPTION_CLASS =
'mt-1 text-xs font-semibold text-[var(--platform-text-soft)]';
function ProfileNicknameModal({
value,
error,
@@ -2840,65 +2848,61 @@ function ProfileNicknameModal({
onSubmit: () => void;
}) {
return (
<div className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6">
<div
role="dialog"
aria-modal="true"
aria-labelledby="profile-nickname-title"
className="platform-modal-shell platform-remap-surface w-full max-w-sm overflow-hidden rounded-[1.4rem]"
>
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
<div id="profile-nickname-title" className="text-base font-black">
</div>
<PlatformModalCloseButton
label="关闭昵称修改"
variant="profileCompact"
onClick={onClose}
icon="×"
/>
</div>
<div className="px-5 py-5">
<label className="block">
<span className="sr-only"></span>
<PlatformTextField
autoFocus
value={value}
onChange={(event) => onChange(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
onSubmit();
}
}}
maxLength={20}
surface="editorDark"
size="lg"
density="roomy"
className="rounded-2xl border-white/12 bg-white/10 text-[var(--platform-text-strong)] focus:border-[var(--platform-surface-hover-border)]"
placeholder="输入新昵称"
/>
</label>
{error ? (
<PlatformStatusMessage
tone="error"
surface="tinted"
className="mt-3 rounded-2xl border-rose-400/25 text-rose-600"
>
{error}
</PlatformStatusMessage>
) : null}
<div className="mt-5 grid grid-cols-2 gap-3">
<PlatformActionButton tone="secondary" onClick={onClose}>
</PlatformActionButton>
<PlatformActionButton onClick={onSubmit} disabled={isSaving}>
{isSaving ? '保存中' : '保存'}
</PlatformActionButton>
</div>
</div>
<UnifiedModal
open
title="修改昵称"
onClose={onClose}
closeLabel="关闭昵称修改"
closeVariant="profileCompact"
closeOnBackdrop={false}
closeOnEscape={false}
portal={false}
size="sm"
zIndexClassName="z-[80]"
overlayClassName={PROFILE_MODAL_OVERLAY_CLASS}
panelClassName="platform-remap-surface !max-w-sm rounded-[1.4rem]"
headerClassName={PROFILE_MODAL_HEADER_CLASS}
titleClassName={PROFILE_MODAL_TITLE_CLASS}
bodyClassName="px-5 py-5"
>
<label className="block">
<span className="sr-only"></span>
<PlatformTextField
autoFocus
value={value}
onChange={(event) => onChange(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
onSubmit();
}
}}
maxLength={20}
surface="editorDark"
size="lg"
density="roomy"
className="rounded-2xl border-white/12 bg-white/10 text-[var(--platform-text-strong)] focus:border-[var(--platform-surface-hover-border)]"
placeholder="输入新昵称"
/>
</label>
{error ? (
<PlatformStatusMessage
tone="error"
surface="tinted"
className="mt-3 rounded-2xl border-rose-400/25 text-rose-600"
>
{error}
</PlatformStatusMessage>
) : null}
<div className="mt-5 grid grid-cols-2 gap-3">
<PlatformActionButton tone="secondary" onClick={onClose}>
</PlatformActionButton>
<PlatformActionButton onClick={onSubmit} disabled={isSaving}>
{isSaving ? '保存中' : '保存'}
</PlatformActionButton>
</div>
</div>
</UnifiedModal>
);
}
@@ -3189,125 +3193,126 @@ function ProfileRechargeModal({
);
return (
<div className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6">
<div className="platform-recharge-modal w-full max-w-[34rem] overflow-hidden rounded-[1.4rem]">
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
<div>
<div className="text-base font-black"></div>
<div className="mt-1 text-xs font-semibold text-[var(--platform-text-soft)]">
{center
? `${center.walletBalance}泥点 · ${memberLabel}`
: '读取中'}
</div>
</div>
<PlatformModalCloseButton
label="关闭账户充值"
onClick={onClose}
icon="×"
/>
</div>
<div className="max-h-[min(76vh,36rem)] overflow-y-auto px-5 py-5">
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => onTabChange('points')}
className={`platform-category-chip justify-center ${activeTab === 'points' ? 'platform-category-chip--active' : ''}`}
>
</button>
<button
type="button"
onClick={() => onTabChange('membership')}
className={`platform-category-chip justify-center ${activeTab === 'membership' ? 'platform-category-chip--active' : ''}`}
>
</button>
</div>
{error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="mt-4 rounded-2xl font-semibold"
>
<div>{error}</div>
<PlatformActionButton
surface="profile"
size="xs"
className="mt-3"
onClick={onRetry}
>
</PlatformActionButton>
</PlatformStatusMessage>
) : null}
{isLoading ? (
<div className="mt-4 grid gap-3 sm:grid-cols-2">
{Array.from({ length: 4 }).map((_, index) => (
<div
key={index}
className="h-28 animate-pulse rounded-[1.15rem] bg-white/10"
/>
))}
</div>
) : products.length > 0 ? (
<div className="mt-4 grid gap-3 sm:grid-cols-2">
{products.map((product) => (
<RechargeProductCard
key={product.productId}
product={product}
submittingProductId={submittingProductId}
onBuy={onBuy}
/>
))}
</div>
) : (
<PlatformEmptyState
surface="subpanel"
size="inline"
className="mt-4"
>
</PlatformEmptyState>
)}
{nativePayment ? (
<PlatformSubpanel
as="div"
radius="sm"
padding="md"
className="mt-4 text-center"
>
<div className="text-sm font-black"></div>
<div className="mx-auto mt-3 flex h-[180px] w-[180px] items-center justify-center rounded-xl bg-white p-2">
{nativeQrImageUrl ? (
<img
src={nativeQrImageUrl}
alt="微信 Native 支付二维码"
className="h-full w-full"
/>
) : (
<span className="text-xs font-semibold text-slate-500">
</span>
)}
</div>
<PlatformActionButton
surface="profile"
size="xs"
className="mt-4 disabled:cursor-wait"
onClick={onConfirmNativePayment}
disabled={nativePayment.isConfirming}
>
{nativePayment.isConfirming ? '确认中' : '我已支付'}
</PlatformActionButton>
</PlatformSubpanel>
) : null}
</div>
<UnifiedModal
open
title="账户充值"
description={
center ? `${center.walletBalance}泥点 · ${memberLabel}` : '读取中'
}
onClose={onClose}
closeLabel="关闭账户充值"
closeVariant="profile"
closeOnBackdrop={false}
closeOnEscape={false}
portal={false}
size="md"
zIndexClassName="z-[80]"
overlayClassName={PROFILE_MODAL_OVERLAY_CLASS}
panelClassName="platform-recharge-modal !max-w-[34rem] rounded-[1.4rem]"
headerClassName={PROFILE_MODAL_HEADER_CLASS}
titleClassName={PROFILE_MODAL_TITLE_CLASS}
descriptionClassName={PROFILE_MODAL_DESCRIPTION_CLASS}
bodyClassName="max-h-[min(76vh,36rem)] overflow-y-auto px-5 py-5"
>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => onTabChange('points')}
className={`platform-category-chip justify-center ${activeTab === 'points' ? 'platform-category-chip--active' : ''}`}
>
</button>
<button
type="button"
onClick={() => onTabChange('membership')}
className={`platform-category-chip justify-center ${activeTab === 'membership' ? 'platform-category-chip--active' : ''}`}
>
</button>
</div>
</div>
{error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="mt-4 rounded-2xl font-semibold"
>
<div>{error}</div>
<PlatformActionButton
surface="profile"
size="xs"
className="mt-3"
onClick={onRetry}
>
</PlatformActionButton>
</PlatformStatusMessage>
) : null}
{isLoading ? (
<div className="mt-4 grid gap-3 sm:grid-cols-2">
{Array.from({ length: 4 }).map((_, index) => (
<div
key={index}
className="h-28 animate-pulse rounded-[1.15rem] bg-white/10"
/>
))}
</div>
) : products.length > 0 ? (
<div className="mt-4 grid gap-3 sm:grid-cols-2">
{products.map((product) => (
<RechargeProductCard
key={product.productId}
product={product}
submittingProductId={submittingProductId}
onBuy={onBuy}
/>
))}
</div>
) : (
<PlatformEmptyState
surface="subpanel"
size="inline"
className="mt-4"
>
</PlatformEmptyState>
)}
{nativePayment ? (
<PlatformSubpanel
as="div"
radius="sm"
padding="md"
className="mt-4 text-center"
>
<div className="text-sm font-black"></div>
<div className="mx-auto mt-3 flex h-[180px] w-[180px] items-center justify-center rounded-xl bg-white p-2">
{nativeQrImageUrl ? (
<img
src={nativeQrImageUrl}
alt="微信 Native 支付二维码"
className="h-full w-full"
/>
) : (
<span className="text-xs font-semibold text-slate-500">
</span>
)}
</div>
<PlatformActionButton
surface="profile"
size="xs"
className="mt-4 disabled:cursor-wait"
onClick={onConfirmNativePayment}
disabled={nativePayment.isConfirming}
>
{nativePayment.isConfirming ? '确认中' : '我已支付'}
</PlatformActionButton>
</PlatformSubpanel>
) : null}
</UnifiedModal>
);
}
@@ -3545,113 +3550,114 @@ function ProfileTaskCenterModal({
const walletBalance = center?.walletBalance ?? fallbackBalance;
return (
<div className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6">
<div className="platform-recharge-modal w-full max-w-md overflow-hidden rounded-[1.4rem]">
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
<div>
<div className="text-base font-black"></div>
<div className="mt-1 text-xs font-semibold text-[var(--platform-text-soft)]">
{walletBalance}
</div>
</div>
<PlatformModalCloseButton
label="关闭每日任务"
onClick={onClose}
icon="×"
/>
<UnifiedModal
open
title="每日任务"
description={`${walletBalance}泥点`}
onClose={onClose}
closeLabel="关闭每日任务"
closeVariant="profile"
closeOnBackdrop={false}
closeOnEscape={false}
portal={false}
size="sm"
zIndexClassName="z-[80]"
overlayClassName={PROFILE_MODAL_OVERLAY_CLASS}
panelClassName="platform-recharge-modal !max-w-md rounded-[1.4rem]"
headerClassName={PROFILE_MODAL_HEADER_CLASS}
titleClassName={PROFILE_MODAL_TITLE_CLASS}
descriptionClassName={PROFILE_MODAL_DESCRIPTION_CLASS}
bodyClassName="space-y-3 px-5 py-5"
>
{error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
<div>{error}</div>
<PlatformActionButton
surface="profile"
size="xs"
className="mt-3"
onClick={onRetry}
>
</PlatformActionButton>
</PlatformStatusMessage>
) : null}
{success ? (
<PlatformStatusMessage
tone="success"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
{success}
</PlatformStatusMessage>
) : null}
{isLoading ? (
<div className="space-y-3">
{Array.from({ length: 2 }).map((_, index) => (
<div
key={index}
className="h-20 animate-pulse rounded-2xl bg-white/10"
/>
))}
</div>
<div className="space-y-3 px-5 py-5">
{error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
<div>{error}</div>
<PlatformActionButton
surface="profile"
size="xs"
className="mt-3"
onClick={onRetry}
>
</PlatformActionButton>
</PlatformStatusMessage>
) : null}
{success ? (
<PlatformStatusMessage
tone="success"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
{success}
</PlatformStatusMessage>
) : null}
{isLoading ? (
<div className="space-y-3">
{Array.from({ length: 2 }).map((_, index) => (
<div
key={index}
className="h-20 animate-pulse rounded-2xl bg-white/10"
/>
))}
</div>
) : tasks.length === 0 ? (
<PlatformEmptyState surface="subpanel" size="inline">
</PlatformEmptyState>
) : (
<div className="space-y-3">
{tasks.map((task) => {
const isClaimable = task.status === 'claimable';
const isClaiming = claimingTaskId === task.taskId;
const progressLabel = buildProfileTaskProgressLabel(task);
) : tasks.length === 0 ? (
<PlatformEmptyState surface="subpanel" size="inline">
</PlatformEmptyState>
) : (
<div className="space-y-3">
{tasks.map((task) => {
const isClaimable = task.status === 'claimable';
const isClaiming = claimingTaskId === task.taskId;
const progressLabel = buildProfileTaskProgressLabel(task);
return (
<PlatformSubpanel
as="div"
key={task.taskId}
radius="sm"
padding="md"
>
<div className="flex min-w-0 items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-base font-black text-[var(--platform-text-strong)]">
{task.title}
</div>
<div className="mt-1 text-xs font-semibold text-[var(--platform-text-soft)]">
{progressLabel}
</div>
</div>
<div className="shrink-0 text-right">
<div className="text-sm font-black text-[var(--platform-text-strong)]">
+{task.rewardPoints}
</div>
<div className="mt-1 text-[11px] font-semibold text-[var(--platform-text-soft)]">
{getProfileTaskStatusLabel(task.status)}
</div>
</div>
return (
<PlatformSubpanel
as="div"
key={task.taskId}
radius="sm"
padding="md"
>
<div className="flex min-w-0 items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-base font-black text-[var(--platform-text-strong)]">
{task.title}
</div>
<PlatformActionButton
surface="profile"
fullWidth
size="sm"
className="mt-3 disabled:opacity-50"
disabled={!isClaimable || Boolean(claimingTaskId)}
onClick={() => onClaim(task.taskId)}
>
{getProfileTaskClaimButtonLabel(task, isClaiming)}
</PlatformActionButton>
</PlatformSubpanel>
);
})}
</div>
)}
<div className="mt-1 text-xs font-semibold text-[var(--platform-text-soft)]">
{progressLabel}
</div>
</div>
<div className="shrink-0 text-right">
<div className="text-sm font-black text-[var(--platform-text-strong)]">
+{task.rewardPoints}
</div>
<div className="mt-1 text-[11px] font-semibold text-[var(--platform-text-soft)]">
{getProfileTaskStatusLabel(task.status)}
</div>
</div>
</div>
<PlatformActionButton
surface="profile"
fullWidth
size="sm"
className="mt-3 disabled:opacity-50"
disabled={!isClaimable || Boolean(claimingTaskId)}
onClick={() => onClaim(task.taskId)}
>
{getProfileTaskClaimButtonLabel(task, isClaiming)}
</PlatformActionButton>
</PlatformSubpanel>
);
})}
</div>
</div>
</div>
)}
</UnifiedModal>
);
}
@@ -3673,65 +3679,69 @@ function RewardCodeRedeemModal({
onClose: () => void;
}) {
return (
<div className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6">
<div className="platform-recharge-modal w-full max-w-sm overflow-hidden rounded-[1.4rem]">
<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">
<PlatformTextField
value={value}
onChange={(event) => onChange(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
onSubmit();
}
}}
size="sm"
density="roomy"
className="uppercase tracking-normal"
placeholder="输入兑换码"
aria-label="兑换码"
autoFocus
/>
<PlatformActionButton
surface="profile"
fullWidth
size="md"
className="disabled:opacity-50"
onClick={onSubmit}
disabled={isSubmitting || !value.trim()}
>
{isSubmitting ? '兑换中' : '兑换'}
</PlatformActionButton>
{error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
{error}
</PlatformStatusMessage>
) : null}
{success ? (
<PlatformStatusMessage
tone="success"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
{success}
</PlatformStatusMessage>
) : null}
</div>
</div>
</div>
<UnifiedModal
open
title="兑换码"
onClose={onClose}
closeLabel="关闭兑换码"
closeVariant="profile"
closeOnBackdrop={false}
closeOnEscape={false}
portal={false}
size="sm"
zIndexClassName="z-[80]"
overlayClassName={PROFILE_MODAL_OVERLAY_CLASS}
panelClassName="platform-recharge-modal !max-w-sm rounded-[1.4rem]"
headerClassName={PROFILE_MODAL_HEADER_CLASS}
titleClassName={PROFILE_MODAL_TITLE_CLASS}
bodyClassName="space-y-3 px-5 py-5"
>
<PlatformTextField
value={value}
onChange={(event) => onChange(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
onSubmit();
}
}}
size="sm"
density="roomy"
className="uppercase tracking-normal"
placeholder="输入兑换码"
aria-label="兑换码"
autoFocus
/>
<PlatformActionButton
surface="profile"
fullWidth
size="md"
className="disabled:opacity-50"
onClick={onSubmit}
disabled={isSubmitting || !value.trim()}
>
{isSubmitting ? '兑换中' : '兑换'}
</PlatformActionButton>
{error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
{error}
</PlatformStatusMessage>
) : null}
{success ? (
<PlatformStatusMessage
tone="success"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
{success}
</PlatformStatusMessage>
) : null}
</UnifiedModal>
);
}