diff --git a/src/app/subscription/page.tsx b/src/app/subscription/page.tsx index a6a87c44..6ccd066f 100644 --- a/src/app/subscription/page.tsx +++ b/src/app/subscription/page.tsx @@ -1,12 +1,32 @@ import { Suspense } from "react"; +import { MobileShell } from "@/app/_components/core"; + import { SubscriptionPageClient } from "./subscription-page-client"; -import { SubscriptionScreen } from "./subscription-screen"; export default function SubscriptionPage() { return ( - }> + }> ); } + +function SubscriptionFallback() { + return ( + +
+ Loading subscription... +
+
+ ); +} diff --git a/src/app/subscription/return/page.tsx b/src/app/subscription/return/page.tsx index 2f2bf3f0..e1a0f5b7 100644 --- a/src/app/subscription/return/page.tsx +++ b/src/app/subscription/return/page.tsx @@ -10,8 +10,11 @@ import { ROUTES } from "@/router/routes"; type ReturnStatus = "loading" | "missing"; function buildSubscriptionUrl(subscriptionType: "vip" | "voice"): string { - if (subscriptionType === "voice") return `${ROUTES.subscription}?type=voice`; - return `${ROUTES.subscription}?type=vip`; + const params = new URLSearchParams({ + type: subscriptionType, + paymentReturn: "1", + }); + return `${ROUTES.subscription}?${params.toString()}`; } export default function SubscriptionReturnPage() { diff --git a/src/app/subscription/subscription-page-client.tsx b/src/app/subscription/subscription-page-client.tsx index 4aab8c03..944406eb 100644 --- a/src/app/subscription/subscription-page-client.tsx +++ b/src/app/subscription/subscription-page-client.tsx @@ -14,6 +14,12 @@ function toSubscriptionType(value: string | null): SubscriptionType { export function SubscriptionPageClient() { const searchParams = useSearchParams(); const subscriptionType = toSubscriptionType(searchParams.get("type")); + const shouldResumePendingOrder = searchParams.get("paymentReturn") === "1"; - return ; + return ( + + ); } diff --git a/src/app/subscription/subscription-screen.tsx b/src/app/subscription/subscription-screen.tsx index 4fe084d1..e5db9782 100644 --- a/src/app/subscription/subscription-screen.tsx +++ b/src/app/subscription/subscription-screen.tsx @@ -113,10 +113,12 @@ function toPlanView( export interface SubscriptionScreenProps { subscriptionType?: SubscriptionType; + shouldResumePendingOrder?: boolean; } export function SubscriptionScreen({ subscriptionType = "vip", + shouldResumePendingOrder = false, }: SubscriptionScreenProps) { const user = useUserState(); const userDispatch = useUserDispatch(); @@ -139,15 +141,31 @@ export function SubscriptionScreen({ }, [payment.currentOrderId, payment.isPaid, userDispatch]); useEffect(() => { - if (payment.status !== "ready") return; + const canInspectPendingOrder = + payment.status === "ready" || + (!shouldResumePendingOrder && + (payment.isPollingOrder || payment.isPaid || payment.status === "failed")); + if (!canInspectPendingOrder) return; let cancelled = false; - const resumePendingOrder = async () => { + const handlePendingOrder = async () => { const result = await PendingPaymentOrderStorage.getPendingOrderForType( subscriptionType, ); if (cancelled || !result.success || result.data === null) return; + + if (!shouldResumePendingOrder) { + await PendingPaymentOrderStorage.clearPendingOrder(); + if ( + payment.currentOrderId === result.data.orderId && + (payment.isPollingOrder || payment.isPaid || payment.status === "failed") + ) { + paymentDispatch({ type: "PaymentReset" }); + } + return; + } + if (payment.currentOrderId === result.data.orderId) return; if (resumedPendingOrderRef.current === result.data.orderId) return; @@ -158,11 +176,19 @@ export function SubscriptionScreen({ }); }; - void resumePendingOrder(); + void handlePendingOrder(); return () => { cancelled = true; }; - }, [payment.currentOrderId, payment.status, paymentDispatch, subscriptionType]); + }, [ + payment.currentOrderId, + payment.isPaid, + payment.isPollingOrder, + payment.status, + paymentDispatch, + shouldResumePendingOrder, + subscriptionType, + ]); useEffect(() => { if (!payment.currentOrderId) return; diff --git a/src/data/dto/chat/ui_message.ts b/src/data/dto/chat/ui_message.ts index 7b76b4e7..fa154322 100644 --- a/src/data/dto/chat/ui_message.ts +++ b/src/data/dto/chat/ui_message.ts @@ -10,7 +10,7 @@ import { z } from "zod"; export const UiMessageSchema = z.object({ content: z.string(), isFromAI: z.boolean(), - /** 显示用时间戳(HH:mm) */ + /** 日期分隔条使用的本地日期(YYYY-MM-DD) */ date: z.string(), /** 图片 URL(base64 data URL 或 http URL) */ imageUrl: z.string().optional(), diff --git a/src/stores/chat/chat-machine.helpers.ts b/src/stores/chat/chat-machine.helpers.ts index 19de1ca0..9b1565c5 100644 --- a/src/stores/chat/chat-machine.helpers.ts +++ b/src/stores/chat/chat-machine.helpers.ts @@ -25,7 +25,7 @@ import { chatRepository } from "@/data/repositories/chat_repository"; import type { IChatRepository } from "@/data/repositories/interfaces"; import { ChatStorage } from "@/data/storage/chat/chat_storage"; import { ChatSendResponse } from "@/data/dto/chat/chat_send_response"; -import { formatDate, Result, Logger } from "@/utils"; +import { todayString, Result, Logger } from "@/utils"; import type { ChatState } from "./chat-state"; @@ -61,10 +61,16 @@ export function localMessagesToUi( return records.map((m) => ({ content: m.content, isFromAI: m.role === "assistant", - date: m.createdAt, + date: messageDateFromCreatedAt(m.createdAt), })); } +function messageDateFromCreatedAt(createdAt: string): string { + const parsed = new Date(createdAt); + if (Number.isNaN(parsed.getTime())) return todayString(); + return todayString(parsed); +} + /** * ChatSendResponse → UiMessage(纯函数) * - 业务事实:后端响应就是 AI 的回复 @@ -74,7 +80,7 @@ export function sendResponseToUiMessage(response: ChatSendResponse): UiMessage { return { content: response.reply, isFromAI: true, - date: formatDate(new Date(response.timestamp)), + date: todayString(new Date(response.timestamp)), }; } @@ -201,7 +207,7 @@ export async function readAndSyncHistory(): Promise<{ "there were things I couldn't say there. " + "Finally I can relax. How was your day out?", isFromAI: true, - date: formatDate(), + date: todayString(), }; // 1. 读 local diff --git a/src/stores/chat/chat-machine.ts b/src/stores/chat/chat-machine.ts index 4472bfbc..3768a476 100644 --- a/src/stores/chat/chat-machine.ts +++ b/src/stores/chat/chat-machine.ts @@ -38,7 +38,7 @@ import { setup, assign } from "xstate"; import { ChatStorage } from "@/data/storage/chat/chat_storage"; -import { formatDate, todayString, Logger } from "@/utils"; +import { todayString, Logger } from "@/utils"; import { ChatState, initialState } from "./chat-state"; import type { ChatEvent } from "./chat-events"; @@ -229,7 +229,7 @@ export const chatMachine = setup({ messages.push({ content: event.text, isFromAI: true, - date: formatDate(), + date: todayString(), }); } else { const last = messages[messages.length - 1]; @@ -255,7 +255,7 @@ export const chatMachine = setup({ { content: "Something went wrong. Try sending again?", isFromAI: true, - date: formatDate(), + date: todayString(), }, ]; return { messages, isReplyingAI: false };