产品体验修复:P0 阻塞 + P1/P2 改善(产品审查 14 项中修复 10 项)

P0 阻塞:
- Pro 加载不再阻塞整个看板:只显示精简顶栏 + 加载状态,不再遮盖整个页面
- Scan 失败加重试按钮:所有用户(含免费)均可手动重试
- Detail Panel 同步超时后显示提示:"同步时间较长,当前展示的数据可能不完整"

P1 体验:
- 登录页增加"忘记密码"链接:调用 Supabase 密码重置流程
- 登录失败/邮箱未验证增加明确提示:"如刚注册,请先点击验证链接"
- 公告横幅增加 ✕ 关闭按钮:点击后永久隐藏

P2 改善:
- entitlement-required 页面重设计:品牌化、增加返回首页和登录按钮
- 订阅帮助页中英双语:所有 FAQ 和 UI 文案支持中英文切换
- 新增产品审查文档 docs/product-review-jun-2026.md

Tested: npx tsc --noEmit
This commit is contained in:
2569718930@qq.com
2026-05-10 16:13:34 +08:00
parent 9635387beb
commit de0f037bf4
8 changed files with 392 additions and 46 deletions
+63 -16
View File
@@ -1,3 +1,5 @@
import Link from "next/link";
type Props = {
searchParams?: Promise<{ next?: string }>;
};
@@ -21,37 +23,82 @@ export default async function EntitlementRequiredPage({ searchParams }: Props) {
<section
style={{
width: "100%",
maxWidth: 720,
maxWidth: 480,
border: "1px solid rgba(68, 92, 140, 0.45)",
borderRadius: 16,
padding: 24,
background: "rgba(9, 18, 36, 0.88)",
boxShadow: "0 20px 50px rgba(0, 0, 0, 0.35)",
textAlign: "center",
}}
>
<h1 style={{ margin: 0, fontSize: 28, lineHeight: 1.2 }}>
Entitlement Required
<h1
style={{
margin: 0,
fontSize: 22,
lineHeight: 1.3,
fontWeight: 800,
}}
>
访
</h1>
<p style={{ marginTop: 12, color: "#9fb2da", lineHeight: 1.6 }}>
This dashboard is protected. If Supabase auth is enabled, please go to{" "}
<a
<br />
Sign in required to access this page.
</p>
<div style={{ marginTop: 20, display: "flex", gap: 12, justifyContent: "center", flexWrap: "wrap" }}>
<Link
href={`/auth/login?next=${encodeURIComponent(nextPath)}`}
style={{
color: "#8fc5ff",
display: "inline-flex",
minHeight: 36,
alignItems: "center",
gap: 6,
minHeight: 40,
padding: "8px 20px",
borderRadius: 12,
background: "linear-gradient(135deg, #2563EB, #4F46E5)",
color: "#fff",
fontWeight: 700,
textDecoration: "none",
fontSize: 14,
}}
>
/auth/login
</a>{" "}
to sign in first.
</p>
<p style={{ marginTop: 12, color: "#9fb2da", lineHeight: 1.6 }}>
Legacy mode still supports <code>?access_token=&lt;your-token&gt;</code>.
</p>
<p style={{ marginTop: 12, color: "#9fb2da", lineHeight: 1.6 }}>
Requested path: <code>{nextPath}</code>
/ Sign in
</Link>
<Link
href="/"
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
minHeight: 40,
padding: "8px 20px",
borderRadius: 12,
border: "1px solid rgba(68, 92, 140, 0.45)",
background: "rgba(68, 92, 140, 0.2)",
color: "#d6e2ff",
fontWeight: 600,
textDecoration: "none",
fontSize: 14,
}}
>
/ Back to Home
</Link>
</div>
<p
style={{
marginTop: 20,
fontSize: 12,
color: "#7891b5",
lineHeight: 1.5,
}}
>
<code style={{ background: "rgba(255,255,255,0.06)", padding: "2px 6px", borderRadius: 4 }}>?access_token=&lt;your-token&gt;</code>
<br />
<span style={{ marginTop: 4, display: "inline-block" }}>
/ Requested path: <code>{nextPath}</code>
</span>
</p>
</section>
</main>
+53 -29
View File
@@ -1,5 +1,7 @@
import type { Metadata } from "next";
"use client";
import Link from "next/link";
import { useMemo } from "react";
import {
ArrowLeft,
CheckCircle2,
@@ -8,11 +10,7 @@ import {
MessageSquare,
ShieldCheck,
} from "lucide-react";
export const metadata: Metadata = {
title: "PolyWeather | 订阅说明",
description: "PolyWeather Pro 订阅、积分抵扣与支付方式说明。",
};
import { useI18n } from "@/hooks/useI18n";
const TELEGRAM_GROUP_URL = String(
process.env.NEXT_PUBLIC_TELEGRAM_GROUP_URL ||
@@ -21,24 +19,50 @@ const TELEGRAM_GROUP_URL = String(
const FAQ_ITEMS = [
{
q: "Pro 包含哪些功能?",
a: "开通后可解锁:今日日内机场报文规则分析(含高温时段)、历史对账 + 未来日期分析、全平台智能气象推送。",
q_zh: "Pro 包含哪些功能?",
q_en: "What features does Pro include?",
a_zh: "开通后可解锁:今日日内机场报文规则分析(含高温时段)、历史对账 + 未来日期分析、全平台智能气象推送。",
a_en: "Unlocks: intraday METAR rule analysis (including peak window), historical reconciliation + future-date analysis, and cross-platform smart weather push.",
},
{
q: "当前订阅价格是多少?",
a: "目前仅提供月付:5 USDC / 30 天。",
q_zh: "当前订阅价格是多少?",
q_en: "What is the current subscription price?",
a_zh: "目前仅提供月付:5 USDC / 30 天。",
a_en: "Monthly plan only: 5 USDC / 30 days.",
},
{
q: "积分如何抵扣?",
a: "满 500 积分起兑,每 500 积分抵 1U,单次最多抵 3U。",
q_zh: "积分如何抵扣?",
q_en: "How do points work for discounts?",
a_zh: "满 500 积分起兑,每 500 积分抵 1U,单次最多抵 3U。",
a_en: "500 points minimum — every 500 points = 1 USDC off, up to 3 USDC discount per payment.",
},
{
q: "支持哪些钱包和支付方式?",
a: "支持 EVM 浏览器钱包(MetaMask / OKX / Rabby / Bitget 等)及 WalletConnect 扫码钱包(Trust Wallet / Binance Web3 Wallet / TokenPocket 等)。",
q_zh: "支持哪些钱包和支付方式?",
q_en: "Which wallets and payment methods are supported?",
a_zh: "支持 EVM 浏览器钱包(MetaMask / OKX / Rabby / Bitget 等)及 WalletConnect 扫码钱包(Trust Wallet / Binance Web3 Wallet / TokenPocket 等)。",
a_en: "EVM browser wallets (MetaMask, OKX, Rabby, Bitget, etc.) and WalletConnect-compatible wallets (Trust Wallet, Binance Web3 Wallet, TokenPocket, etc.).",
},
];
export default function SubscriptionHelpPage() {
const { locale } = useI18n();
const isEn = locale === "en-US";
const copy = useMemo(() => ({
back: isEn ? "Back to Account" : "返回账户中心",
title: isEn ? "PolyWeather Pro Subscription Guide" : "PolyWeather Pro 订阅说明",
description: isEn
? "Complete subscription rules and payment guide."
: "这里是完整的订阅规则和支付说明。你可以先在页面内绑定钱包,再直接开通 Pro。",
priceLabel: isEn ? "Price" : "订阅价格",
priceText: "5 USDC / 30 " + (isEn ? "Days" : "天"),
discountLabel: isEn ? "Points Discount" : "积分抵扣",
discountText: isEn ? "Up to 3 USDC off" : "最多抵 3U",
communityLabel: isEn ? "Community Points" : "社群积分",
communityLink: isEn ? "Join community to earn" : "加入社群即可赚取积分",
faqTitle: isEn ? "FAQ" : "常见问题",
}), [isEn]);
return (
<main className="min-h-screen bg-[#070d1d] px-4 py-10 text-slate-100">
<div className="mx-auto w-full max-w-4xl">
@@ -47,62 +71,62 @@ export default function SubscriptionHelpPage() {
className="mb-5 inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm text-slate-300 transition hover:bg-white/10"
>
<ArrowLeft size={15} />
{copy.back}
</Link>
<section className="rounded-3xl border border-blue-400/20 bg-gradient-to-b from-[#162541] to-[#0e1730] p-6 md:p-8">
<div className="mb-5 flex items-center gap-3">
<ShieldCheck className="text-cyan-300" size={22} />
<h1 className="text-2xl font-bold md:text-3xl">PolyWeather Pro </h1>
<h1 className="text-2xl font-bold md:text-3xl">{copy.title}</h1>
</div>
<p className="text-sm text-slate-300 md:text-base">
Pro
</p>
<p className="text-sm text-slate-300 md:text-base">{copy.description}</p>
<div className="mt-6 grid gap-3 md:grid-cols-3">
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="mb-2 flex items-center gap-2 text-cyan-300">
<CreditCard size={16} />
<span className="text-sm font-semibold"></span>
<span className="text-sm font-semibold">{copy.priceLabel}</span>
</div>
<p className="text-xl font-bold">5 USDC / 30 </p>
<p className="text-xl font-bold">{copy.priceText}</p>
</div>
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="mb-2 flex items-center gap-2 text-emerald-300">
<Coins size={16} />
<span className="text-sm font-semibold"></span>
<span className="text-sm font-semibold">{copy.discountLabel}</span>
</div>
<p className="text-xl font-bold"> 3U</p>
<p className="text-xl font-bold">{copy.discountText}</p>
</div>
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="mb-2 flex items-center gap-2 text-violet-300">
<MessageSquare size={16} />
<span className="text-sm font-semibold"></span>
<span className="text-sm font-semibold">{copy.communityLabel}</span>
</div>
<Link
href={TELEGRAM_GROUP_URL}
target="_blank"
className="inline-flex min-h-9 items-center text-sm font-semibold text-blue-300 underline decoration-blue-500/50 underline-offset-4"
>
{copy.communityLink}
</Link>
</div>
</div>
</section>
<section className="mt-6 rounded-3xl border border-white/10 bg-[#0f162a]/80 p-6 md:p-8">
<h2 className="mb-4 text-lg font-bold"></h2>
<h2 className="mb-4 text-lg font-bold">{copy.faqTitle}</h2>
<div className="space-y-4">
{FAQ_ITEMS.map((item) => (
<article
key={item.q}
key={item.q_en}
className="rounded-2xl border border-white/10 bg-white/[0.03] p-4"
>
<h3 className="mb-2 flex items-center gap-2 text-sm font-semibold text-blue-300">
<CheckCircle2 size={14} />
{item.q}
{isEn ? item.q_en : item.q_zh}
</h3>
<p className="text-sm leading-6 text-slate-300">{item.a}</p>
<p className="text-sm leading-6 text-slate-300">
{isEn ? item.a_en : item.a_zh}
</p>
</article>
))}
</div>