's'
This commit is contained in:
@@ -180,6 +180,8 @@ export default function HomeClient() {
|
||||
() => readVipBootstrapState().userEmail
|
||||
);
|
||||
const [payPendingOrderId, setPayPendingOrderId] = useState<string | null>(null);
|
||||
const [wechatNick, setWechatNick] = useState<string | null>(null);
|
||||
const [wechatAvatar, setWechatAvatar] = useState<string | null>(null);
|
||||
|
||||
const savePaidType = useCallback((type: string | null) => {
|
||||
if (type) setStorage("paidType", type);
|
||||
@@ -192,6 +194,110 @@ export default function HomeClient() {
|
||||
setUserName(name);
|
||||
}, []);
|
||||
|
||||
const recoverAnonymousUserId = useCallback(
|
||||
async (params: { userId?: string; email?: string; wechatUserId?: string }) => {
|
||||
const payload: Record<string, string> = {};
|
||||
if (params.userId?.trim()) payload.user_id = params.userId.trim();
|
||||
if (params.email?.trim()) payload.email = params.email.trim().toLowerCase();
|
||||
if (params.wechatUserId?.trim()) payload.wechat_userid = params.wechatUserId.trim();
|
||||
if (!payload.user_id && !payload.email && !payload.wechat_userid) return null;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/vip/identity-diagnose", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
credentials: "same-origin",
|
||||
cache: "no-store",
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
const candidate = [
|
||||
data?.resolved?.linked_anonymous_user_id,
|
||||
data?.vip_email_link?.by_anonymous_user_id?.anonymous_user_id,
|
||||
data?.vip_email_link?.by_email?.anonymous_user_id,
|
||||
data?.payments?.by_user_id?.user_id,
|
||||
data?.payments?.by_linked_user_id?.user_id,
|
||||
].find((x) => typeof x === "string" && /^user\d+$/.test(x.trim()));
|
||||
return typeof candidate === "string" ? candidate.trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const syncWechatProfile = useCallback(async () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const urlWechatUserId =
|
||||
params.get("userid")?.trim() ||
|
||||
params.get("wechat_userid")?.trim() ||
|
||||
"";
|
||||
if (urlWechatUserId) {
|
||||
// 兼容历史字段:userid(微信私信真实用户ID)
|
||||
setStorage("userid", urlWechatUserId);
|
||||
setStorage("wechatUserId", urlWechatUserId);
|
||||
// 安全要求:落本地后立即去掉 URL 中敏感 userid 参数,避免被转发扩散
|
||||
const cleanPath = window.location.pathname || "/";
|
||||
window.location.replace(cleanPath);
|
||||
return;
|
||||
}
|
||||
const wechatUserId =
|
||||
(getStorage("wechatUserId") || "").trim() ||
|
||||
(getStorage("userid") || "").trim();
|
||||
if (!wechatUserId) return;
|
||||
|
||||
const userId = getStorage("userId") || "";
|
||||
const userEmail = (getStorage("userEmail") || "").trim().toLowerCase();
|
||||
try {
|
||||
const res = await fetch("/api/vip/wechat-profile", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
user_id: userId || undefined,
|
||||
email: userEmail || undefined,
|
||||
userid: wechatUserId,
|
||||
wechat_userid: wechatUserId,
|
||||
}),
|
||||
credentials: "same-origin",
|
||||
cache: "no-store",
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
const profile = data?.profile || null;
|
||||
const relations = data?.relations || null;
|
||||
const nickname =
|
||||
typeof profile?.nickname === "string" ? profile.nickname.trim() : "";
|
||||
const avatar =
|
||||
typeof profile?.avatar === "string" ? profile.avatar.trim() : "";
|
||||
const linkedAnonymousUserId =
|
||||
typeof relations?.linked_anonymous_user_id === "string"
|
||||
? relations.linked_anonymous_user_id.trim()
|
||||
: typeof relations?.anonymous_user_id === "string"
|
||||
? relations.anonymous_user_id.trim()
|
||||
: "";
|
||||
if (/^user\d+$/.test(linkedAnonymousUserId)) {
|
||||
setStorage("userId", linkedAnonymousUserId);
|
||||
setStorage("memosAccount", linkedAnonymousUserId);
|
||||
setAnonymousUserCookie(linkedAnonymousUserId);
|
||||
}
|
||||
if (nickname) {
|
||||
setWechatNick(nickname);
|
||||
setStorage("wechatNick", nickname);
|
||||
} else {
|
||||
setWechatNick(null);
|
||||
removeStorage("wechatNick");
|
||||
}
|
||||
if (avatar) {
|
||||
setWechatAvatar(avatar);
|
||||
setStorage("wechatAvatar", avatar);
|
||||
} else {
|
||||
setWechatAvatar(null);
|
||||
removeStorage("wechatAvatar");
|
||||
}
|
||||
} catch {
|
||||
// 忽略失败,不影响主流程
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getStoredAnonymousUserId = useCallback((): string | null => {
|
||||
const memosAccount = getStorage("memosAccount");
|
||||
if (memosAccount && /^user\d+$/.test(memosAccount)) {
|
||||
@@ -278,6 +384,9 @@ export default function HomeClient() {
|
||||
): Promise<{ ok: boolean; error?: string }> => {
|
||||
try {
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
const wechatUserId =
|
||||
(getStorage("wechatUserId") || "").trim() ||
|
||||
(getStorage("userid") || "").trim();
|
||||
const res = await fetch("/api/vip/check-by-email", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -285,6 +394,7 @@ export default function HomeClient() {
|
||||
email: normalizedEmail,
|
||||
password,
|
||||
...(user_id?.trim() && { user_id: user_id.trim() }),
|
||||
...(wechatUserId && { wechat_userid: wechatUserId }),
|
||||
}),
|
||||
credentials: "include",
|
||||
});
|
||||
@@ -523,6 +633,14 @@ export default function HomeClient() {
|
||||
isH5 ? payPendingOrderId ?? undefined : undefined
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const nick = getStorage("wechatNick");
|
||||
const avatar = getStorage("wechatAvatar");
|
||||
if (nick) setWechatNick(nick);
|
||||
if (avatar) setWechatAvatar(avatar);
|
||||
void syncWechatProfile();
|
||||
}, [syncWechatProfile]);
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get("type")?.trim() === "clean") {
|
||||
@@ -583,7 +701,23 @@ export default function HomeClient() {
|
||||
}
|
||||
|
||||
// 校验逻辑:有本地存储(登录资料+VIP)时优先使用并校验;无身份/VIP 时点击按钮直接进入支付,不校验
|
||||
const anonymousCandidate = getStoredAnonymousUserId();
|
||||
const wechatUserId =
|
||||
(getStorage("wechatUserId") || "").trim() ||
|
||||
(getStorage("userid") || "").trim();
|
||||
let anonymousCandidate = getStoredAnonymousUserId();
|
||||
if (!anonymousCandidate && wechatUserId) {
|
||||
const recoveredFromWechat = await recoverAnonymousUserId({
|
||||
wechatUserId,
|
||||
email: (getStorage("userEmail") || "").trim().toLowerCase(),
|
||||
userId: (getStorage("userId") || "").trim(),
|
||||
});
|
||||
if (recoveredFromWechat && /^user\d+$/.test(recoveredFromWechat)) {
|
||||
anonymousCandidate = recoveredFromWechat;
|
||||
setStorage("userId", recoveredFromWechat);
|
||||
setStorage("memosAccount", recoveredFromWechat);
|
||||
setAnonymousUserCookie(recoveredFromWechat);
|
||||
}
|
||||
}
|
||||
const hadStoredUserId = !!anonymousCandidate;
|
||||
const emailVerifiedAt = Number(getStorage(EMAIL_VERIFY_TS_KEY) || "0");
|
||||
const skipAnonymousRecover =
|
||||
@@ -634,7 +768,28 @@ export default function HomeClient() {
|
||||
) {
|
||||
setStorage("memosAccount", meData.user.anonymousUserId);
|
||||
}
|
||||
const wechatUserId =
|
||||
(getStorage("wechatUserId") || "").trim() ||
|
||||
(getStorage("userid") || "").trim();
|
||||
const recoveredAnonymousUserId =
|
||||
(cur && /^user\d+$/.test(cur) ? cur : "") ||
|
||||
(typeof meData.user.anonymousUserId === "string" &&
|
||||
/^user\d+$/.test(meData.user.anonymousUserId)
|
||||
? meData.user.anonymousUserId.trim()
|
||||
: "") ||
|
||||
((await recoverAnonymousUserId({
|
||||
userId: cur || meData.user.id || "",
|
||||
email: meData.user.email || "",
|
||||
wechatUserId,
|
||||
})) ||
|
||||
"");
|
||||
if (/^user\d+$/.test(recoveredAnonymousUserId)) {
|
||||
setStorage("userId", recoveredAnonymousUserId);
|
||||
setStorage("memosAccount", recoveredAnonymousUserId);
|
||||
setAnonymousUserCookie(recoveredAnonymousUserId);
|
||||
} else {
|
||||
setStorage("userId", meData.user.id);
|
||||
}
|
||||
setStorage("emailLinked", "1");
|
||||
if (meData.user.email) {
|
||||
setStorage("userEmail", meData.user.email);
|
||||
@@ -667,6 +822,7 @@ export default function HomeClient() {
|
||||
getOrCreateUserId,
|
||||
getStoredAnonymousUserId,
|
||||
checkPaymentFromPB,
|
||||
recoverAnonymousUserId,
|
||||
savePaidType,
|
||||
saveUserName,
|
||||
tryRecoverByAnonymousUserId,
|
||||
@@ -734,7 +890,14 @@ export default function HomeClient() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<VipLayout isLoggedIn={isLoggedIn} userName={userName} userEmail={userEmail} onLogout={handleLogout}>
|
||||
<VipLayout
|
||||
isLoggedIn={isLoggedIn}
|
||||
userName={userName}
|
||||
userEmail={userEmail}
|
||||
wechatNick={wechatNick}
|
||||
wechatAvatar={wechatAvatar}
|
||||
onLogout={handleLogout}
|
||||
>
|
||||
{isLoggedIn ? (
|
||||
<DashboardPage userName={userName} userEmail={userEmail} />
|
||||
) : (
|
||||
|
||||
@@ -66,9 +66,10 @@ async function checkVipByEmailFromPayjsapi(
|
||||
request: NextRequest,
|
||||
email: string,
|
||||
password: string,
|
||||
userId?: string
|
||||
userId?: string,
|
||||
wechatUserId?: string
|
||||
): Promise<PayjsVipEmailResult | null> {
|
||||
const inflightKey = `${email}::${userId || ""}`;
|
||||
const inflightKey = `${email}::${userId || ""}::${wechatUserId || ""}`;
|
||||
const existed = inflightEmailChecks.get(inflightKey);
|
||||
if (existed) {
|
||||
return existed;
|
||||
@@ -96,6 +97,7 @@ async function checkVipByEmailFromPayjsapi(
|
||||
email,
|
||||
password,
|
||||
...(userId ? { user_id: userId } : {}),
|
||||
...(wechatUserId ? { wechat_userid: wechatUserId } : {}),
|
||||
}),
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(PAYJS_CHECK_BY_EMAIL_TIMEOUT),
|
||||
@@ -291,6 +293,9 @@ export async function POST(request: NextRequest) {
|
||||
const requestedAnonymousUserId = String(
|
||||
body?.user_id ?? body?.anonymousUserId ?? ""
|
||||
).trim();
|
||||
const wechatUserId = String(
|
||||
body?.wechat_userid ?? body?.userid ?? ""
|
||||
).trim();
|
||||
|
||||
console.log(
|
||||
"[check-by-email] request email=%s user_id=%s",
|
||||
@@ -326,7 +331,8 @@ export async function POST(request: NextRequest) {
|
||||
request,
|
||||
email,
|
||||
password,
|
||||
requestedAnonymousUserId || undefined
|
||||
requestedAnonymousUserId || undefined,
|
||||
wechatUserId || undefined
|
||||
);
|
||||
// PB 登录失败时,尝试使用 payjsapi 返回的 token/record 兜底恢复
|
||||
if (payjsResult?.vip && payjsResult.token && payjsResult.record?.id) {
|
||||
@@ -357,7 +363,8 @@ export async function POST(request: NextRequest) {
|
||||
request,
|
||||
email,
|
||||
password,
|
||||
requestedAnonymousUserId || undefined
|
||||
requestedAnonymousUserId || undefined,
|
||||
wechatUserId || undefined
|
||||
);
|
||||
}
|
||||
if (payjsResult?.vip) {
|
||||
@@ -397,7 +404,8 @@ export async function POST(request: NextRequest) {
|
||||
request,
|
||||
email,
|
||||
password,
|
||||
requestedAnonymousUserId || undefined
|
||||
requestedAnonymousUserId || undefined,
|
||||
wechatUserId || undefined
|
||||
);
|
||||
recovery = await resolveRecovery(
|
||||
adminToken,
|
||||
@@ -451,6 +459,20 @@ export async function POST(request: NextRequest) {
|
||||
anonymousUserId
|
||||
);
|
||||
|
||||
if (wechatUserId) {
|
||||
const apiUrl = resolvePayApiUrl(request);
|
||||
await fetch(`${apiUrl}/nomadvip/wechat_profile`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
user_id: anonymousUserId || authData.record.id,
|
||||
email,
|
||||
wechat_userid: wechatUserId,
|
||||
}),
|
||||
cache: "no-store",
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[check-by-email] success source=%s pbUserId=%s anonymousUserId=%s",
|
||||
recovery.source ?? "pocketbase",
|
||||
|
||||
32
app/api/vip/identity-diagnose/route.ts
Normal file
32
app/api/vip/identity-diagnose/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getApiUrlFromRequest, getPaymentConfig } from "@/config/services.config";
|
||||
|
||||
function resolvePayApiUrl(request: NextRequest): string {
|
||||
const payConfig = getPaymentConfig();
|
||||
const hostHeader =
|
||||
request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||
return (
|
||||
(hostHeader ? getApiUrlFromRequest(hostHeader) : null) || payConfig.apiUrl
|
||||
).replace(/\/$/, "");
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const apiUrl = resolvePayApiUrl(request);
|
||||
const res = await fetch(`${apiUrl}/nomadvip/identity_diagnose`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body || {}),
|
||||
cache: "no-store",
|
||||
}).catch(() => null);
|
||||
|
||||
if (!res) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "upstream unavailable" },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return NextResponse.json(data, { status: res.ok ? 200 : 502 });
|
||||
}
|
||||
|
||||
29
app/api/vip/wechat-profile/route.ts
Normal file
29
app/api/vip/wechat-profile/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getApiUrlFromRequest, getPaymentConfig } from "@/config/services.config";
|
||||
|
||||
function resolvePayApiUrl(request: NextRequest): string {
|
||||
const payConfig = getPaymentConfig();
|
||||
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||
return (
|
||||
(hostHeader ? getApiUrlFromRequest(hostHeader) : null) ||
|
||||
payConfig.apiUrl
|
||||
).replace(/\/$/, "");
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const apiUrl = resolvePayApiUrl(request);
|
||||
const res = await fetch(`${apiUrl}/nomadvip/wechat_profile`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body || {}),
|
||||
cache: "no-store",
|
||||
}).catch(() => null);
|
||||
|
||||
if (!res) {
|
||||
return NextResponse.json({ ok: false, found: false, error: "upstream unavailable" }, { status: 502 });
|
||||
}
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return NextResponse.json(data, { status: res.ok ? 200 : 502 });
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ const ANONYMOUS_USER_COOKIE = "nomadvip_anon_uid";
|
||||
const ANONYMOUS_USER_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
|
||||
const PAY_ORDER_COOKIE = "nomadvip_pay_order";
|
||||
const PAY_ORDER_MAX_AGE = 60 * 15;
|
||||
const VIP_PAY_TEST_MODE_KEY = "vipPayTestMode";
|
||||
|
||||
function getStorage(key: string): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
@@ -114,6 +115,33 @@ async function getOrCreateUserId(): Promise<string> {
|
||||
return syncId;
|
||||
}
|
||||
|
||||
async function recoverAnonymousUserIdByWechatUserId(): Promise<string | null> {
|
||||
const wechatUserId =
|
||||
(getStorage("wechatUserId") || "").trim() ||
|
||||
(getStorage("userid") || "").trim();
|
||||
if (!wechatUserId) return null;
|
||||
try {
|
||||
const res = await fetch("/api/vip/identity-diagnose", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ wechat_userid: wechatUserId }),
|
||||
credentials: "same-origin",
|
||||
cache: "no-store",
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
const candidate = [
|
||||
data?.resolved?.linked_anonymous_user_id,
|
||||
data?.vip_email_link?.by_anonymous_user_id?.anonymous_user_id,
|
||||
data?.vip_email_link?.by_email?.anonymous_user_id,
|
||||
data?.payments?.by_user_id?.user_id,
|
||||
data?.payments?.by_linked_user_id?.user_id,
|
||||
].find((x) => typeof x === "string" && /^user\d+$/.test(x.trim()));
|
||||
return typeof candidate === "string" ? candidate.trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function redirectToPayZpay(params: {
|
||||
user_id: string;
|
||||
return_url: string;
|
||||
@@ -248,8 +276,10 @@ function payWechatXorpaySync(params: {
|
||||
export async function triggerPay(
|
||||
onPendingOrder?: (orderId: string) => void
|
||||
): Promise<void> {
|
||||
const userId =
|
||||
typeof navigator !== "undefined" && /MicroMessenger/i.test(navigator.userAgent)
|
||||
const recoveredByWechat = await recoverAnonymousUserIdByWechatUserId();
|
||||
const userId = recoveredByWechat
|
||||
? recoveredByWechat
|
||||
: typeof navigator !== "undefined" && /MicroMessenger/i.test(navigator.userAgent)
|
||||
? getOrCreateUserIdSync()
|
||||
: await getOrCreateUserId();
|
||||
if (/^user\d+$/.test(userId)) {
|
||||
@@ -260,7 +290,8 @@ export async function triggerPay(
|
||||
const returnUrl = `${window.location.origin}/paid`;
|
||||
const env = getPayEnv();
|
||||
const device = getDeviceFromEnv(env);
|
||||
const totalFee = getPaymentConfig().defaultAmount ?? 8800;
|
||||
const isPayTestMode = getStorage(VIP_PAY_TEST_MODE_KEY) === "1";
|
||||
const totalFee = isPayTestMode ? 100 : getPaymentConfig().defaultAmount ?? 8800;
|
||||
const base = {
|
||||
user_id: userId,
|
||||
return_url: returnUrl,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { LandingHero } from "./landing/LandingHero";
|
||||
import { AssetStats } from "./landing/AssetStats";
|
||||
import { TopicEbookShowcase } from "./landing/TopicEbookShowcase";
|
||||
@@ -31,6 +31,22 @@ export function LandingPage({
|
||||
}: LandingPageProps) {
|
||||
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||
const [promoModalOpen, setPromoModalOpen] = useState(false);
|
||||
const [payTestMode, setPayTestMode] = useState(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
return localStorage.getItem("vipPayTestMode") === "1";
|
||||
});
|
||||
const titleTapCountRef = useRef(0);
|
||||
|
||||
const handleHeroTitleTap = () => {
|
||||
titleTapCountRef.current += 1;
|
||||
if (titleTapCountRef.current < 10) return;
|
||||
titleTapCountRef.current = 0;
|
||||
const nextMode = !payTestMode;
|
||||
setPayTestMode(nextMode);
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("vipPayTestMode", nextMode ? "1" : "0");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -43,7 +59,7 @@ export function LandingPage({
|
||||
<div className={styles.landingNoise} />
|
||||
</div>
|
||||
<div className={styles.landingSceneInner}>
|
||||
<LandingHero />
|
||||
<LandingHero onTitleTap={handleHeroTitleTap} />
|
||||
<AssetStats />
|
||||
<TopicEbookShowcase onLockedClick={() => setLoginModalOpen(true)} />
|
||||
<CompareSection />
|
||||
@@ -54,6 +70,7 @@ export function LandingPage({
|
||||
<LandingCTA
|
||||
onJoin={onJoin}
|
||||
onLogin={() => setLoginModalOpen(true)}
|
||||
payTestMode={payTestMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,10 +17,19 @@ type VipHeaderProps = {
|
||||
isLoggedIn: boolean;
|
||||
userName?: string | null;
|
||||
userEmail?: string | null;
|
||||
wechatNick?: string | null;
|
||||
wechatAvatar?: string | null;
|
||||
onLogout?: () => void;
|
||||
};
|
||||
|
||||
export function VipHeader({ isLoggedIn, userName, userEmail, onLogout }: VipHeaderProps) {
|
||||
export function VipHeader({
|
||||
isLoggedIn,
|
||||
userName,
|
||||
userEmail,
|
||||
wechatNick,
|
||||
wechatAvatar,
|
||||
onLogout,
|
||||
}: VipHeaderProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -38,8 +47,9 @@ export function VipHeader({ isLoggedIn, userName, userEmail, onLogout }: VipHead
|
||||
onLogout?.();
|
||||
};
|
||||
|
||||
const showUser = isLoggedIn || userEmail;
|
||||
const displayName = userName || userEmail || "会员";
|
||||
const hasWechatProfile = !!(wechatNick || wechatAvatar);
|
||||
const showUser = isLoggedIn || !!userEmail || hasWechatProfile;
|
||||
const displayName = wechatNick || userName || userEmail || "会员";
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
@@ -54,10 +64,15 @@ export function VipHeader({ isLoggedIn, userName, userEmail, onLogout }: VipHead
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-amber-500 text-sm font-semibold text-white shadow-sm hover:bg-amber-600"
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full bg-amber-500 text-sm font-semibold text-white shadow-sm hover:bg-amber-600"
|
||||
aria-label="用户菜单"
|
||||
>
|
||||
{getAvatarLetter(displayName)}
|
||||
{wechatAvatar ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={wechatAvatar} alt={displayName} className="h-full w-full object-cover" />
|
||||
) : (
|
||||
getAvatarLetter(displayName)
|
||||
)}
|
||||
</button>
|
||||
{isLoggedIn && (
|
||||
<span
|
||||
|
||||
@@ -9,13 +9,30 @@ type VipLayoutProps = {
|
||||
isLoggedIn: boolean;
|
||||
userName?: string | null;
|
||||
userEmail?: string | null;
|
||||
wechatNick?: string | null;
|
||||
wechatAvatar?: string | null;
|
||||
onLogout?: () => void;
|
||||
};
|
||||
|
||||
export function VipLayout({ children, isLoggedIn, userName, userEmail, onLogout }: VipLayoutProps) {
|
||||
export function VipLayout({
|
||||
children,
|
||||
isLoggedIn,
|
||||
userName,
|
||||
userEmail,
|
||||
wechatNick,
|
||||
wechatAvatar,
|
||||
onLogout,
|
||||
}: VipLayoutProps) {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<VipHeader isLoggedIn={isLoggedIn} userName={userName} userEmail={userEmail} onLogout={onLogout} />
|
||||
<VipHeader
|
||||
isLoggedIn={isLoggedIn}
|
||||
userName={userName}
|
||||
userEmail={userEmail}
|
||||
wechatNick={wechatNick}
|
||||
wechatAvatar={wechatAvatar}
|
||||
onLogout={onLogout}
|
||||
/>
|
||||
<main className={styles.main}>{children}</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { formatRestoreDateShort } from "@/config/promo.config";
|
||||
import styles from "../vip.module.css";
|
||||
|
||||
type LandingCTAProps = {
|
||||
onJoin: () => void;
|
||||
onLogin: () => void;
|
||||
payTestMode?: boolean;
|
||||
};
|
||||
|
||||
/** 设为 true 可重新展示「适合你 / 请止步」区块(下方 JSX 与 vip.module.css 内样式均保留) */
|
||||
const SHOW_CTA_AUDIENCE_BOUNDARY = false;
|
||||
|
||||
export function LandingCTA({ onJoin, onLogin }: LandingCTAProps) {
|
||||
const [restoreDate, setRestoreDate] = useState<string>("");
|
||||
useEffect(() => {
|
||||
setRestoreDate(formatRestoreDateShort());
|
||||
}, []);
|
||||
export function LandingCTA({ onJoin, onLogin, payTestMode }: LandingCTAProps) {
|
||||
const [restoreDate] = useState<string>(() => formatRestoreDateShort());
|
||||
|
||||
return (
|
||||
<section className={`${styles.section} ${styles.ctaSection}`} aria-labelledby="cta-heading">
|
||||
@@ -74,6 +72,7 @@ export function LandingCTA({ onJoin, onLogin }: LandingCTAProps) {
|
||||
type="button"
|
||||
onClick={onJoin}
|
||||
className={`${styles.btnPrimary} ${styles.btnLarge} ${styles.ctaBtnPrimary}`}
|
||||
style={payTestMode ? { background: "linear-gradient(135deg,#16a34a,#22c55e)" } : undefined}
|
||||
>
|
||||
🔒 立即加入
|
||||
</button>
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
|
||||
import styles from "../vip.module.css";
|
||||
|
||||
export function LandingHero() {
|
||||
type LandingHeroProps = {
|
||||
onTitleTap?: () => void;
|
||||
};
|
||||
|
||||
export function LandingHero({ onTitleTap }: LandingHeroProps) {
|
||||
return (
|
||||
<section className={`${styles.section} ${styles.heroSection} ${styles.heroSection3d}`} style={{ textAlign: "center" }}>
|
||||
<p
|
||||
className={`${styles.heroEyebrow} animate__animated animate__fadeInDown`}
|
||||
style={{ animationDelay: "0.1s", animationFillMode: "both" }}
|
||||
style={{ animationDelay: "0.1s", animationFillMode: "both", cursor: "pointer" }}
|
||||
onClick={onTitleTap}
|
||||
title="连续点击 10 次切换测试价格"
|
||||
>
|
||||
异度星球会员中心
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user