fix staging reports and membership navigation
This commit is contained in:
@@ -1916,6 +1916,7 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled
|
||||
color: var(--color-ink);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.membership-back svg {
|
||||
@@ -1932,6 +1933,13 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled
|
||||
letter-spacing: -.03em;
|
||||
}
|
||||
|
||||
.membership-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.membership-orders-entry,
|
||||
.membership-redeem-entry {
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
@@ -1947,22 +1955,18 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.membership-orders-entry {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-ink-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.membership-redeem-entry svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.membership-notice {
|
||||
margin: 0;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border: 1px solid color-mix(in srgb, var(--color-action) 22%, var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--color-action-soft) 62%, transparent);
|
||||
color: var(--color-ink-secondary);
|
||||
font-size: var(--type-body-sm);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.membership-hero {
|
||||
position: relative;
|
||||
min-height: 260px;
|
||||
@@ -2124,6 +2128,11 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.membership-section-heading > div {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.membership-section-heading h2 {
|
||||
margin: 0;
|
||||
color: var(--color-ink);
|
||||
@@ -2524,29 +2533,43 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled
|
||||
}
|
||||
|
||||
.membership-tabs {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
gap: 4px;
|
||||
margin-bottom: var(--space-5);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-canvas-soft);
|
||||
}
|
||||
|
||||
.membership-tab {
|
||||
position: relative;
|
||||
min-height: 44px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
color: var(--color-ink-secondary);
|
||||
cursor: pointer;
|
||||
font-size: var(--type-body-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.membership-tab.is-active,
|
||||
.membership-tab[aria-selected="true"] {
|
||||
.membership-tab:hover {
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.membership-tab:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--color-action) 46%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.membership-tab[data-active] {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-action);
|
||||
border-bottom-color: var(--color-action);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.membership-panel {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { formatPrice, orderStatusLabel } from "@/lib/membership";
|
||||
|
||||
type PaymentOrder = {
|
||||
orderNo: string;
|
||||
status: string;
|
||||
credits: number;
|
||||
moneyCents: number;
|
||||
currency: string;
|
||||
productName: string;
|
||||
paidAt: string | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
function normalizeOrders(list: readonly unknown[]): PaymentOrder[] {
|
||||
return list.map((raw) => {
|
||||
const item = (raw ?? {}) as Record<string, unknown>;
|
||||
const snapshot = (item.product_snapshot ?? item.product ?? {}) as Record<string, unknown>;
|
||||
const price =
|
||||
typeof item.price === "number"
|
||||
? item.price
|
||||
: typeof item.money_cents === "number"
|
||||
? item.money_cents
|
||||
: typeof item.moneyCents === "number"
|
||||
? item.moneyCents
|
||||
: 0;
|
||||
return {
|
||||
orderNo: String(item.order_no ?? item.orderNo ?? ""),
|
||||
status: String(item.status ?? "unknown"),
|
||||
credits: typeof item.credits === "number" ? item.credits : 0,
|
||||
moneyCents: price,
|
||||
currency: String(item.currency ?? "CNY"),
|
||||
productName:
|
||||
typeof snapshot.name === "string"
|
||||
? snapshot.name
|
||||
: typeof item.name === "string"
|
||||
? item.name
|
||||
: String(item.product_code ?? item.productCode ?? ""),
|
||||
paidAt: item.paid_at ? String(item.paid_at) : item.paidAt ? String(item.paidAt) : null,
|
||||
createdAt: String(item.created_at ?? item.createdAt ?? ""),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default function MembershipOrdersPage() {
|
||||
const [orders, setOrders] = useState<PaymentOrder[]>([]);
|
||||
const [ordersState, setOrdersState] = useState<"loading" | "ready" | "unavailable">("loading");
|
||||
|
||||
const fetchOrders = useCallback(async () => {
|
||||
setOrdersState("loading");
|
||||
try {
|
||||
const response = await fetch("/api/payment/orders", { cache: "no-store" });
|
||||
if (response.status === 401) {
|
||||
window.location.assign("/login");
|
||||
return;
|
||||
}
|
||||
if (!response.ok) throw new Error("订单记录暂时不可用");
|
||||
const payload = await response.json().catch(() => null);
|
||||
const list = Array.isArray(payload)
|
||||
? payload
|
||||
: payload && typeof payload === "object" && Array.isArray(payload.orders)
|
||||
? payload.orders
|
||||
: [];
|
||||
setOrders(normalizeOrders(list));
|
||||
setOrdersState("ready");
|
||||
} catch {
|
||||
setOrders([]);
|
||||
setOrdersState("unavailable");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
await fetchOrders();
|
||||
})();
|
||||
}, [fetchOrders]);
|
||||
|
||||
return (
|
||||
<main className="membership-page">
|
||||
<div className="membership-shell">
|
||||
<header className="membership-header">
|
||||
<div className="membership-header-left">
|
||||
<Link className="membership-back" href="/membership" aria-label="返回套餐与会员">
|
||||
<ArrowLeft aria-hidden="true" />
|
||||
<span>返回</span>
|
||||
</Link>
|
||||
<h1 className="membership-title">订单记录</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="membership-orders" aria-labelledby="membership-orders-title">
|
||||
<div className="membership-section-heading">
|
||||
<div>
|
||||
<h2 id="membership-orders-title">全部订单</h2>
|
||||
<p>查看套餐与点数充值的支付和到账状态。</p>
|
||||
</div>
|
||||
<button className="membership-orders-refresh" type="button" onClick={() => void fetchOrders()}>刷新</button>
|
||||
</div>
|
||||
{ordersState === "loading" && <p className="membership-empty">正在加载订单…</p>}
|
||||
{ordersState === "unavailable" && <p className="membership-empty">订单记录暂时不可用,请稍后刷新。</p>}
|
||||
{ordersState === "ready" && orders.length === 0 && <p className="membership-empty">暂无订单。</p>}
|
||||
{orders.length > 0 && (
|
||||
<ul className="membership-order-list">
|
||||
{orders.map((order) => (
|
||||
<li className="membership-order-row" key={order.orderNo}>
|
||||
<div>
|
||||
<strong>{order.productName || "订单"}</strong>
|
||||
<small>{order.orderNo}</small>
|
||||
</div>
|
||||
<span className={`membership-order-status status-${order.status}`}>
|
||||
{orderStatusLabel(order.status)}
|
||||
</span>
|
||||
<b>{formatPrice(order.moneyCents, order.currency)}</b>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { FormEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { ArrowLeft, CheckCircle2, Gift, Sparkles, X } from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { keepFocusWithin } from "@/lib/focus-trap";
|
||||
import {
|
||||
entitlementLabel,
|
||||
formatMembershipDate,
|
||||
formatPrice,
|
||||
formatProductDuration,
|
||||
membershipSourceNotice,
|
||||
notifyBalanceChanged,
|
||||
orderStatusLabel,
|
||||
planAlias,
|
||||
planDisplayLabel,
|
||||
productAudience,
|
||||
@@ -37,17 +37,6 @@ type MembershipAccount = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
type PaymentOrder = {
|
||||
orderNo: string;
|
||||
status: string;
|
||||
credits: number;
|
||||
moneyCents: number;
|
||||
currency: string;
|
||||
productName: string;
|
||||
paidAt: string | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
type PaymentOrderState = {
|
||||
orderNo: string;
|
||||
payUrl: string | null;
|
||||
@@ -56,36 +45,6 @@ type PaymentOrderState = {
|
||||
productId: string;
|
||||
};
|
||||
|
||||
function normalizeOrders(list: readonly unknown[]): PaymentOrder[] {
|
||||
return list.map((raw) => {
|
||||
const item = (raw ?? {}) as Record<string, unknown>;
|
||||
const snapshot = (item.product_snapshot ?? item.product ?? {}) as Record<string, unknown>;
|
||||
const price =
|
||||
typeof item.price === "number"
|
||||
? item.price
|
||||
: typeof item.money_cents === "number"
|
||||
? item.money_cents
|
||||
: typeof item.moneyCents === "number"
|
||||
? item.moneyCents
|
||||
: 0;
|
||||
return {
|
||||
orderNo: String(item.order_no ?? item.orderNo ?? ""),
|
||||
status: String(item.status ?? "unknown"),
|
||||
credits: typeof item.credits === "number" ? item.credits : 0,
|
||||
moneyCents: price,
|
||||
currency: String(item.currency ?? "CNY"),
|
||||
productName:
|
||||
typeof snapshot.name === "string"
|
||||
? snapshot.name
|
||||
: typeof item.name === "string"
|
||||
? item.name
|
||||
: String(item.product_code ?? item.productCode ?? ""),
|
||||
paidAt: item.paid_at ? String(item.paid_at) : item.paidAt ? String(item.paidAt) : null,
|
||||
createdAt: String(item.created_at ?? item.createdAt ?? ""),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) window.history.back();
|
||||
else window.location.assign("/");
|
||||
@@ -93,7 +52,6 @@ function goBack() {
|
||||
|
||||
function MembershipContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const source = searchParams.get("source");
|
||||
const planHint = searchParams.get("plan");
|
||||
const highlightedPlan: MembershipPlanAlias | null =
|
||||
planHint === "trial" || planHint === "monthly" || planHint === "yearly" ? planHint : null;
|
||||
@@ -103,8 +61,6 @@ function MembershipContent() {
|
||||
const [paymentPackages, setPaymentPackages] = useState<MembershipProduct[]>([]);
|
||||
const [paymentEnabled, setPaymentEnabled] = useState(false);
|
||||
const [packagesError, setPackagesError] = useState("");
|
||||
const [orders, setOrders] = useState<PaymentOrder[]>([]);
|
||||
const [ordersState, setOrdersState] = useState<"loading" | "ready" | "unavailable">("loading");
|
||||
const [redeemOpen, setRedeemOpen] = useState(() => searchParams.get("redeem") === "1");
|
||||
const [redeemCode, setRedeemCode] = useState("");
|
||||
const [redeemError, setRedeemError] = useState("");
|
||||
@@ -114,7 +70,6 @@ function MembershipContent() {
|
||||
const [paymentError, setPaymentError] = useState("");
|
||||
const [payingProductId, setPayingProductId] = useState<string | null>(null);
|
||||
const [selectedProductId, setSelectedProductId] = useState<string | null>(null);
|
||||
const [activeSegment, setActiveSegment] = useState<"membership" | "credits">("membership");
|
||||
const [redeemDone, setRedeemDone] = useState(false);
|
||||
const redeemDialogRef = useRef<HTMLElement>(null);
|
||||
const redeemInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -173,35 +128,12 @@ function MembershipContent() {
|
||||
}
|
||||
}, [resetPaymentState]);
|
||||
|
||||
const fetchOrders = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/payment/orders", { cache: "no-store" });
|
||||
if (!response.ok) {
|
||||
setOrdersState("unavailable");
|
||||
setOrders([]);
|
||||
return;
|
||||
}
|
||||
const payload = await response.json().catch(() => null);
|
||||
const list = Array.isArray(payload)
|
||||
? payload
|
||||
: payload && typeof payload === "object" && Array.isArray(payload.orders)
|
||||
? payload.orders
|
||||
: [];
|
||||
setOrders(normalizeOrders(list));
|
||||
setOrdersState("ready");
|
||||
} catch {
|
||||
setOrdersState("unavailable");
|
||||
setOrders([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
await fetchAccountData();
|
||||
await fetchPackages();
|
||||
await fetchOrders();
|
||||
})();
|
||||
}, [fetchAccountData, fetchOrders, fetchPackages]);
|
||||
}, [fetchAccountData, fetchPackages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!redeemOpen) return;
|
||||
@@ -273,7 +205,6 @@ function MembershipContent() {
|
||||
setRedeemMessage(result.message || `本次到账 ${awarded} 点,最新余额 ${result.credits} 点。`);
|
||||
const refreshed = await fetchAccountData();
|
||||
notifyBalanceChanged(refreshed?.credits ?? result.credits);
|
||||
void fetchOrders();
|
||||
} catch (caught) {
|
||||
setRedeemError(caught instanceof Error ? caught.message : "兑换失败,请稍后重试");
|
||||
} finally {
|
||||
@@ -333,16 +264,14 @@ function MembershipContent() {
|
||||
if (paid) {
|
||||
const refreshed = await fetchAccountData();
|
||||
notifyBalanceChanged(refreshed?.credits ?? 0);
|
||||
void fetchOrders();
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [fetchAccountData, fetchOrders, paymentOrder]);
|
||||
}, [fetchAccountData, paymentOrder]);
|
||||
|
||||
const plans = selectMembershipPlans(paymentPackages);
|
||||
const creditPacks = paymentPackages.filter((product) => product.productType === "credit_pack");
|
||||
const sourceNotice = membershipSourceNotice(source);
|
||||
|
||||
return (
|
||||
<main className="membership-page">
|
||||
@@ -355,17 +284,15 @@ function MembershipContent() {
|
||||
</button>
|
||||
<h1 className="membership-title">套餐与会员</h1>
|
||||
</div>
|
||||
<button className="membership-redeem-entry" type="button" onClick={() => setRedeemOpen(true)}>
|
||||
<Gift aria-hidden="true" />
|
||||
<span>兑换码</span>
|
||||
</button>
|
||||
<div className="membership-header-actions">
|
||||
<Link className="membership-orders-entry" href="/membership/orders">订单记录</Link>
|
||||
<button className="membership-redeem-entry" type="button" onClick={() => setRedeemOpen(true)}>
|
||||
<Gift aria-hidden="true" />
|
||||
<span>兑换码</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{sourceNotice && <p className="membership-notice" role="status">{sourceNotice}</p>}
|
||||
{highlightedPlan && (
|
||||
<p className="membership-notice" role="status">已为你定位到{planDisplayLabel(highlightedPlan)}套餐。</p>
|
||||
)}
|
||||
|
||||
<section className="membership-hero" aria-labelledby="membership-hero-title">
|
||||
<div className="membership-hero-copy">
|
||||
<p className="membership-greeting">{account?.user.email ? `账户:${account.user.email}` : "欢迎回来"}</p>
|
||||
@@ -376,14 +303,13 @@ function MembershipContent() {
|
||||
<article className="membership-summary-card">
|
||||
<span>会员状态</span>
|
||||
<strong>{activeSubscription ? activeSubscription.product?.name || activeSubscription.productCode : "未开通会员"}</strong>
|
||||
<small>{activeSubscription
|
||||
? `${formatMembershipDate(activeSubscription.startsAt)} 至 ${formatMembershipDate(activeSubscription.endsAt)}`
|
||||
: "体验 / 月卡 / 年卡见下方套餐"}</small>
|
||||
{activeSubscription && (
|
||||
<small>{formatMembershipDate(activeSubscription.startsAt)} 至 {formatMembershipDate(activeSubscription.endsAt)}</small>
|
||||
)}
|
||||
</article>
|
||||
<article className="membership-summary-card">
|
||||
<span>当前余额</span>
|
||||
<strong>{account ? `${account.credits} 点` : accountError || "正在读取…"}</strong>
|
||||
<small>用于咨询与生时校正等服务</small>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,59 +323,18 @@ function MembershipContent() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="membership-orders" aria-labelledby="membership-orders-title">
|
||||
<div className="membership-section-heading">
|
||||
<h2 id="membership-orders-title">订单记录</h2>
|
||||
<button className="membership-orders-refresh" type="button" onClick={() => void fetchOrders()}>刷新</button>
|
||||
</div>
|
||||
{ordersState === "loading" && <p className="membership-empty">正在加载订单…</p>}
|
||||
{ordersState === "unavailable" && <p className="membership-empty">订单记录暂时不可用,请稍后刷新。</p>}
|
||||
{ordersState === "ready" && orders.length === 0 && <p className="membership-empty">暂无订单。</p>}
|
||||
{orders.length > 0 && (
|
||||
<ul className="membership-order-list">
|
||||
{orders.map((order) => (
|
||||
<li className="membership-order-row" key={order.orderNo}>
|
||||
<div>
|
||||
<strong>{order.productName || "订单"}</strong>
|
||||
<small>{order.orderNo}</small>
|
||||
</div>
|
||||
<span className={`membership-order-status status-${order.status}`}>
|
||||
{orderStatusLabel(order.status)}
|
||||
</span>
|
||||
<b>{formatPrice(order.moneyCents, order.currency)}</b>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="membership-segment" aria-label="套餐与点数">
|
||||
<div className="membership-tabs" role="tablist" aria-label="套餐分类">
|
||||
<button
|
||||
className={`membership-tab${activeSegment === "membership" ? " is-active" : ""}`}
|
||||
id="membership-plans-tab"
|
||||
role="tab"
|
||||
type="button"
|
||||
aria-selected={activeSegment === "membership"}
|
||||
aria-controls="membership-plans-panel"
|
||||
onClick={() => setActiveSegment("membership")}
|
||||
>
|
||||
会员套餐
|
||||
</button>
|
||||
<button
|
||||
className={`membership-tab${activeSegment === "credits" ? " is-active" : ""}`}
|
||||
id="membership-credits-tab"
|
||||
role="tab"
|
||||
type="button"
|
||||
aria-selected={activeSegment === "credits"}
|
||||
aria-controls="membership-credits-panel"
|
||||
onClick={() => setActiveSegment("credits")}
|
||||
>
|
||||
点数充值
|
||||
</button>
|
||||
</div>
|
||||
<Tabs defaultValue="membership">
|
||||
<TabsList className="membership-tabs" aria-label="套餐分类">
|
||||
<TabsTrigger className="membership-tab" id="membership-plans-tab" value="membership">
|
||||
会员套餐
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="membership-tab" id="membership-credits-tab" value="credits">
|
||||
点数充值
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{paymentError && (
|
||||
{paymentError && (
|
||||
<div className="membership-payment-error" role="alert">
|
||||
<p>{paymentError}</p>
|
||||
{selectedProductId && (
|
||||
@@ -463,16 +348,10 @@ function MembershipContent() {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{packagesError && <p className="form-error" role="alert">{packagesError}</p>}
|
||||
)}
|
||||
{packagesError && <p className="form-error" role="alert">{packagesError}</p>}
|
||||
|
||||
{activeSegment === "membership" && (
|
||||
<div
|
||||
className="membership-panel"
|
||||
id="membership-plans-panel"
|
||||
role="tabpanel"
|
||||
aria-labelledby="membership-plans-tab"
|
||||
>
|
||||
<TabsContent className="membership-panel" value="membership">
|
||||
{!packagesError && !paymentEnabled && <p className="membership-empty">在线支付暂未开放。</p>}
|
||||
{plans.length > 0 && (
|
||||
<div className="membership-plan-grid">
|
||||
@@ -516,7 +395,7 @@ function MembershipContent() {
|
||||
<li>{isCurrent
|
||||
? `当前有效期至 ${formatMembershipDate(activeSubscription?.endsAt)},续费从到期后顺延,不会覆盖未到期权益。`
|
||||
: "支付成功后权益立即生效。"}</li>
|
||||
<li>订单与到账状态可在本页「订单记录」中查看。</li>
|
||||
<li>订单与到账状态可在「订单记录」页面查看。</li>
|
||||
<li>支付由合作渠道处理;如长时间未到账,请联系支持并出示订单号。</li>
|
||||
</ul>
|
||||
</details>
|
||||
@@ -534,16 +413,9 @@ function MembershipContent() {
|
||||
</div>
|
||||
)}
|
||||
{!packagesError && paymentEnabled && plans.length === 0 && <p className="membership-empty">会员套餐正在上架中。</p>}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{activeSegment === "credits" && (
|
||||
<div
|
||||
className="membership-panel"
|
||||
id="membership-credits-panel"
|
||||
role="tabpanel"
|
||||
aria-labelledby="membership-credits-tab"
|
||||
>
|
||||
<TabsContent className="membership-panel" value="credits">
|
||||
{creditPacks.length > 0 && (
|
||||
<div className="membership-credit-grid">
|
||||
{creditPacks.map((product) => (
|
||||
@@ -563,7 +435,6 @@ function MembershipContent() {
|
||||
<summary>详细规则</summary>
|
||||
<ul>
|
||||
<li>点数充值支付成功后立即到账。</li>
|
||||
<li>点数用于咨询、报告与生时校正等服务。</li>
|
||||
</ul>
|
||||
</details>
|
||||
<button
|
||||
@@ -579,8 +450,8 @@ function MembershipContent() {
|
||||
</div>
|
||||
)}
|
||||
{!packagesError && paymentEnabled && creditPacks.length === 0 && <p className="membership-empty">点数包正在上架中。</p>}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</section>
|
||||
|
||||
{paymentOrder && (
|
||||
@@ -607,7 +478,7 @@ function MembershipContent() {
|
||||
<>
|
||||
<h3>等待完成支付</h3>
|
||||
<p>订单号:{paymentOrder.orderNo}</p>
|
||||
<p>请在已打开的收银台页面完成支付;本页会自动刷新余额与订单记录。</p>
|
||||
<p>请在已打开的收银台页面完成支付;本页会自动检查支付状态并刷新余额。</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client"
|
||||
|
||||
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({ className, ...props }: TabsPrimitive.Root.Props) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList({ className, ...props }: TabsPrimitive.List.Props) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn("inline-flex w-fit items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
||||
return (
|
||||
<TabsPrimitive.Tab
|
||||
data-slot="tabs-trigger"
|
||||
className={cn("inline-flex items-center justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
|
||||
return (
|
||||
<TabsPrimitive.Panel
|
||||
data-slot="tabs-content"
|
||||
className={cn("outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger }
|
||||
@@ -68,7 +68,7 @@ export async function runConsultationWorkflow(input: ConsultationInput) {
|
||||
question_text: workflowRequest.question,
|
||||
theme: workflowRequest.themes,
|
||||
}),
|
||||
signal: AbortSignal.timeout(45_000),
|
||||
signal: AbortSignal.timeout(90_000),
|
||||
});
|
||||
|
||||
const data = await response.json().catch(() => null);
|
||||
|
||||
Reference in New Issue
Block a user