diff --git a/frontend/components/account/AccountCenter.tsx b/frontend/components/account/AccountCenter.tsx index 4007aae5..354cd818 100644 --- a/frontend/components/account/AccountCenter.tsx +++ b/frontend/components/account/AccountCenter.tsx @@ -4,25 +4,29 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import type { User } from "@supabase/supabase-js"; +import type { LucideIcon } from "lucide-react"; import { - ArrowLeft, Bot, CheckCircle2, + ChevronLeft, + Clock, Copy, - KeyRound, + Crown, + Fingerprint, + Hash, Loader2, LogIn, LogOut, + Mail, RefreshCw, - ShieldCheck, - UserCircle2, + Shield, + User as UserIcon, + UserCheck, } from "lucide-react"; -import { useI18n } from "@/hooks/useI18n"; import { getSupabaseBrowserClient, hasSupabasePublicEnv, } from "@/lib/supabase/client"; -import styles from "./AccountCenter.module.css"; type AuthMeResponse = { authenticated?: boolean; @@ -34,11 +38,18 @@ type AuthMeResponse = { subscription_active?: boolean | null; }; +type InfoItemProps = { + icon: LucideIcon; + label: string; + value: string; + status?: "default" | "primary"; +}; + function formatTime(value: string | undefined | null, locale: string) { - if (!value) return ""; + if (!value) return "--"; try { const dt = new Date(value); - if (Number.isNaN(dt.getTime())) return ""; + if (Number.isNaN(dt.getTime())) return "--"; return new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", @@ -47,7 +58,7 @@ function formatTime(value: string | undefined | null, locale: string) { minute: "2-digit", }).format(dt); } catch { - return ""; + return "--"; } } @@ -61,8 +72,27 @@ function normalizeProvider(user: User | null) { return ""; } +function InfoItem({ icon: Icon, label, value, status = "default" }: InfoItemProps) { + return ( +
+
+
+ +
+ {label} +
+ + {value} + +
+ ); +} + export function AccountCenter() { - const { locale, t } = useI18n(); const router = useRouter(); const [loading, setLoading] = useState(true); @@ -91,9 +121,10 @@ export function AccountCenter() { setUser(userResult.data?.user ?? null); if (!backendResult.ok) { - const raw = (await backendResult.text()).slice(0, 240); + const raw = (await backendResult.text()).slice(0, 260); throw new Error(`HTTP ${backendResult.status} ${raw}`.trim()); } + const backendJson = (await backendResult.json()) as AuthMeResponse; setBackend(backendJson); setUpdatedAt(new Date().toISOString()); @@ -124,42 +155,54 @@ export function AccountCenter() { const onSignOut = async () => { if (supabaseReady) { try { - const supabase = getSupabaseBrowserClient(); - await supabase.auth.signOut(); + await getSupabaseBrowserClient().auth.signOut(); } catch {} } - router.replace("/auth/login"); + router.replace("/"); }; const userId = backend?.user_id || user?.id || ""; const isAuthenticated = Boolean(userId); const email = backend?.email || user?.email || ""; - const providerRaw = normalizeProvider(user); - const provider = providerRaw ? providerRaw.toUpperCase() : t("account.na"); - const lastSignIn = formatTime(user?.last_sign_in_at, locale) || t("account.na"); - const updatedAtLabel = formatTime(updatedAt, locale) || t("account.na"); const displayName = String(user?.user_metadata?.full_name || "").trim() || (email ? String(email).split("@")[0] : "") || - t("account.guestName"); - const initials = displayName.slice(0, 2).toUpperCase(); + "PolyWeather 用户"; + const initials = (displayName.slice(0, 2) || "PW").toUpperCase(); + const providerRaw = normalizeProvider(user); + const provider = providerRaw ? providerRaw.toUpperCase() : "--"; + const lastSignIn = formatTime( + user?.last_sign_in_at, + typeof navigator !== "undefined" ? navigator.language : "zh-CN", + ); + const updatedAtLabel = formatTime( + updatedAt, + typeof navigator !== "undefined" ? navigator.language : "zh-CN", + ); const modeLabel = useMemo(() => { const mode = String(backend?.entitlement_mode || "").trim().toLowerCase(); - if (mode === "supabase_required") return t("account.mode.supabaseRequired"); - if (mode === "supabase_optional") return t("account.mode.supabaseOptional"); - if (mode === "supabase") return t("account.mode.supabase"); - if (mode === "legacy_token") return t("account.mode.legacy"); - if (mode === "disabled") return t("account.mode.disabled"); - return t("account.mode.unknown"); - }, [backend?.entitlement_mode, t]); + if (mode === "supabase_required") return "Supabase 强制登录"; + if (mode === "supabase_optional") return "Supabase 可选登录"; + if (mode === "legacy_token") return "Legacy Token 鉴权"; + if (mode === "disabled") return "未启用鉴权"; + return "未知模式"; + }, [backend?.entitlement_mode]); - const subscriptionLabel = useMemo(() => { - if (!backend?.subscription_required) return t("account.subscription.notRequired"); - if (backend.subscription_active === true) return t("account.subscription.active"); - if (backend.subscription_active === false) return t("account.subscription.inactive"); - return t("account.subscription.unknown"); - }, [backend?.subscription_active, backend?.subscription_required, t]); + const backendStatus = useMemo(() => { + if (backend?.authenticated) return "通过"; + if (backend?.auth_required) return "未登录"; + return "游客模式"; + }, [backend?.authenticated, backend?.auth_required]); + + const subscriptionRequirement = backend?.subscription_required + ? "已启用订阅校验" + : "当前未强制订阅"; + const subscriptionResult = !backend?.subscription_required + ? "当前未强制订阅" + : backend?.subscription_active + ? "有效订阅" + : "无有效订阅"; const bindCommand = userId ? `/bind ${userId}${email ? ` ${email}` : ""}` @@ -169,185 +212,187 @@ export function AccountCenter() { try { await navigator.clipboard.writeText(bindCommand); setCopied(true); - window.setTimeout(() => setCopied(false), 1300); + window.setTimeout(() => setCopied(false), 2000); } catch {} }; return ( -
-
-
-
-
-
-

{t("account.title")}

-

{t("account.subtitle")}

+
+
+
+ +
+
+
+

+ 账户中心 +

+

+ 管理您的身份、权限与 Bot 绑定 +

-
- - - {t("account.backDashboard")} +
+ + 返回看板 {isAuthenticated ? ( - ) : ( - - - {t("account.signIn")} + + 登录 / 注册 )}
-
-
{initials || "PW"}
-
-

{displayName}

-

{email || t("account.na")}

-
- {isAuthenticated ? ( - <> - - - {t("account.authenticated")} - - {backend?.subscription_active ? ( - - - {t("account.subscriptionActive")} - - ) : backend?.subscription_required ? ( - - - {t("account.subscriptionRequired")} - - ) : ( - - - {t("account.subscriptionUnknown")} - - )} - - ) : ( - - - {t("account.guest")} - - )} -
-
-
{t("account.updatedAt", { time: updatedAtLabel })}
-
- - {loading ? ( -
- - {t("account.loading")} -
- ) : null} - {errorText ? ( -
- {t("account.error", { message: errorText })} -
+
+ 加载失败: {errorText} +
) : null} -
-
-

- - {t("account.card.membership")} -

-
-
-
{t("account.field.mode")}
-
{modeLabel}
-
-
-
{t("account.field.backendStatus")}
-
- {backend?.authenticated - ? t("account.backend.ok") - : backend?.auth_required - ? t("account.backend.fail") - : t("account.guest")} -
-
-
-
{t("account.field.requirement")}
-
- {backend?.subscription_required - ? t("account.subscriptionRequired") - : t("account.subscription.notRequired")} -
-
-
-
{t("account.field.subscription")}
-
{subscriptionLabel}
-
-
-
- -
-

- - {t("account.card.identity")} -

-
-
-
{t("account.field.email")}
-
{email || t("account.na")}
-
-
-
{t("account.field.userId")}
-
{userId || t("account.na")}
-
-
-
{t("account.field.provider")}
-
{provider}
-
-
-
{t("account.field.lastSignIn")}
-
{lastSignIn}
-
-
-
- -
-

- - {t("account.card.bot")} -

-

{t("account.field.bindHint")}

-
- {bindCommand} - -
-
-
- {!supabaseReady ? ( -
- - NEXT_PUBLIC_SUPABASE_URL / ANON_KEY is not configured. -
+
+ NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY 未配置。 +
) : null} + +
+
+
+ + 最近同步: {updatedAtLabel} + +
+ +
+
+
+ {initials} +
+
+
+ +
+
+

{displayName}

+ + + {isAuthenticated ? "已登录" : "游客模式"} + +
+

{email || "--"}

+
+ +
+
+

+ 当前角色 +

+

+ Free Tier +

+
+
+
+
+ +
+
+

+ 会员与权限 +

+
+ + + + +
+
+
+ +
+
+

+ 身份信息 +

+
+ + + + +
+
+
+ +
+
+
+ +
+ +
+

+ Bot 绑定 +

+

+ 将下面命令发送至 Telegram Bot,即可把网页账户与机器人权限绑定,实现全平台气象推送。 +

+ +
+
+ {bindCommand} +
+ +
+
+
+
+
+ +
-
+ ); } diff --git a/frontend/components/auth/LoginClient.tsx b/frontend/components/auth/LoginClient.tsx index c8dba5ce..e8c13233 100644 --- a/frontend/components/auth/LoginClient.tsx +++ b/frontend/components/auth/LoginClient.tsx @@ -2,6 +2,15 @@ import { FormEvent, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; +import { + ArrowRight, + Chrome, + Cloud, + CloudRain, + Lock, + Mail, + Sun, +} from "lucide-react"; import { getSupabaseBrowserClient, hasSupabasePublicEnv, @@ -119,154 +128,128 @@ export function LoginClient({ nextPath }: LoginClientProps) { } }; + const isLogin = mode === "login"; + return ( -
-
-

PolyWeather 登录

-

- 优先推荐 Google 一键登录,邮箱注册/登录可并行使用。 -

+
+
+
+ +
+
+
+ +
+

PolyWeather

+

探索世界每一个角落的气象细节

+
-
- 或使用邮箱 {mode === "login" ? "登录" : "注册"} +
+
+ + 或使用邮箱 + +
-
+
-
void onEmailSubmit(event)}> - setEmail(event.target.value)} - placeholder="you@example.com" - style={{ - width: "100%", - marginBottom: 10, - padding: "12px", - borderRadius: 8, - border: "1px solid rgba(116, 148, 206, 0.4)", - background: "rgba(10, 23, 47, 0.92)", - color: "#e6f0ff", - }} - /> - setPassword(event.target.value)} - placeholder="至少 6 位密码" - style={{ - width: "100%", - marginBottom: 12, - padding: "12px", - borderRadius: 8, - border: "1px solid rgba(116, 148, 206, 0.4)", - background: "rgba(10, 23, 47, 0.92)", - color: "#e6f0ff", - }} - /> + void onEmailSubmit(event)} className="space-y-4"> +
+ + setEmail(event.target.value)} + placeholder="you@example.com" + className="w-full rounded-xl border border-white/10 bg-white/5 py-3.5 pl-12 pr-4 text-white placeholder:text-slate-600 transition-all focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50" + /> +
+
+ + setPassword(event.target.value)} + placeholder={isLogin ? "输入密码" : "设置至少 6 位密码"} + className="w-full rounded-xl border border-white/10 bg-white/5 py-3.5 pl-12 pr-4 text-white placeholder:text-slate-600 transition-all focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50" + /> +
+
- {errorText ? ( -

{errorText}

- ) : null} - {infoText ? ( -

{infoText}

- ) : null} + {errorText ?

{errorText}

: null} + {infoText ?

{infoText}

: null} -

- 登录后将跳转到: {nextPath} +

+

+ {isLogin ? "登录后将为您个性化定制首页数据" : "注册即代表同意我们的服务条款"} +

+
+ +

+ 登录后跳转到: {nextPath}

-
-
+ + {!supabaseReady ? ( +

+ Supabase 未配置,无法使用登录 +

+ ) : null} + + +
+ + 实时数据 + + + 高精度预测 + +
+ ); } -