feat: surface platform errors in copyable dialogs

This commit is contained in:
kdletters
2026-05-26 14:27:18 +08:00
parent 44c65df5c9
commit fbda614156
16 changed files with 715 additions and 191 deletions

View File

@@ -255,18 +255,25 @@ const RECOMMEND_ENTRY_DRAG_LIMIT_PX = 160;
const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
const WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS = [800, 1600, 3000] as const;
const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180;
const PROFILE_TASK_STATUS_PRIORITY_RANK: Record<ProfileTaskItem['status'], number> = {
const PROFILE_TASK_STATUS_PRIORITY_RANK: Record<
ProfileTaskItem['status'],
number
> = {
claimable: 2,
incomplete: 1,
disabled: 0,
claimed: -1,
};
const PROFILE_TASK_CARD_FALLBACK_REWARD_POINTS = 10;
const PROFILE_QR_SCAN_INTERVAL_MS = 360;
function selectProfileTaskCenterTasks(tasks: ProfileTaskItem[]) {
return tasks
.map((task, index) => ({ task, index }))
.filter(({ task }) => task.status === 'claimable' || task.status === 'incomplete')
.filter(
({ task }) =>
task.status === 'claimable' || task.status === 'incomplete',
)
.sort(
(left, right) =>
PROFILE_TASK_STATUS_PRIORITY_RANK[right.task.status] -
@@ -277,6 +284,37 @@ function selectProfileTaskCenterTasks(tasks: ProfileTaskItem[]) {
.map(({ task }) => task);
}
function selectProfileTaskCardTask(tasks: ProfileTaskItem[]) {
return (
selectProfileTaskCenterTasks(tasks)[0] ??
tasks.find((task) => task.status === 'claimed') ??
tasks.find((task) => task.status !== 'disabled') ??
null
);
}
function buildProfileTaskCardSummary(center: ProfileTaskCenterResponse | null) {
const task = selectProfileTaskCardTask(center?.tasks ?? []);
const threshold = Math.max(1, task?.threshold ?? 1);
const progressCount = Math.min(task?.progressCount ?? 0, threshold);
const rewardPoints =
task?.rewardPoints ?? PROFILE_TASK_CARD_FALLBACK_REWARD_POINTS;
const actionLabel =
task?.status === 'claimable'
? '领取'
: task?.status === 'claimed'
? '已完成'
: '去完成';
return {
actionLabel,
progressCount,
progressPercent: Math.round((progressCount / threshold) * 100),
rewardPoints,
threshold,
};
}
type ProfileReferralPanel = 'invite' | 'redeem' | 'community';
type ProfilePopupPanel = ProfileReferralPanel | 'saveArchives';
type BarcodeDetectorLike = {
@@ -2449,42 +2487,6 @@ function ProfileSettingsRow({
);
}
function ProfileSecondaryShortcutButton({
label,
subLabel,
icon,
onClick,
}: {
label: string;
subLabel?: string;
icon: ComponentType<{ className?: string }>;
onClick: () => void;
}) {
const Icon = icon;
return (
<button
type="button"
onClick={onClick}
className="platform-profile-secondary-shortcut inline-flex items-center gap-2 rounded-full px-3 py-2 text-left"
>
<span className="platform-profile-secondary-shortcut__icon">
<Icon className="h-4 w-4" />
</span>
<span className="min-w-0">
<span className="block truncate text-[13px] font-semibold text-[var(--platform-text-strong)]">
{label}
</span>
{subLabel ? (
<span className="mt-0.5 block truncate text-[11px] font-medium text-[var(--platform-text-soft)]">
{subLabel}
</span>
) : null}
</span>
</button>
);
}
function ProfileLegalSection({
onOpenDocument,
}: {
@@ -4218,12 +4220,10 @@ export function RpgEntryHomeView({
profileDashboard?.totalPlayTimeMs ?? 0,
);
const playedWorkCount = profileDashboard?.playedWorldCount ?? 0;
const canShowReferralRedeemShortcut =
isAuthenticated &&
isWithinProfileInviteRedeemWindow(authUi?.user?.createdAt) &&
isReferralCenterInitialized &&
Boolean(referralCenter) &&
referralCenter?.hasRedeemedCode !== true;
const profileTaskCardSummary = useMemo(
() => buildProfileTaskCardSummary(taskCenter),
[taskCenter],
);
const tabIcons: Record<
PlatformHomeTab,
ComponentType<{ className?: string }>
@@ -4776,7 +4776,7 @@ export function RpgEntryHomeView({
document.removeEventListener('visibilitychange', handleResume);
};
}, [handleWechatPayResult]);
const loadTaskCenter = () => {
const loadTaskCenter = useCallback(() => {
setTaskCenterError(null);
setIsLoadingTaskCenter(true);
void getRpgProfileTasks()
@@ -4788,11 +4788,24 @@ export function RpgEntryHomeView({
);
})
.finally(() => setIsLoadingTaskCenter(false));
};
}, []);
useEffect(() => {
if (activeTab !== 'profile' || !isAuthenticated) {
setTaskCenter(null);
setTaskCenterError(null);
return;
}
loadTaskCenter();
}, [activeTab, isAuthenticated, loadTaskCenter]);
const openTaskCenterPanel = () => {
setIsTaskCenterOpen(true);
setTaskClaimSuccess(null);
loadTaskCenter();
if (!taskCenter) {
loadTaskCenter();
}
};
const openQrScannerPanel = () => {
if (!authUi?.user) {
@@ -6185,14 +6198,24 @@ export function RpgEntryHomeView({
</span>
<span className="platform-profile-daily-task-card__desc mt-4 block text-[13px] font-medium text-[var(--platform-text-base)]">
<span className="text-[#c45b2a]">10</span>
{' '}
<span className="text-[#c45b2a]">
{profileTaskCardSummary.rewardPoints}
</span>{' '}
</span>
<span className="platform-profile-daily-task-card__progress mt-4 flex items-center gap-3">
<span className="platform-profile-daily-task-card__progress-value text-[14px] font-semibold text-[#dc3f0e]">
0 / 1
{profileTaskCardSummary.progressCount} /{' '}
{profileTaskCardSummary.threshold}
</span>
<span className="platform-profile-daily-task-card__track">
<span className="platform-profile-daily-task-card__bar" />
<span
className="platform-profile-daily-task-card__bar"
style={{
width: `${profileTaskCardSummary.progressPercent}%`,
}}
/>
</span>
</span>
</span>
@@ -6202,7 +6225,7 @@ export function RpgEntryHomeView({
className="platform-profile-daily-task-card__mascot"
/>
<span className="platform-profile-daily-task-card__action">
{profileTaskCardSummary.actionLabel}
</span>
</button>
@@ -6267,20 +6290,6 @@ export function RpgEntryHomeView({
/>
</section>
{canShowReferralRedeemShortcut ? (
<section
className="platform-profile-secondary-shortcuts"
aria-label="次级入口"
>
<ProfileSecondaryShortcutButton
label="填邀请码"
subLabel="新用户奖励"
icon={Ticket}
onClick={() => openProfilePopupPanel('redeem')}
/>
</section>
) : null}
<ProfileLegalSection onOpenDocument={setActiveLegalDocumentId} />
</>
) : (