Merge branch 'dev' into test
This commit is contained in:
@@ -3,8 +3,8 @@ NEXT_PUBLIC_APP_ENV=test
|
||||
# NextAuth v4 —— OAuth callback **公**网 base URL(**必**须与**公**网域名**一**致)
|
||||
NEXTAUTH_URL=https://frontend-test.banlv-ai.com
|
||||
NEXTAUTH_URL_INTERNAL=http://localhost:3000
|
||||
NEXT_PUBLIC_API_BASE_URL=https://testapi.banlv-ai.com
|
||||
NEXT_PUBLIC_WS_BASE_URL=wss://testapi.banlv-ai.com/ws
|
||||
NEXT_PUBLIC_API_BASE_URL=https://proapi.banlv-ai.com
|
||||
NEXT_PUBLIC_WS_BASE_URL=wss://proapi.banlv-ai.com/ws
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51Te8ybEJDtUGxQHo0UMySJ2hW0vVryynzllmIUI3UQdQCFgxNLci0Jmm2lQkRsTaIP7C7IQlFzfFyBJ5rOhr1ML800G8P8DF5R
|
||||
|
||||
NEXT_PUBLIC_API_CONNECT_TIMEOUT=30000
|
||||
|
||||
@@ -43,10 +43,15 @@ export function ChatScreen() {
|
||||
// isGuest 派生(chat-screen 是唯一知道这个值的地方 —— chat 机器无 isGuest 字段)
|
||||
const isGuest = deriveIsGuest(authState.loginStatus);
|
||||
|
||||
// 消息数量限制由后端统一返回 blocked/daily_limit,前端不再处理本地额度。
|
||||
const showDailyLimitBanner =
|
||||
// 消息数量限制由后端统一返回 blocked,前端不再处理本地额度。
|
||||
const showMessageLimitBanner =
|
||||
state.paywallTriggered &&
|
||||
state.paywallReason === "daily_limit";
|
||||
(state.paywallReason === "daily_limit" ||
|
||||
state.paywallReason === "total_limit");
|
||||
const messageLimitTitle =
|
||||
state.paywallReason === "total_limit"
|
||||
? "The limit for free chat times\nhas been reached"
|
||||
: undefined;
|
||||
const showPhotoPaywallBanner =
|
||||
state.paywallTriggered && state.paywallReason === "photo_paywall";
|
||||
const showPrivatePaywallBanner =
|
||||
@@ -168,8 +173,9 @@ export function ChatScreen() {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{showDailyLimitBanner ? (
|
||||
{showMessageLimitBanner ? (
|
||||
<ChatQuotaExhaustedBanner
|
||||
title={messageLimitTitle}
|
||||
/>
|
||||
) : (
|
||||
<ChatInputBar disabled={state.isReplyingAI} />
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
* 订阅页返回箭头
|
||||
*
|
||||
* 视觉规格(与设计稿对齐):
|
||||
* - 24×24 SVG chevron-left
|
||||
* - 24×24 chevron-left 图标
|
||||
* - 黑色,2px 描边
|
||||
* - 无 "Back" 文本
|
||||
*/
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { ROUTES } from "@/router/routes";
|
||||
@@ -24,22 +25,12 @@ export function SubscriptionBackLink({ className }: SubscriptionBackLinkProps) {
|
||||
aria-label="Back"
|
||||
className={[styles.backLink, className].filter(Boolean).join(" ")}
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
<ChevronLeft
|
||||
size={24}
|
||||
strokeWidth={2}
|
||||
aria-hidden="true"
|
||||
className={styles.icon}
|
||||
>
|
||||
<path
|
||||
d="M15 6l-6 6 6 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
flex-direction: column;
|
||||
min-height: 100dvh;
|
||||
background: var(--color-page-background);
|
||||
padding: 30px 30px 10px;
|
||||
padding: 18 30px 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.backSlot {
|
||||
@@ -73,7 +72,7 @@
|
||||
}
|
||||
|
||||
.agreementLabel {
|
||||
font-size: 17px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.agreementLink {
|
||||
|
||||
@@ -78,7 +78,8 @@ function currencySymbol(currency: string): string {
|
||||
return `${currency} `;
|
||||
}
|
||||
|
||||
function formatAmount(amountCents: number): string {
|
||||
function formatAmount(amountCents: number | null): string | null {
|
||||
if (amountCents === null) return null;
|
||||
return (amountCents / 100).toFixed(2).replace(/\.00$/, "");
|
||||
}
|
||||
|
||||
@@ -91,9 +92,15 @@ function planCaption(
|
||||
? "Voice package"
|
||||
: `${plan.dolAmount} voice messages`;
|
||||
}
|
||||
if (plan.dailyPriceCents !== null) {
|
||||
const dailyPrice = formatAmount(plan.dailyPriceCents);
|
||||
return dailyPrice === null
|
||||
? "Lifetime"
|
||||
: `${currencySymbol(plan.currency)}${dailyPrice}/day`;
|
||||
}
|
||||
if (plan.vipDays === null) return "Lifetime";
|
||||
const perDay = plan.amountCents / 100 / Math.max(plan.vipDays, 1);
|
||||
return `${currencySymbol(plan.currency)}${perDay.toFixed(2)}/day`;
|
||||
const fallbackDailyPrice = plan.amountCents / Math.max(plan.vipDays, 1);
|
||||
return `${currencySymbol(plan.currency)}${formatAmount(fallbackDailyPrice)}/day`;
|
||||
}
|
||||
|
||||
function toPlanView(
|
||||
@@ -103,8 +110,8 @@ function toPlanView(
|
||||
return {
|
||||
id: plan.planId,
|
||||
name: plan.planName,
|
||||
price: formatAmount(plan.amountCents),
|
||||
originalPrice: null,
|
||||
price: formatAmount(plan.amountCents) ?? "",
|
||||
originalPrice: formatAmount(plan.originalAmountCents),
|
||||
perDay: planCaption(plan, subscriptionType),
|
||||
currencySymbol: currencySymbol(plan.currency),
|
||||
};
|
||||
|
||||
@@ -12,6 +12,8 @@ export class PaymentPlan {
|
||||
declare readonly planName: string;
|
||||
declare readonly orderType: string;
|
||||
declare readonly amountCents: number;
|
||||
declare readonly originalAmountCents: number | null;
|
||||
declare readonly dailyPriceCents: number | null;
|
||||
declare readonly currency: string;
|
||||
declare readonly vipDays: number | null;
|
||||
declare readonly dolAmount: number | null;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"success": true,
|
||||
"data": {
|
||||
"mode": "http",
|
||||
"type": "text",
|
||||
"reply": "",
|
||||
"voiceUrl": "",
|
||||
"audioUrl": "",
|
||||
"intimacyChange": 0,
|
||||
"newIntimacy": 0,
|
||||
"relationshipStage": "密友",
|
||||
"currentMood": "happy",
|
||||
"messageId": "",
|
||||
"isGuest": true,
|
||||
"timestamp": 1782197944984,
|
||||
"blocked": true,
|
||||
"blockReason": "total_limit",
|
||||
"blockDetail": {
|
||||
"type": "guest_total_msg_limit",
|
||||
"usedTotal": 12,
|
||||
"limit": 5
|
||||
},
|
||||
"paywallTriggered": false,
|
||||
"showUpgrade": false,
|
||||
"imageType": null,
|
||||
"imageUrl": null
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import { z } from "zod";
|
||||
|
||||
export const ChatBlockDetailSchema = z.object({
|
||||
type: z.string(),
|
||||
usedToday: z.number(),
|
||||
usedToday: z.number().optional(),
|
||||
usedTotal: z.number().optional(),
|
||||
limit: z.number(),
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ const PaymentPlanWireSchema = z.object({
|
||||
plan_name: z.string(),
|
||||
order_type: z.string(),
|
||||
amount_cents: z.number(),
|
||||
original_amount_cents: z.number().nullable().default(null),
|
||||
daily_price_cents: z.number().nullable().default(null),
|
||||
currency: z.string(),
|
||||
vip_days: z.number().nullable(),
|
||||
dol_amount: z.number().nullable(),
|
||||
@@ -23,6 +25,8 @@ export const PaymentPlanSchema = z
|
||||
plan_name: data.planName,
|
||||
order_type: data.orderType,
|
||||
amount_cents: data.amountCents,
|
||||
original_amount_cents: data.originalAmountCents,
|
||||
daily_price_cents: data.dailyPriceCents,
|
||||
currency: data.currency,
|
||||
vip_days: data.vipDays,
|
||||
dol_amount: data.dolAmount,
|
||||
@@ -35,6 +39,8 @@ export const PaymentPlanSchema = z
|
||||
planName: data.plan_name,
|
||||
orderType: data.order_type,
|
||||
amountCents: data.amount_cents,
|
||||
originalAmountCents: data.original_amount_cents,
|
||||
dailyPriceCents: data.daily_price_cents,
|
||||
currency: data.currency,
|
||||
vipDays: data.vip_days,
|
||||
dolAmount: data.dol_amount,
|
||||
|
||||
@@ -103,7 +103,12 @@ export function applyHttpSendOutput(
|
||||
): Partial<ChatState> {
|
||||
const { response, reply } = output;
|
||||
|
||||
if (response.blocked === true && response.blockReason === "daily_limit") {
|
||||
const isMessageLimitBlocked =
|
||||
response.blocked === true &&
|
||||
(response.blockReason === "daily_limit" ||
|
||||
response.blockReason === "total_limit");
|
||||
|
||||
if (isMessageLimitBlocked) {
|
||||
const detail = response.blockDetail;
|
||||
const lastMessage = context.messages[context.messages.length - 1];
|
||||
const messages =
|
||||
@@ -115,9 +120,19 @@ export function applyHttpSendOutput(
|
||||
messages,
|
||||
isReplyingAI: false,
|
||||
paywallTriggered: true,
|
||||
paywallReason: "daily_limit",
|
||||
paywallReason:
|
||||
response.blockReason === "total_limit" ? "total_limit" : "daily_limit",
|
||||
paywallDetail: detail
|
||||
? { usedToday: detail.usedToday, limit: detail.limit }
|
||||
? {
|
||||
type: detail.type,
|
||||
...(detail.usedToday != null
|
||||
? { usedToday: detail.usedToday }
|
||||
: {}),
|
||||
...(detail.usedTotal != null
|
||||
? { usedTotal: detail.usedTotal }
|
||||
: {}),
|
||||
limit: detail.limit,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,8 +19,20 @@ export interface ChatState {
|
||||
*/
|
||||
wsConnected: boolean;
|
||||
paywallTriggered: boolean;
|
||||
paywallReason: "daily_limit" | "photo_paywall" | "private_paywall" | null;
|
||||
paywallDetail: { usedToday: number; limit: number } | null;
|
||||
paywallReason:
|
||||
| "daily_limit"
|
||||
| "total_limit"
|
||||
| "photo_paywall"
|
||||
| "private_paywall"
|
||||
| null;
|
||||
paywallDetail:
|
||||
| {
|
||||
type?: string;
|
||||
usedToday?: number;
|
||||
usedTotal?: number;
|
||||
limit: number;
|
||||
}
|
||||
| null;
|
||||
unlockingPrivateMessageId: string | null;
|
||||
isLoadingMore: boolean;
|
||||
hasMore: boolean;
|
||||
|
||||
Reference in New Issue
Block a user