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