diff --git a/backend/app/routers/ebook.py b/backend/app/routers/ebook.py index b8ad957..5cb5754 100644 --- a/backend/app/routers/ebook.py +++ b/backend/app/routers/ebook.py @@ -23,8 +23,21 @@ def _user(authorization: str | None) -> dict: async def ebook_status(authorization: str | None = Header(None)): user = _user(authorization) owned = social_store.has_ebook(user["id"]) + has_files = bool(EBOOK_PDF_URL or EBOOK_EPUB_URL) return { "owned": owned, + "has_files": has_files, + "amount_fen": PAYMENT_EBOOK_AMOUNT, + "amount_yuan": f"{PAYMENT_EBOOK_AMOUNT / 100:.2f}", + } + + +@router.get("/product") +async def ebook_product(): + """Public product info — no auth (buy CTA honesty).""" + has_files = bool(EBOOK_PDF_URL or EBOOK_EPUB_URL) + return { + "has_files": has_files, "amount_fen": PAYMENT_EBOOK_AMOUNT, "amount_yuan": f"{PAYMENT_EBOOK_AMOUNT / 100:.2f}", } @@ -41,5 +54,5 @@ async def ebook_downloads(authorization: str | None = Header(None)): if EBOOK_EPUB_URL: files.append({"format": "epub", "label": "EPUB", "url": EBOOK_EPUB_URL}) if not files: - files.append({"format": "online", "label": "在线阅读", "url": "/book/read"}) - return {"owned": True, "files": files} + files.append({"format": "online", "label": "Online reading", "url": "/book/read"}) + return {"owned": True, "files": files, "has_files": bool(EBOOK_PDF_URL or EBOOK_EPUB_URL)} diff --git a/frontend/.env.local.example b/frontend/.env.local.example index 0ec0a2d..0d3600c 100644 --- a/frontend/.env.local.example +++ b/frontend/.env.local.example @@ -1,2 +1,4 @@ NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1 NEXT_PUBLIC_SITE_URL=http://localhost:3000 +# Optional — shows Google Sign-In on /login when set +# NEXT_PUBLIC_GOOGLE_CLIENT_ID= diff --git a/frontend/src/app/changelog/page.tsx b/frontend/src/app/changelog/page.tsx index 84fa1cc..66c80fa 100644 --- a/frontend/src/app/changelog/page.tsx +++ b/frontend/src/app/changelog/page.tsx @@ -8,6 +8,15 @@ export const metadata: Metadata = { }; const LOGS = [ + { + date: "2026-09-04", + tag: "转化/信任打磨", + items: [ + "订阅条中英校验与说明;Join 保存资料后直达匹配;定价免费 CTA 改走完善资料", + "VIP/电子书支付说明更诚实(支付宝微信、无文件时写清在线解锁);登录可接 Google(需配置 Client ID)", + "移动端隐藏易裁切的 hero 浮卡;活动后下一步补齐私信入口", + ], + }, { date: "2026-09-04", tag: "首页/关于页", diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 7766be4..25a7989 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1301,6 +1301,7 @@ img { max-width: 100%; display: block; } .hero-stats { justify-content: center; } .hero-visual { order: -1; } .globe-container { max-width: 280px; } + .floating-cards .float-card { display: none; } .dest-grid { grid-template-columns: repeat(2, 1fr); } .tools-grid { grid-template-columns: repeat(2, 1fr); } @@ -9624,6 +9625,20 @@ a.profile-stat-link:hover { .auth-demo-btn { width: 100%; } +.auth-google { + display: flex; + justify-content: center; + margin-bottom: 12px; + min-height: 44px; +} +.auth-google.is-disabled { + pointer-events: none; + opacity: 0.55; +} +.join-pay-note { + margin-top: 10px; + font-size: 0.85rem; +} .tools-core-banner { display: grid; grid-template-columns: repeat(3, 1fr); diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index c3ec760..d627786 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -9,9 +9,10 @@ import { useToast } from "@/lib/toast"; import { useI18n } from "@/lib/i18n"; import { resolveAuthNext } from "@/lib/authNext"; import SiteShell from "@/components/SiteShell"; +import { GoogleSignInButton } from "@/components/GoogleSignInButton"; function LoginForm() { - const { login, register, demoLogin } = useAuth(); + const { login, register, demoLogin, googleLogin } = useAuth(); const { toast } = useToast(); const { t } = useI18n(); const router = useRouter(); @@ -65,6 +66,15 @@ function LoginForm() { if (ok) goAfterAuth(t.login.demoToast); }; + const handleGoogle = async (idToken: string) => { + setError(""); + setLoading(true); + const ok = await googleLogin(idToken); + setLoading(false); + if (ok) goAfterAuth(meta.welcomeToast); + else setError(t.login.googleFail); + }; + const headerHint = mode === "login" ? meta.hint : t.login.registerHint; return ( @@ -145,6 +155,7 @@ function LoginForm() {
{t.common.loading}
+加载中…
}> +{t.login.processing}
} +{t.join.payMethods}
} {!user && {t.nav.login}} diff --git a/frontend/src/components/NewsletterSubscribe.tsx b/frontend/src/components/NewsletterSubscribe.tsx index dd6e9d4..5ad1512 100644 --- a/frontend/src/components/NewsletterSubscribe.tsx +++ b/frontend/src/components/NewsletterSubscribe.tsx @@ -14,7 +14,7 @@ export default function NewsletterSubscribe({ source = "meetups" }: { source?: s const submit = async () => { const value = email.trim(); if (!value || !value.includes("@")) { - toast("请输入有效邮箱", "info"); + toast(t.newsletter.invalidEmail, "info"); return; } setBusy(true); @@ -46,7 +46,7 @@ export default function NewsletterSubscribe({ source = "meetups" }: { source?: s {busy ? "…" : t.newsletter.submit} -先登记邮箱;邮件推送开通后会同步发送,不会假装已群发
+{t.newsletter.note}
); } diff --git a/frontend/src/components/PricingClient.tsx b/frontend/src/components/PricingClient.tsx index e950d9d..54fcec2 100644 --- a/frontend/src/components/PricingClient.tsx +++ b/frontend/src/components/PricingClient.tsx @@ -19,7 +19,7 @@ export default function PricingClient() {{t.pricing.freeDesc}
{t.pricing.payNote}
{t("payment.buySub")}
+{buySub}
{error ?{error}
: null}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index c2df3c3..dd13269 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -163,11 +163,13 @@ export const api = {
return res.json() as Promise<{ success: boolean; url: string; object_key: string }>;
},
ebookStatus: (token: string) =>
- fetchAPI<{ owned: boolean; amount_fen: number; amount_yuan: string }>("/ebook/status", {
+ fetchAPI<{ owned: boolean; has_files?: boolean; amount_fen: number; amount_yuan: string }>("/ebook/status", {
headers: { Authorization: `Bearer ${token}` },
}),
+ ebookProduct: () =>
+ fetchAPI<{ has_files: boolean; amount_fen: number; amount_yuan: string }>("/ebook/product"),
ebookDownloads: (token: string) =>
- fetchAPI<{ owned: boolean; files: { format: string; label: string; url: string }[] }>("/ebook/downloads", {
+ fetchAPI<{ owned: boolean; has_files?: boolean; files: { format: string; label: string; url: string }[] }>("/ebook/downloads", {
headers: { Authorization: `Bearer ${token}` },
}),
getFavorites: (token: string) =>
diff --git a/frontend/src/lib/auth.tsx b/frontend/src/lib/auth.tsx
index 82eaba9..edb0873 100644
--- a/frontend/src/lib/auth.tsx
+++ b/frontend/src/lib/auth.tsx
@@ -13,6 +13,7 @@ interface AuthCtx {
login: (email: string, password: string) => Promise