Merge branch 'master' into codex/puzzle-clear-template-runtime-fixes
# Conflicts: # .hermes/shared-memory/decision-log.md # .hermes/shared-memory/project-overview.md # docs/【开发运维】本地开发验证与生产运维-2026-05-15.md # scripts/dev.test.ts # server-rs/crates/api-server/src/creation_entry_config.rs # server-rs/crates/api-server/src/wooden_fish.rs # server-rs/crates/module-auth/src/lib.rs # server-rs/crates/spacetime-client/src/wooden_fish.rs # server-rs/crates/spacetime-module/src/auth/procedures.rs # src/components/custom-world-home/creationWorkShelf.ts # src/components/platform-entry/PlatformEntryFlowShellImpl.tsx # src/components/rpg-entry/rpgEntryWorldPresentation.ts # src/services/miniGameDraftGenerationProgress.test.ts # src/services/miniGameDraftGenerationProgress.ts
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
Heart,
|
||||
LogIn,
|
||||
MessageCircle,
|
||||
Loader2,
|
||||
Palette,
|
||||
Pencil,
|
||||
Plus,
|
||||
@@ -74,6 +75,7 @@ import type {
|
||||
ProfileWalletLedgerResponse,
|
||||
RedeemProfileRewardCodeResponse,
|
||||
WechatMiniProgramPayParams,
|
||||
WechatMiniProgramVirtualPayParams,
|
||||
WechatNativePayment,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
@@ -86,10 +88,10 @@ import {
|
||||
} from '../../services/authService';
|
||||
import { copyTextToClipboard } from '../../services/clipboard';
|
||||
import {
|
||||
resolveProfileRechargePaymentChannel,
|
||||
resolveProfileRechargeProductPaymentChannel,
|
||||
shouldShowRechargeEntry,
|
||||
WECHAT_H5_PAYMENT_CHANNEL,
|
||||
WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL,
|
||||
WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL,
|
||||
WECHAT_NATIVE_PAYMENT_CHANNEL,
|
||||
} from '../../services/payment/paymentPlatform';
|
||||
import { redirectToPaymentUrl } from '../../services/payment/paymentRedirect';
|
||||
@@ -103,6 +105,7 @@ import {
|
||||
getRpgProfileWalletLedger,
|
||||
redeemRpgProfileReferralInviteCode,
|
||||
redeemRpgProfileRewardCode,
|
||||
watchWechatRpgProfileRechargeOrder,
|
||||
} from '../../services/rpg-entry/rpgProfileClient';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
@@ -329,6 +332,7 @@ type WechatPayResult = {
|
||||
requestId: string;
|
||||
orderId: string | null;
|
||||
status: WechatMiniProgramPaymentStatus;
|
||||
errorMessage: string | null;
|
||||
};
|
||||
type RechargePaymentResultKind = 'success' | 'pending' | 'cancel' | 'failed';
|
||||
type RechargePaymentResult = {
|
||||
@@ -336,6 +340,11 @@ type RechargePaymentResult = {
|
||||
title: string;
|
||||
message: string;
|
||||
};
|
||||
type WechatRechargeOrderConfirmationState = {
|
||||
orderId: string;
|
||||
};
|
||||
const WECHAT_PAY_RESULT_RECHECK_INTERVAL_MS = 250;
|
||||
const WECHAT_PAY_RESULT_RECHECK_TIMEOUT_MS = 10000;
|
||||
|
||||
function getBarcodeDetectorConstructor(): BarcodeDetectorConstructorLike | null {
|
||||
const maybeDetector = (globalThis as unknown as {
|
||||
@@ -1201,6 +1210,7 @@ function PlatformTabButton({
|
||||
onClick,
|
||||
emphasized = false,
|
||||
showDot = false,
|
||||
disabled = false,
|
||||
}: {
|
||||
active: boolean;
|
||||
label: string;
|
||||
@@ -1208,6 +1218,7 @@ function PlatformTabButton({
|
||||
onClick: () => void;
|
||||
emphasized?: boolean;
|
||||
showDot?: boolean;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const ariaLabel = showDot ? `${label},有新草稿` : label;
|
||||
|
||||
@@ -1215,8 +1226,9 @@ function PlatformTabButton({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel}
|
||||
className={`platform-bottom-nav__button ${emphasized ? 'platform-bottom-nav__button--primary' : ''} ${active ? 'platform-bottom-nav__button--active' : ''}`}
|
||||
className={`platform-bottom-nav__button ${emphasized ? 'platform-bottom-nav__button--primary' : ''} ${active ? 'platform-bottom-nav__button--active' : ''} disabled:cursor-not-allowed disabled:opacity-55`}
|
||||
>
|
||||
<span className="platform-bottom-nav__button-content">
|
||||
<span
|
||||
@@ -1246,6 +1258,7 @@ function DesktopTabButton({
|
||||
onClick,
|
||||
emphasized = false,
|
||||
showDot = false,
|
||||
disabled = false,
|
||||
}: {
|
||||
active: boolean;
|
||||
label: string;
|
||||
@@ -1253,6 +1266,7 @@ function DesktopTabButton({
|
||||
onClick: () => void;
|
||||
emphasized?: boolean;
|
||||
showDot?: boolean;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const ariaLabel = showDot ? `${label},有新草稿` : label;
|
||||
|
||||
@@ -1260,8 +1274,9 @@ function DesktopTabButton({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel}
|
||||
className={`platform-desktop-rail__button ${emphasized ? 'platform-desktop-rail__button--primary' : ''} ${active ? 'platform-desktop-rail__button--active' : ''}`}
|
||||
className={`platform-desktop-rail__button ${emphasized ? 'platform-desktop-rail__button--primary' : ''} ${active ? 'platform-desktop-rail__button--active' : ''} disabled:cursor-not-allowed disabled:opacity-55`}
|
||||
>
|
||||
<span className="platform-desktop-rail__icon-shell">
|
||||
<Icon className="platform-desktop-rail__icon h-[1.1rem] w-[1.1rem]" />
|
||||
@@ -2434,7 +2449,7 @@ function ProfileShortcutButton({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick ?? undefined}
|
||||
className="platform-profile-shortcut-button flex min-h-[5.25rem] flex-col items-center justify-center gap-2 px-2.5 py-3 text-center transition"
|
||||
className="platform-profile-shortcut-button flex min-h-[5.25rem] w-full flex-col items-center justify-center gap-2 px-2.5 py-3 text-center transition"
|
||||
>
|
||||
<div className="platform-profile-shortcut-button__icon">
|
||||
{imageSrc ? (
|
||||
@@ -2679,22 +2694,34 @@ function readWechatPayResultFromHash(): WechatPayResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [requestId = '', rawStatus = ''] = result.split(':');
|
||||
const orderId = requestId
|
||||
const [requestId = '', rawStatus = '', explicitOrderId = '', ...rawErrors] =
|
||||
result.split(':');
|
||||
const inferredOrderId = requestId
|
||||
.replace(/^wechat_pay_/, '')
|
||||
.replace(/_\d+$/, '')
|
||||
.trim();
|
||||
const orderId = explicitOrderId.trim() || inferredOrderId;
|
||||
const status =
|
||||
rawStatus === 'success'
|
||||
? 'success'
|
||||
: rawStatus === 'cancel'
|
||||
? 'cancel'
|
||||
: 'fail';
|
||||
let errorMessage: string | null = null;
|
||||
const rawError = rawErrors.join(':');
|
||||
if (rawError) {
|
||||
try {
|
||||
errorMessage = decodeURIComponent(rawError);
|
||||
} catch (_error) {
|
||||
errorMessage = rawError;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
requestId,
|
||||
orderId: orderId || null,
|
||||
status,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2739,7 +2766,11 @@ function loadWechatJsSdk() {
|
||||
}
|
||||
|
||||
async function requestWechatMiniProgramPayment(
|
||||
payload: WechatMiniProgramPayParams | null | undefined,
|
||||
payload:
|
||||
| WechatMiniProgramPayParams
|
||||
| WechatMiniProgramVirtualPayParams
|
||||
| null
|
||||
| undefined,
|
||||
orderId: string,
|
||||
): Promise<void> {
|
||||
if (!payload) {
|
||||
@@ -2781,7 +2812,7 @@ async function confirmWechatRechargeOrderUntilSettled(
|
||||
orderId: string,
|
||||
): Promise<ConfirmWechatProfileRechargeOrderResponse> {
|
||||
let latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
|
||||
if (latestResponse.order.status === 'paid') {
|
||||
if (latestResponse.order.status !== 'pending') {
|
||||
return latestResponse;
|
||||
}
|
||||
|
||||
@@ -2789,12 +2820,17 @@ async function confirmWechatRechargeOrderUntilSettled(
|
||||
await waitWechatPayConfirmDelay(delayMs);
|
||||
|
||||
latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
|
||||
if (latestResponse.order.status === 'paid') {
|
||||
if (latestResponse.order.status !== 'pending') {
|
||||
return latestResponse;
|
||||
}
|
||||
}
|
||||
|
||||
return latestResponse;
|
||||
try {
|
||||
const streamedResponse = await watchWechatRpgProfileRechargeOrder(orderId);
|
||||
return streamedResponse;
|
||||
} catch {
|
||||
return latestResponse;
|
||||
}
|
||||
}
|
||||
|
||||
function useWechatNativeQrCode(codeUrl: string | null) {
|
||||
@@ -3079,6 +3115,35 @@ function RechargePaymentResultModal({
|
||||
);
|
||||
}
|
||||
|
||||
function RechargePaymentConfirmationMask({
|
||||
orderId,
|
||||
}: {
|
||||
orderId: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="platform-modal-backdrop fixed inset-0 z-[95] flex items-center justify-center px-4 py-6">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="正在确认支付"
|
||||
className="platform-modal-shell platform-remap-surface w-full max-w-sm overflow-hidden rounded-[1.4rem]"
|
||||
>
|
||||
<div className="px-5 pb-5 pt-6 text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-white/10 text-[var(--platform-accent)]">
|
||||
<Loader2 className="h-8 w-8 animate-spin" aria-hidden="true" />
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WalletLedgerModal({
|
||||
ledger,
|
||||
fallbackBalance,
|
||||
@@ -4005,6 +4070,8 @@ export function RpgEntryHomeView({
|
||||
const [rechargeError, setRechargeError] = useState<string | null>(null);
|
||||
const [rechargePaymentResult, setRechargePaymentResult] =
|
||||
useState<RechargePaymentResult | null>(null);
|
||||
const [wechatRechargeOrderConfirmationState, setWechatRechargeOrderConfirmationState] =
|
||||
useState<WechatRechargeOrderConfirmationState | null>(null);
|
||||
const [nativeWechatPayment, setNativeWechatPayment] =
|
||||
useState<NativeWechatPaymentState | null>(null);
|
||||
const [activeRechargeTab, setActiveRechargeTab] =
|
||||
@@ -4085,6 +4152,7 @@ export function RpgEntryHomeView({
|
||||
const profileCopyResetTimerRef = useRef<number | null>(null);
|
||||
const avatarFileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const pendingWechatRechargeOrderIdRef = useRef<string | null>(null);
|
||||
const confirmingWechatRechargeOrderIdRef = useRef<string | null>(null);
|
||||
const [isNicknameModalOpen, setIsNicknameModalOpen] = useState(false);
|
||||
const [nicknameInput, setNicknameInput] = useState('');
|
||||
const [nicknameError, setNicknameError] = useState<string | null>(null);
|
||||
@@ -4575,12 +4643,14 @@ export function RpgEntryHomeView({
|
||||
loadRechargeCenter();
|
||||
setSubmittingRechargeProductId(null);
|
||||
pendingWechatRechargeOrderIdRef.current = null;
|
||||
confirmingWechatRechargeOrderIdRef.current = null;
|
||||
setWechatRechargeOrderConfirmationState(null);
|
||||
setNativeWechatPayment(null);
|
||||
}, [loadRechargeCenter]);
|
||||
const handleWechatPayResult = useCallback(() => {
|
||||
const payResult = readWechatPayResultFromHash();
|
||||
if (!payResult) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -4588,68 +4658,131 @@ export function RpgEntryHomeView({
|
||||
payResult.orderId &&
|
||||
payResult.orderId !== pendingWechatRechargeOrderIdRef.current
|
||||
) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payResult.status === 'success') {
|
||||
setRechargePaymentResult({
|
||||
kind: 'pending',
|
||||
title: '支付已提交',
|
||||
message: '正在确认到账状态,请稍后查看余额或会员状态。',
|
||||
});
|
||||
if (payResult.orderId) {
|
||||
void confirmWechatRechargeOrderUntilSettled(payResult.orderId)
|
||||
.then((response) => {
|
||||
const isPaid = response.order.status === 'paid';
|
||||
setRechargeCenter(response.center);
|
||||
setRechargePaymentResult(
|
||||
isPaid
|
||||
? {
|
||||
kind: 'success',
|
||||
title: '支付成功',
|
||||
message: '已到账,账户状态已刷新。',
|
||||
}
|
||||
: {
|
||||
kind: 'pending',
|
||||
title: '支付已提交',
|
||||
message: '正在等待微信支付确认,请稍后查看账户状态。',
|
||||
},
|
||||
);
|
||||
if (isPaid) {
|
||||
void onRechargeSuccess?.();
|
||||
}
|
||||
setSubmittingRechargeProductId(null);
|
||||
pendingWechatRechargeOrderIdRef.current = null;
|
||||
})
|
||||
.catch(() => {
|
||||
setRechargePaymentResult({
|
||||
kind: 'pending',
|
||||
title: '支付已提交',
|
||||
message: '暂时没能确认到账状态,请稍后查看余额或会员状态。',
|
||||
});
|
||||
refreshRechargeState();
|
||||
});
|
||||
} else {
|
||||
refreshRechargeState();
|
||||
const orderId = payResult.orderId || pendingWechatRechargeOrderIdRef.current;
|
||||
if (!orderId) {
|
||||
clearWechatPayResultHash();
|
||||
return true;
|
||||
}
|
||||
if (confirmingWechatRechargeOrderIdRef.current === orderId) {
|
||||
clearWechatPayResultHash();
|
||||
return true;
|
||||
}
|
||||
confirmingWechatRechargeOrderIdRef.current = orderId;
|
||||
setWechatRechargeOrderConfirmationState({ orderId });
|
||||
setSubmittingRechargeProductId(null);
|
||||
setRechargePaymentResult(null);
|
||||
void confirmWechatRechargeOrderUntilSettled(orderId)
|
||||
.then((response) => {
|
||||
const isPaid = response.order.status === 'paid';
|
||||
setRechargeCenter(response.center);
|
||||
pendingWechatRechargeOrderIdRef.current = null;
|
||||
confirmingWechatRechargeOrderIdRef.current = null;
|
||||
setWechatRechargeOrderConfirmationState(null);
|
||||
setRechargePaymentResult(
|
||||
isPaid
|
||||
? {
|
||||
kind: 'success',
|
||||
title: '支付成功',
|
||||
message: '已到账,账户状态已刷新。',
|
||||
}
|
||||
: {
|
||||
kind: 'pending',
|
||||
title: '支付处理中',
|
||||
message: '正在等待到账状态确认,请稍后查看余额或会员状态。',
|
||||
},
|
||||
);
|
||||
if (isPaid) {
|
||||
void onRechargeSuccess?.();
|
||||
}
|
||||
clearWechatPayResultHash();
|
||||
})
|
||||
.catch(() => {
|
||||
confirmingWechatRechargeOrderIdRef.current = null;
|
||||
setWechatRechargeOrderConfirmationState(null);
|
||||
setRechargePaymentResult({
|
||||
kind: 'pending',
|
||||
title: '支付处理中',
|
||||
message: '暂时没能确认到账状态,请稍后查看余额或会员状态。',
|
||||
});
|
||||
clearWechatPayResultHash();
|
||||
});
|
||||
} else if (payResult.status === 'cancel') {
|
||||
setRechargePaymentResult({
|
||||
kind: 'cancel',
|
||||
title: '支付已取消',
|
||||
message: '本次没有扣款,账户状态未发生变化。',
|
||||
});
|
||||
setWechatRechargeOrderConfirmationState(null);
|
||||
refreshRechargeState();
|
||||
} else {
|
||||
const detail = payResult.errorMessage
|
||||
? `微信返回:${payResult.errorMessage}`
|
||||
: '微信支付没有完成,本次不会入账。';
|
||||
setRechargePaymentResult({
|
||||
kind: 'failed',
|
||||
title: '支付未完成',
|
||||
message: '微信支付没有完成,本次不会入账。',
|
||||
message: detail,
|
||||
});
|
||||
setWechatRechargeOrderConfirmationState(null);
|
||||
refreshRechargeState();
|
||||
}
|
||||
|
||||
clearWechatPayResultHash();
|
||||
return true;
|
||||
}, [onRechargeSuccess, refreshRechargeState]);
|
||||
const pollWechatPayResultFromHash = useCallback(
|
||||
() => handleWechatPayResult(),
|
||||
[handleWechatPayResult],
|
||||
);
|
||||
const confirmPendingWechatRechargeOrder = useCallback(() => {
|
||||
const orderId = pendingWechatRechargeOrderIdRef.current;
|
||||
if (!orderId || confirmingWechatRechargeOrderIdRef.current === orderId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
confirmingWechatRechargeOrderIdRef.current = orderId;
|
||||
setWechatRechargeOrderConfirmationState({ orderId });
|
||||
setRechargePaymentResult(null);
|
||||
void confirmWechatRechargeOrderUntilSettled(orderId)
|
||||
.then((response) => {
|
||||
const isPaid = response.order.status === 'paid';
|
||||
setRechargeCenter(response.center);
|
||||
pendingWechatRechargeOrderIdRef.current = null;
|
||||
confirmingWechatRechargeOrderIdRef.current = null;
|
||||
setWechatRechargeOrderConfirmationState(null);
|
||||
setSubmittingRechargeProductId(null);
|
||||
setRechargePaymentResult(
|
||||
isPaid
|
||||
? {
|
||||
kind: 'success',
|
||||
title: '支付成功',
|
||||
message: '已到账,账户状态已刷新。',
|
||||
}
|
||||
: {
|
||||
kind: 'pending',
|
||||
title: '支付处理中',
|
||||
message: '正在等待到账状态确认,请稍后查看余额或会员状态。',
|
||||
},
|
||||
);
|
||||
if (isPaid) {
|
||||
void onRechargeSuccess?.();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
confirmingWechatRechargeOrderIdRef.current = null;
|
||||
setWechatRechargeOrderConfirmationState(null);
|
||||
setRechargePaymentResult({
|
||||
kind: 'pending',
|
||||
title: '支付处理中',
|
||||
message: '暂时没能确认到账状态,请稍后查看余额或会员状态。',
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}, [onRechargeSuccess]);
|
||||
const openRechargeModal = () => {
|
||||
if (!authUi?.user) {
|
||||
authUi?.openLoginModal();
|
||||
@@ -4672,20 +4805,24 @@ export function RpgEntryHomeView({
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentChannel = resolveProfileRechargePaymentChannel();
|
||||
const paymentChannel = resolveProfileRechargeProductPaymentChannel(
|
||||
{ kind: product.kind },
|
||||
{},
|
||||
);
|
||||
setSubmittingRechargeProductId(product.productId);
|
||||
setRechargeError(null);
|
||||
setRechargePaymentResult(null);
|
||||
setWechatRechargeOrderConfirmationState(null);
|
||||
setNativeWechatPayment(null);
|
||||
void createRpgProfileRechargeOrder(product.productId, paymentChannel)
|
||||
.then(async (response) => {
|
||||
if (paymentChannel === WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL) {
|
||||
if (paymentChannel === WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL) {
|
||||
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
|
||||
setRechargeCenter(response.center);
|
||||
await requestWechatMiniProgramPayment(
|
||||
response.wechatMiniProgramPayParams,
|
||||
response.order.orderId,
|
||||
);
|
||||
setRechargeCenter(response.center);
|
||||
return;
|
||||
}
|
||||
if (paymentChannel === WECHAT_H5_PAYMENT_CHANNEL) {
|
||||
@@ -4787,22 +4924,67 @@ export function RpgEntryHomeView({
|
||||
.finally(() => setSubmittingRechargeProductId(null));
|
||||
}, [nativeWechatPayment, onRechargeSuccess]);
|
||||
useEffect(() => {
|
||||
const handleResume = () => {
|
||||
const handleHashChange = () => {
|
||||
handleWechatPayResult();
|
||||
};
|
||||
const handleResume = () => {
|
||||
if (
|
||||
typeof document !== 'undefined' &&
|
||||
document.visibilityState === 'hidden'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!handleWechatPayResult()) {
|
||||
confirmPendingWechatRechargeOrder();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('hashchange', handleResume);
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
window.addEventListener('focus', handleResume);
|
||||
window.addEventListener('pageshow', handleResume);
|
||||
document.addEventListener('visibilitychange', handleResume);
|
||||
handleResume();
|
||||
handleWechatPayResult();
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', handleResume);
|
||||
window.removeEventListener('hashchange', handleHashChange);
|
||||
window.removeEventListener('focus', handleResume);
|
||||
window.removeEventListener('pageshow', handleResume);
|
||||
document.removeEventListener('visibilitychange', handleResume);
|
||||
};
|
||||
}, [handleWechatPayResult]);
|
||||
}, [confirmPendingWechatRechargeOrder, handleWechatPayResult]);
|
||||
useEffect(() => {
|
||||
if (!submittingRechargeProductId || wechatRechargeOrderConfirmationState) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
let timer: number | null = null;
|
||||
const pollPayResult = () => {
|
||||
if (pollWechatPayResultFromHash()) {
|
||||
return;
|
||||
}
|
||||
if (Date.now() - startedAt >= WECHAT_PAY_RESULT_RECHECK_TIMEOUT_MS) {
|
||||
return;
|
||||
}
|
||||
timer = window.setTimeout(
|
||||
pollPayResult,
|
||||
WECHAT_PAY_RESULT_RECHECK_INTERVAL_MS,
|
||||
);
|
||||
};
|
||||
|
||||
timer = window.setTimeout(
|
||||
pollPayResult,
|
||||
WECHAT_PAY_RESULT_RECHECK_INTERVAL_MS,
|
||||
);
|
||||
return () => {
|
||||
if (timer !== null) {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
pollWechatPayResultFromHash,
|
||||
submittingRechargeProductId,
|
||||
wechatRechargeOrderConfirmationState,
|
||||
]);
|
||||
const loadTaskCenter = useCallback(() => {
|
||||
const requestId = ++taskCenterRequestIdRef.current;
|
||||
setTaskCenterError(null);
|
||||
@@ -6042,6 +6224,11 @@ export function RpgEntryHomeView({
|
||||
<div className={MOBILE_PAGE_STAGE_CLASS}>
|
||||
<section>
|
||||
<SectionHeader title="我的创作" detail="草稿与已发布" />
|
||||
{platformError ? (
|
||||
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
|
||||
{platformError}
|
||||
</div>
|
||||
) : null}
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取你的作品..." />
|
||||
) : myEntries.length > 0 ? (
|
||||
@@ -6078,7 +6265,16 @@ export function RpgEntryHomeView({
|
||||
const createContent: ReactNode =
|
||||
createTabContent ?? fallbackCreateStartContent;
|
||||
|
||||
const savesContent: ReactNode = draftTabContent ?? fallbackDraftContent;
|
||||
const savesContent: ReactNode = (
|
||||
<>
|
||||
{platformError ? (
|
||||
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
|
||||
{platformError}
|
||||
</div>
|
||||
) : null}
|
||||
{draftTabContent ?? fallbackDraftContent}
|
||||
</>
|
||||
);
|
||||
|
||||
const profileContent: ReactNode = (
|
||||
<div className={`${MOBILE_PROFILE_PAGE_STAGE_CLASS} platform-profile-page`}>
|
||||
@@ -6292,7 +6488,7 @@ export function RpgEntryHomeView({
|
||||
className="platform-profile-shortcut-panel"
|
||||
aria-label="常用功能"
|
||||
>
|
||||
<div className="platform-profile-shortcut-grid">
|
||||
<div className="platform-profile-shortcut-grid grid w-full !grid-cols-4">
|
||||
<ProfileShortcutButton
|
||||
label="泥点充值"
|
||||
subLabel="充值泥点"
|
||||
@@ -6796,6 +6992,15 @@ export function RpgEntryHomeView({
|
||||
onClose={() => setRechargePaymentResult(null)}
|
||||
/>
|
||||
) : null;
|
||||
const rechargePaymentConfirmationMask: ReactNode =
|
||||
wechatRechargeOrderConfirmationState ? (
|
||||
<RechargePaymentConfirmationMask
|
||||
orderId={wechatRechargeOrderConfirmationState.orderId}
|
||||
/>
|
||||
) : null;
|
||||
const isRechargePaymentConfirmationPending = Boolean(
|
||||
wechatRechargeOrderConfirmationState,
|
||||
);
|
||||
const categoryFilterDialog: ReactNode = isCategoryFilterPanelOpen ? (
|
||||
<PlatformCategoryFilterDialog
|
||||
kindFilter={categoryKindFilter}
|
||||
@@ -6831,93 +7036,100 @@ export function RpgEntryHomeView({
|
||||
const isMobileRecommendTab = activeTab === 'home';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`platform-mobile-entry-shell ${isMobileRecommendTab ? 'platform-mobile-entry-shell--recommend' : ''} flex h-full min-h-0 min-w-0 flex-col overflow-hidden`}
|
||||
>
|
||||
{!isMobileRecommendTab ? (
|
||||
<div className="platform-mobile-topbar mb-3 flex shrink-0 items-center justify-between gap-3 px-0.5">
|
||||
<RpgEntryBrandLogo />
|
||||
{isAuthenticated && activeTab === 'profile' ? (
|
||||
<div className="flex items-center gap-2.5">
|
||||
<>
|
||||
<div
|
||||
inert={isRechargePaymentConfirmationPending ? true : undefined}
|
||||
className={`platform-mobile-entry-shell ${isMobileRecommendTab ? 'platform-mobile-entry-shell--recommend' : ''} flex h-full min-h-0 min-w-0 flex-col overflow-hidden`}
|
||||
>
|
||||
{!isMobileRecommendTab ? (
|
||||
<div className="platform-mobile-topbar mb-3 flex shrink-0 items-center justify-between gap-3 px-0.5">
|
||||
<RpgEntryBrandLogo />
|
||||
{isAuthenticated && activeTab === 'profile' ? (
|
||||
<div className="flex items-center gap-2.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openQrScannerPanel}
|
||||
className="platform-profile-header__icon-button"
|
||||
aria-label="扫码"
|
||||
>
|
||||
<ScanLine className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => authUi?.openSettingsModal()}
|
||||
className="platform-profile-header__icon-button"
|
||||
aria-label="打开设置"
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
) : isAuthenticated &&
|
||||
(activeTab === 'create' || activeTab === 'saves') ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openQrScannerPanel}
|
||||
className="platform-profile-header__icon-button"
|
||||
aria-label="扫码"
|
||||
onClick={openRechargeOrRewardCodeModal}
|
||||
className="platform-mobile-create-wallet-chip inline-flex shrink-0 items-center gap-1.5 rounded-full border border-[#f0cfae] bg-[#fff5eb] px-2.5 py-1.5 text-xs font-black text-[#b65f2c] shadow-[0_10px_22px_rgba(174,111,73,0.12)]"
|
||||
aria-label={`${formatDashboardCount(remainingNarrativeCoins)}泥点`}
|
||||
>
|
||||
<ScanLine className="h-5 w-5" />
|
||||
<span className="grid h-6 w-6 place-items-center rounded-full bg-[#ffe0ab] text-[#cf7b34]">
|
||||
<Coins className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<span>{formatDashboardCount(remainingNarrativeCoins)}泥点</span>
|
||||
</button>
|
||||
) : !isAuthenticated ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => authUi?.openSettingsModal()}
|
||||
className="platform-profile-header__icon-button"
|
||||
aria-label="打开设置"
|
||||
onClick={openUserSurface}
|
||||
className="platform-button platform-button--primary shrink-0 px-3 py-2 text-xs"
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
<LogIn className="h-3.5 w-3.5" />
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
) : isAuthenticated && activeTab === 'create' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openUserSurface}
|
||||
className="platform-mobile-create-wallet-chip inline-flex shrink-0 items-center gap-1.5 rounded-full border border-[#f0cfae] bg-[#fff5eb] px-2.5 py-1.5 text-xs font-black text-[#b65f2c] shadow-[0_10px_22px_rgba(174,111,73,0.12)]"
|
||||
aria-label={`${formatDashboardCount(remainingNarrativeCoins)}泥点`}
|
||||
>
|
||||
<span className="grid h-6 w-6 place-items-center rounded-full bg-[#ffe0ab] text-[#cf7b34]">
|
||||
<Coins className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<span>{formatDashboardCount(remainingNarrativeCoins)}泥点</span>
|
||||
</button>
|
||||
) : !isAuthenticated ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openUserSurface}
|
||||
className="platform-button platform-button--primary shrink-0 px-3 py-2 text-xs"
|
||||
>
|
||||
<LogIn className="h-3.5 w-3.5" />
|
||||
登录
|
||||
</button>
|
||||
) : null}
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="platform-tab-panel-stack min-w-0 flex-1">
|
||||
{tabPanels}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="platform-tab-panel-stack min-w-0 flex-1">
|
||||
{tabPanels}
|
||||
</div>
|
||||
|
||||
<div className="platform-mobile-bottom-dock min-w-0 shrink-0">
|
||||
<div
|
||||
className={`platform-bottom-nav grid ${visibleTabs.length === 5 ? 'grid-cols-5' : visibleTabs.length === 4 ? 'grid-cols-4' : visibleTabs.length === 3 ? 'grid-cols-3' : 'grid-cols-2'}`}
|
||||
>
|
||||
{visibleTabs.map((tab) => (
|
||||
<PlatformTabButton
|
||||
key={tab}
|
||||
active={activeTab === tab}
|
||||
label={
|
||||
activeTab === 'home' && tab === 'home'
|
||||
? '下一个'
|
||||
: tabLabels[tab]
|
||||
}
|
||||
icon={
|
||||
activeTab === 'home' && tab === 'home'
|
||||
? ChevronDown
|
||||
: tabIcons[tab]
|
||||
}
|
||||
emphasized={tab === 'create'}
|
||||
showDot={tab === 'saves' && hasUnreadDraftUpdate}
|
||||
onClick={() => {
|
||||
if (activeTab === 'home' && tab === 'home') {
|
||||
selectNextRecommendEntry();
|
||||
return;
|
||||
<div className="platform-mobile-bottom-dock min-w-0 shrink-0">
|
||||
<div
|
||||
className={`platform-bottom-nav grid ${visibleTabs.length === 5 ? 'grid-cols-5' : visibleTabs.length === 4 ? 'grid-cols-4' : visibleTabs.length === 3 ? 'grid-cols-3' : 'grid-cols-2'}`}
|
||||
>
|
||||
{visibleTabs.map((tab) => (
|
||||
<PlatformTabButton
|
||||
key={tab}
|
||||
active={activeTab === tab}
|
||||
label={
|
||||
activeTab === 'home' && tab === 'home'
|
||||
? '下一个'
|
||||
: tabLabels[tab]
|
||||
}
|
||||
icon={
|
||||
activeTab === 'home' && tab === 'home'
|
||||
? ChevronDown
|
||||
: tabIcons[tab]
|
||||
}
|
||||
emphasized={tab === 'create'}
|
||||
showDot={tab === 'saves' && hasUnreadDraftUpdate}
|
||||
disabled={isRechargePaymentConfirmationPending}
|
||||
onClick={() => {
|
||||
if (isRechargePaymentConfirmationPending) {
|
||||
return;
|
||||
}
|
||||
if (activeTab === 'home' && tab === 'home') {
|
||||
selectNextRecommendEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
onTabChange(tab);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
onTabChange(tab);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{profilePopupPanel === 'saveArchives' ? (
|
||||
{profilePopupPanel === 'saveArchives' ? (
|
||||
<ProfileSaveArchivesModal
|
||||
saveEntries={saveEntries}
|
||||
saveError={saveError}
|
||||
@@ -6989,94 +7201,106 @@ export function RpgEntryHomeView({
|
||||
/>
|
||||
{profileEditModals}
|
||||
</div>
|
||||
{rechargePaymentConfirmationMask}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<div className="platform-desktop-shell flex h-full min-h-0 flex-col p-5 xl:p-6">
|
||||
<div className="platform-desktop-topbar flex items-center gap-4 px-5 py-4">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-5">
|
||||
<RpgEntryBrandLogo className="shrink-0" decorative />
|
||||
<PublicCodeSearchBar
|
||||
value={desktopSearchKeyword}
|
||||
onChange={updateDesktopSearchKeyword}
|
||||
onSubmit={submitDesktopSearch}
|
||||
isSearching={
|
||||
!onSearchPublicCode || Boolean(isSearchingPublicCode)
|
||||
}
|
||||
className="max-w-[34rem] flex-1"
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
inert={isRechargePaymentConfirmationPending ? true : undefined}
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<div className="platform-desktop-shell flex h-full min-h-0 flex-col p-5 xl:p-6">
|
||||
<div className="platform-desktop-topbar flex items-center gap-4 px-5 py-4">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-5">
|
||||
<RpgEntryBrandLogo className="shrink-0" decorative />
|
||||
<PublicCodeSearchBar
|
||||
value={desktopSearchKeyword}
|
||||
onChange={updateDesktopSearchKeyword}
|
||||
onSubmit={submitDesktopSearch}
|
||||
isSearching={
|
||||
!onSearchPublicCode || Boolean(isSearchingPublicCode)
|
||||
}
|
||||
className="max-w-[34rem] flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{isAuthenticated && activeTab === 'create' ? (
|
||||
<div className="flex items-center gap-3">
|
||||
{isAuthenticated &&
|
||||
(activeTab === 'create' || activeTab === 'saves') ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openRechargeOrRewardCodeModal}
|
||||
className="platform-desktop-create-wallet-chip platform-desktop-search inline-flex items-center gap-2 px-3 py-2.5 text-xs font-black text-[#b65f2c]"
|
||||
aria-label={`${formatDashboardCount(remainingNarrativeCoins)}泥点`}
|
||||
>
|
||||
<span className="grid h-7 w-7 place-items-center rounded-full bg-[#ffe0ab] text-[#cf7b34]">
|
||||
<Coins className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<span>{formatDashboardCount(remainingNarrativeCoins)}泥点</span>
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openUserSurface}
|
||||
className="platform-desktop-create-wallet-chip platform-desktop-search inline-flex items-center gap-2 px-3 py-2.5 text-xs font-black text-[#b65f2c]"
|
||||
aria-label={`${formatDashboardCount(remainingNarrativeCoins)}泥点`}
|
||||
className="platform-desktop-search flex items-center gap-3 px-3 py-2.5 text-left"
|
||||
>
|
||||
<span className="grid h-7 w-7 place-items-center rounded-full bg-[#ffe0ab] text-[#cf7b34]">
|
||||
<Coins className="h-3.5 w-3.5" />
|
||||
<span
|
||||
className="flex h-11 w-11 items-center justify-center overflow-hidden rounded-full text-base font-black text-white"
|
||||
style={{
|
||||
background: 'var(--platform-profile-avatar-fill)',
|
||||
boxShadow: 'var(--platform-profile-avatar-shadow)',
|
||||
}}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
avatarLabel
|
||||
)}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||
{authUi?.user?.displayName || '登录'}
|
||||
</span>
|
||||
<span className="block truncate text-xs text-[var(--platform-text-soft)]">
|
||||
{authUi?.user ? publicUserCode : '账号入口'}
|
||||
</span>
|
||||
</span>
|
||||
<span>{formatDashboardCount(remainingNarrativeCoins)}泥点</span>
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openUserSurface}
|
||||
className="platform-desktop-search flex items-center gap-3 px-3 py-2.5 text-left"
|
||||
>
|
||||
<span
|
||||
className="flex h-11 w-11 items-center justify-center overflow-hidden rounded-full text-base font-black text-white"
|
||||
style={{
|
||||
background: 'var(--platform-profile-avatar-fill)',
|
||||
boxShadow: 'var(--platform-profile-avatar-shadow)',
|
||||
}}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
avatarLabel
|
||||
)}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||
{authUi?.user?.displayName || '登录'}
|
||||
</span>
|
||||
<span className="block truncate text-xs text-[var(--platform-text-soft)]">
|
||||
{authUi?.user ? publicUserCode : '账号入口'}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex min-h-0 gap-5">
|
||||
<aside className="platform-desktop-rail flex w-[5.8rem] shrink-0 flex-col gap-3 p-3">
|
||||
{visibleTabs.map((tab) => (
|
||||
<DesktopTabButton
|
||||
key={tab}
|
||||
active={activeTab === tab}
|
||||
label={tabLabels[tab]}
|
||||
icon={tabIcons[tab]}
|
||||
emphasized={tab === 'create'}
|
||||
showDot={tab === 'saves' && hasUnreadDraftUpdate}
|
||||
onClick={() => {
|
||||
onTabChange(tab);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</aside>
|
||||
<div className="mt-5 flex min-h-0 gap-5">
|
||||
<aside className="platform-desktop-rail flex w-[5.8rem] shrink-0 flex-col gap-3 p-3">
|
||||
{visibleTabs.map((tab) => (
|
||||
<DesktopTabButton
|
||||
key={tab}
|
||||
active={activeTab === tab}
|
||||
label={tabLabels[tab]}
|
||||
icon={tabIcons[tab]}
|
||||
emphasized={tab === 'create'}
|
||||
showDot={tab === 'saves' && hasUnreadDraftUpdate}
|
||||
disabled={isRechargePaymentConfirmationPending}
|
||||
onClick={() => {
|
||||
if (isRechargePaymentConfirmationPending) {
|
||||
return;
|
||||
}
|
||||
onTabChange(tab);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</aside>
|
||||
|
||||
<div className="platform-tab-panel-stack min-w-0 flex-1">
|
||||
{tabPanels}
|
||||
<div className="platform-tab-panel-stack min-w-0 flex-1">
|
||||
{tabPanels}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7151,7 +7375,8 @@ export function RpgEntryHomeView({
|
||||
onClose={() => setActiveLegalDocumentId(null)}
|
||||
/>
|
||||
{profileEditModals}
|
||||
</div>
|
||||
{rechargePaymentConfirmationMask}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user