This commit is contained in:
eric
2026-03-30 10:36:08 -05:00
parent 2307411b37
commit 2ffbddff2e
8 changed files with 454 additions and 6 deletions

View File

@@ -9,6 +9,7 @@ import { CommunityPreview } from "./landing/CommunityPreview";
import { FAQSection } from "./landing/FAQSection";
import { LandingCTA } from "./landing/LandingCTA";
import { LoginModal } from "./landing/LoginModal";
import { PromoProofModal } from "./landing/PromoProofModal";
import styles from "./vip.module.css";
type LandingPageProps = {
@@ -29,6 +30,7 @@ export function LandingPage({
onVerifySuccess,
}: LandingPageProps) {
const [loginModalOpen, setLoginModalOpen] = useState(false);
const [promoModalOpen, setPromoModalOpen] = useState(false);
return (
<>
@@ -49,7 +51,10 @@ export function LandingPage({
<CommunityPreview />
</div>
<FAQSection />
<LandingCTA onJoin={onJoin} onLogin={() => setLoginModalOpen(true)} />
<LandingCTA
onJoin={onJoin}
onLogin={() => setLoginModalOpen(true)}
/>
</div>
</div>
<LoginModal
@@ -59,6 +64,7 @@ export function LandingPage({
onVerifyByEmail={onVerifyByEmail}
onSuccess={onVerifySuccess}
/>
<PromoProofModal open={promoModalOpen} onClose={() => setPromoModalOpen(false)} />
</>
);
}

View File

@@ -24,7 +24,7 @@ export function LandingCTA({ onJoin, onLogin }: LandingCTAProps) {
<div className={styles.ctaGlow} aria-hidden />
<div className={styles.ctaCard}>
<h2 id="cta-heading" className={styles.ctaPromoLine}>
<span className={styles.ctaBadgeInline}>
<span className={styles.ctaPromoClickable} aria-disabled="true">
<span className={styles.ctaBadgePulse} aria-hidden />
</span>

View File

@@ -0,0 +1,134 @@
"use client";
import { type ChangeEvent, useRef, useState } from "react";
import styles from "../vip.module.css";
type PromoProofModalProps = {
open: boolean;
onClose: () => void;
};
type RecognizeResult = {
ok: boolean;
nickname?: string;
avatar_image_base64?: string;
avatar_mime?: string;
confidence?: {
nickname?: number;
avatar?: number;
overall?: number;
};
message?: string;
error?: string;
};
export function PromoProofModal({ open, onClose }: PromoProofModalProps) {
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [confirmed, setConfirmed] = useState(false);
const [uploading, setUploading] = useState(false);
const [result, setResult] = useState<RecognizeResult | null>(null);
const [error, setError] = useState<string>("");
if (!open) return null;
const onPickFile = () => {
fileInputRef.current?.click();
};
const onFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
setError("");
setResult(null);
setUploading(true);
try {
const form = new FormData();
form.append("image", file);
const res = await fetch("/api/promo/recognize-discount", {
method: "POST",
body: form,
});
const data = (await res.json().catch(() => ({}))) as RecognizeResult;
if (!res.ok || !data?.ok) {
setError(data?.error || data?.message || "识别失败,请换一张清晰截图再试");
} else {
setResult(data);
}
} catch (e) {
setError(e instanceof Error ? e.message : "上传失败,请稍后重试");
} finally {
setUploading(false);
event.target.value = "";
}
};
const avatarSrc =
result?.avatar_image_base64 && result?.avatar_mime
? `data:${result.avatar_mime};base64,${result.avatar_image_base64}`
: null;
return (
<div className={styles.communityModalBackdrop} role="dialog" aria-modal="true" aria-labelledby="promo-proof-title">
<div className={styles.promoModalPanel}>
<button type="button" onClick={onClose} className={styles.communityModalClose} aria-label="关闭">
×
</button>
<div className={styles.promoModalTop}>
<div className={styles.promoModalTag}></div>
<h3 id="promo-proof-title" className={styles.promoModalTitle}> 50 </h3>
<p className={styles.promoModalDesc}>
icon
<strong> + + </strong>
</p>
<p className={styles.promoModalHint}> / Eric / </p>
</div>
{!confirmed ? (
<div className={styles.promoModalActions}>
<button type="button" className={`${styles.btnPrimary} ${styles.promoPrimaryBtn}`} onClick={() => setConfirmed(true)}>
</button>
</div>
) : (
<div className={styles.promoUploadArea}>
<input
ref={fileInputRef}
type="file"
accept="image/png,image/jpeg,image/webp"
className={styles.promoFileInput}
onChange={onFileChange}
/>
<button type="button" className={styles.promoUploadBtn} onClick={onPickFile} disabled={uploading}>
{uploading ? "识别中..." : "选择手机截图并上传"}
</button>
<p className={styles.promoUploadTip}> jpg/png/webp 720px </p>
{error ? <p className={styles.promoError}>{error}</p> : null}
{result?.ok ? (
<div className={styles.promoResultCard}>
<p className={styles.promoResultTitle}></p>
<div className={styles.promoResultRow}>
<div className={styles.promoAvatarWrap}>
{avatarSrc ? (
<img src={avatarSrc} alt="识别头像" className={styles.promoAvatarImg} />
) : (
<div className={styles.promoAvatarEmpty}></div>
)}
</div>
<div className={styles.promoResultMeta}>
<p className={styles.promoNicknameLabel}></p>
<p className={styles.promoNicknameValue}>{result.nickname || "未识别"}</p>
</div>
</div>
</div>
) : null}
</div>
)}
</div>
</div>
);
}

View File

@@ -1048,10 +1048,30 @@
}
.dark .ctaCard {
background: rgba(20, 20, 26, 0.78);
background:
radial-gradient(90% 70% at 50% -10%, rgba(251, 191, 36, 0.12) 0%, rgba(251, 191, 36, 0) 58%),
linear-gradient(180deg, #0b1220 0%, #0a0f1a 100%);
border-color: rgba(251, 191, 36, 0.18);
box-shadow:
0 24px 56px rgba(0, 0, 0, 0.45),
0 0 0 1px rgba(255, 255, 255, 0.06) inset;
0 24px 60px rgba(0, 0, 0, 0.52),
0 0 0 1px rgba(251, 191, 36, 0.08) inset,
0 1px 0 rgba(255, 255, 255, 0.04) inset;
}
.dark .ctaGlow {
opacity: 0.55;
filter: blur(10px);
}
.dark .ctaPromoLine {
color: rgba(255, 255, 255, 0.92);
}
.dark .ctaHighlight {
background: linear-gradient(120deg, #f59e0b 0%, #fcd34d 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
@media (min-width: 640px) {
@@ -1381,3 +1401,232 @@
transform: translateY(-1px);
background: var(--muted);
}
.ctaPromoClickable {
appearance: none;
-webkit-appearance: none;
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.28rem 0.65rem;
font-size: 0.7em;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--accent);
background: rgba(245, 158, 11, 0.12);
border: 1px solid rgba(245, 158, 11, 0.35);
border-radius: 9999px;
cursor: pointer;
font-family: inherit;
transition: transform 0.15s, box-shadow 0.15s, background 0.15s;
}
.ctaPromoClickable:hover {
transform: translateY(-1px);
background: rgba(245, 158, 11, 0.18);
box-shadow: 0 8px 20px rgba(245, 158, 11, 0.18);
}
.ctaPromoClickable:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.dark .ctaPromoClickable {
color: #fbbf24;
background: rgba(245, 158, 11, 0.16);
border-color: rgba(251, 191, 36, 0.42);
box-shadow: 0 0 0 1px rgba(251, 191, 36, 0.1) inset;
}
.dark .ctaPromoClickable:hover {
background: rgba(245, 158, 11, 0.24);
box-shadow:
0 12px 28px rgba(0, 0, 0, 0.42),
0 0 0 1px rgba(251, 191, 36, 0.18) inset;
}
.dark .ctaPromoDivider {
color: rgba(251, 191, 36, 0.58);
}
.dark .ctaPromoSuffix {
color: rgba(255, 255, 255, 0.88);
}
.dark .ctaChip {
color: rgba(255, 255, 255, 0.82);
border-color: rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.06);
}
.dark .ctaBtnSecondary {
color: rgba(255, 255, 255, 0.92);
border-color: rgba(251, 191, 36, 0.26);
background: rgba(255, 255, 255, 0.03);
}
.dark .ctaBtnSecondary:hover {
background: rgba(251, 191, 36, 0.1);
border-color: rgba(251, 191, 36, 0.42);
color: #fde68a;
}
/* 促销识别弹窗 */
.promoModalPanel {
position: relative;
width: 100%;
max-width: 520px;
padding: 1.4rem 1.2rem 1.2rem;
border-radius: 16px;
border: 1px solid rgba(245, 158, 11, 0.25);
background: var(--card);
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.35);
}
.promoModalTop {
padding-right: 1.25rem;
}
.promoModalTag {
display: inline-block;
font-size: 0.72rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--accent);
border: 1px solid rgba(245, 158, 11, 0.3);
background: rgba(245, 158, 11, 0.12);
border-radius: 999px;
padding: 0.18rem 0.58rem;
margin-bottom: 0.65rem;
}
.promoModalTitle {
margin: 0 0 0.35rem;
font-size: 1.1rem;
color: var(--foreground);
}
.promoModalDesc {
margin: 0 0 0.55rem;
line-height: 1.55;
font-size: 0.9rem;
color: var(--muted-foreground);
}
.promoModalHint {
margin: 0 0 0.9rem;
font-size: 0.78rem;
color: var(--muted-foreground);
}
.promoModalActions {
margin-top: 0.5rem;
}
.promoPrimaryBtn {
width: 100%;
}
.promoUploadArea {
margin-top: 0.4rem;
border: 1px dashed rgba(245, 158, 11, 0.34);
border-radius: 12px;
padding: 0.9rem;
background: rgba(245, 158, 11, 0.04);
}
.promoFileInput {
display: none;
}
.promoUploadBtn {
width: 100%;
border: 1px solid rgba(245, 158, 11, 0.42);
background: linear-gradient(135deg, rgba(245, 158, 11, 0.18), rgba(245, 158, 11, 0.08));
color: var(--foreground);
font-size: 0.92rem;
font-weight: 600;
border-radius: 10px;
padding: 0.72rem 0.9rem;
cursor: pointer;
}
.promoUploadBtn:disabled {
opacity: 0.65;
cursor: not-allowed;
}
.promoUploadTip {
margin: 0.45rem 0 0;
color: var(--muted-foreground);
font-size: 0.76rem;
}
.promoError {
margin: 0.75rem 0 0;
font-size: 0.82rem;
color: #ef4444;
}
.promoResultCard {
margin-top: 0.85rem;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--muted);
padding: 0.8rem;
}
.promoResultTitle {
margin: 0 0 0.5rem;
font-size: 0.83rem;
color: var(--muted-foreground);
}
.promoResultRow {
display: flex;
align-items: center;
gap: 0.72rem;
}
.promoAvatarWrap {
width: 64px;
height: 64px;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border);
background: var(--card);
display: flex;
align-items: center;
justify-content: center;
}
.promoAvatarImg {
width: 100%;
height: 100%;
object-fit: cover;
}
.promoAvatarEmpty {
font-size: 0.72rem;
color: var(--muted-foreground);
}
.promoResultMeta {
min-width: 0;
}
.promoNicknameLabel {
margin: 0 0 0.15rem;
font-size: 0.76rem;
color: var(--muted-foreground);
}
.promoNicknameValue {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--foreground);
}