Merge remote-tracking branch 'origin/master' into codex/wechat-mini-program-virtual-payment

# Conflicts:
#	.hermes/shared-memory/decision-log.md
This commit is contained in:
kdletters
2026-05-27 20:35:32 +08:00
256 changed files with 10164 additions and 6985 deletions

View File

@@ -29,7 +29,6 @@ import {
Star,
ThumbsUp,
Ticket,
UserPlus,
UserRound,
XCircle,
} from 'lucide-react';
@@ -50,7 +49,6 @@ import profileClockImage from '../../../media/profile/_Image (1).png';
import profileGamepadImage from '../../../media/profile/_Image (2).png';
import profileStillLifeImage from '../../../media/profile/_Image (3).png';
import profileCoinsImage from '../../../media/profile/_Image (4).png';
import profileInviteImage from '../../../media/profile/_Image (5).png';
import profileGiftImage from '../../../media/profile/_Image (6).png';
import profileCommunityImage from '../../../media/profile/_Image (7).png';
import profileFeedbackImage from '../../../media/profile/_Image (8).png';
@@ -79,7 +77,6 @@ import type {
WechatMiniProgramVirtualPayParams,
WechatNativePayment,
} from '../../../packages/shared/src/contracts/runtime';
import { isMatch3DDemoProfileId } from '../../data/match3dDemoGalleryCard';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
import type { AuthUser } from '../../services/authService';
@@ -218,6 +215,7 @@ export interface RpgEntryHomeViewProps {
onOpenPlayedWork?: (work: ProfilePlayedWorkSummary) => void;
onOpenFeedback?: () => void;
onRechargeSuccess?: () => void | Promise<void>;
profileTaskRefreshKey?: number;
createTabContent?: ReactNode;
draftTabContent?: ReactNode;
hasUnreadDraftUpdate?: boolean;
@@ -257,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] -
@@ -279,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 = {
@@ -2451,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,
}: {
@@ -3987,6 +3987,7 @@ export function RpgEntryHomeView({
onOpenPlayedWork,
onOpenFeedback,
onRechargeSuccess,
profileTaskRefreshKey = 0,
createTabContent,
draftTabContent,
hasUnreadDraftUpdate = false,
@@ -4028,6 +4029,7 @@ export function RpgEntryHomeView({
useState<ProfileTaskCenterResponse | null>(null);
const [taskCenterError, setTaskCenterError] = useState<string | null>(null);
const [isLoadingTaskCenter, setIsLoadingTaskCenter] = useState(false);
const taskCenterRequestIdRef = useRef(0);
const [claimingTaskId, setClaimingTaskId] = useState<string | null>(null);
const [taskClaimSuccess, setTaskClaimSuccess] = useState<string | null>(null);
const [isQrScannerOpen, setIsQrScannerOpen] = useState(false);
@@ -4047,6 +4049,7 @@ export function RpgEntryHomeView({
: readProfileInviteCodeFromLocationSearch(window.location.search),
[],
);
const promptedLoginForInviteQueryRef = useRef(false);
const autoOpenedInviteQueryRef = useRef(false);
const [referralRedeemCode, setReferralRedeemCode] = useState(
pendingProfileInviteCode,
@@ -4225,12 +4228,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 }>
@@ -4299,19 +4300,13 @@ export function RpgEntryHomeView({
return;
}
const firstCategoryGroup =
categoryGroups.find((group) =>
group.entries.some((entry) => !isMatch3DDemoProfileId(entry.profileId)),
) ?? categoryGroups[0];
const firstCategoryGroup = categoryGroups[0];
const selectedCategoryGroup =
categoryGroups.find((group) => group.tag === selectedCategoryTag) ?? null;
if (
firstCategoryGroup &&
(!selectedCategoryGroup ||
(!hasManualCategoryTagSelectionRef.current &&
selectedCategoryGroup.entries.every((entry) =>
isMatch3DDemoProfileId(entry.profileId),
) &&
firstCategoryGroup.tag !== selectedCategoryGroup.tag))
) {
setSelectedCategoryTag(firstCategoryGroup.tag);
@@ -4397,12 +4392,15 @@ export function RpgEntryHomeView({
return;
}
autoOpenedInviteQueryRef.current = true;
if (!authUi?.user) {
authUi?.openLoginModal();
if (!promptedLoginForInviteQueryRef.current) {
promptedLoginForInviteQueryRef.current = true;
authUi?.openLoginModal();
}
return;
}
autoOpenedInviteQueryRef.current = true;
setReferralRedeemCode(pendingProfileInviteCode);
setReferralError(null);
setReferralSuccess(null);
@@ -4803,23 +4801,49 @@ export function RpgEntryHomeView({
document.removeEventListener('visibilitychange', handleResume);
};
}, [handleWechatPayResult]);
const loadTaskCenter = () => {
const loadTaskCenter = useCallback(() => {
const requestId = ++taskCenterRequestIdRef.current;
setTaskCenterError(null);
setIsLoadingTaskCenter(true);
void getRpgProfileTasks()
.then(setTaskCenter)
.then((center) => {
if (requestId === taskCenterRequestIdRef.current) {
setTaskCenter(center);
}
})
.catch((error: unknown) => {
if (requestId !== taskCenterRequestIdRef.current) {
return;
}
setTaskCenter(null);
setTaskCenterError(
error instanceof Error ? error.message : '读取每日任务失败',
);
})
.finally(() => setIsLoadingTaskCenter(false));
};
.finally(() => {
if (requestId === taskCenterRequestIdRef.current) {
setIsLoadingTaskCenter(false);
}
});
}, []);
useEffect(() => {
if (activeTab !== 'profile' || !isAuthenticated) {
taskCenterRequestIdRef.current += 1;
setTaskCenter(null);
setTaskCenterError(null);
return;
}
loadTaskCenter();
}, [activeTab, isAuthenticated, loadTaskCenter, profileTaskRefreshKey]);
const openTaskCenterPanel = () => {
setIsTaskCenterOpen(true);
setTaskClaimSuccess(null);
loadTaskCenter();
if (!taskCenter) {
loadTaskCenter();
}
};
const openQrScannerPanel = () => {
if (!authUi?.user) {
@@ -5266,7 +5290,6 @@ export function RpgEntryHomeView({
(event: PointerEvent<HTMLElement>) => {
if (
recommendDragCommitDirection ||
!isAuthenticated ||
!activeRecommendEntry ||
recommendedFeedEntries.length <= 1
) {
@@ -5282,7 +5305,6 @@ export function RpgEntryHomeView({
},
[
activeRecommendEntry,
isAuthenticated,
recommendDragCommitDirection,
recommendedFeedEntries.length,
],
@@ -6223,14 +6245,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>
@@ -6240,7 +6272,7 @@ export function RpgEntryHomeView({
className="platform-profile-daily-task-card__mascot"
/>
<span className="platform-profile-daily-task-card__action">
{profileTaskCardSummary.actionLabel}
</span>
</button>
@@ -6256,13 +6288,6 @@ export function RpgEntryHomeView({
imageSrc={profileCoinsImage}
onClick={openRechargeOrRewardCodeModal}
/>
<ProfileShortcutButton
label="邀请好友"
subLabel="双方得 30 泥点"
icon={UserPlus}
imageSrc={profileInviteImage}
onClick={() => openProfilePopupPanel('invite')}
/>
<ProfileShortcutButton
label="兑换码"
subLabel="领取福利"
@@ -6305,20 +6330,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} />
</>
) : (