From 2c038e8c0c63aa545f3328cc55bdf912608a19c3 Mon Sep 17 00:00:00 2001 From: zhu <1812073942@qq.com> Date: Sat, 9 May 2026 15:21:39 +0800 Subject: [PATCH] 1 --- src/app/(home)/components/header/index.tsx | 34 ++++-- src/app/dashboard/(home)/_components/tip.tsx | 120 ++++++++++++++++--- src/app/dashboard/(home)/page.tsx | 1 + src/app/dashboard/_components/header.tsx | 62 ++++++++-- src/app/dashboard/layout.tsx | 9 ++ src/store/subscribe.ts | 91 ++++++++++++++ src/store/user.ts | 4 +- src/utils/format.ts | 14 +++ 8 files changed, 297 insertions(+), 38 deletions(-) create mode 100644 src/store/subscribe.ts create mode 100644 src/utils/format.ts diff --git a/src/app/(home)/components/header/index.tsx b/src/app/(home)/components/header/index.tsx index 6605450..cbb7b8f 100644 --- a/src/app/(home)/components/header/index.tsx +++ b/src/app/(home)/components/header/index.tsx @@ -1,6 +1,10 @@ +"use client" + +import useUserStore from "@/store/user"; import Link from "next/link"; export function Header() { + const userStore = useUserStore(); return (
@@ -17,17 +21,25 @@ export function Header() { className="rounded-md px-3 py-1.5 text-muted-foreground transition-colors hover:text-foreground"> Pricing - - Sign in - - - Start free - + { + userStore.token ? + + Dashboard + : <> + + Sign in + + + Start free + + + }
diff --git a/src/app/dashboard/(home)/_components/tip.tsx b/src/app/dashboard/(home)/_components/tip.tsx index c6b1b7f..a51f0c2 100644 --- a/src/app/dashboard/(home)/_components/tip.tsx +++ b/src/app/dashboard/(home)/_components/tip.tsx @@ -1,40 +1,128 @@ import React from 'react'; -import {AlertTriangle, ArrowRight, Clock, Download, Zap} from "lucide-react"; -import Link from "next/link"; +import Link from 'next/link'; +import {Clock, Zap, ArrowRight, AlertTriangle, Lock, Download} from 'lucide-react'; import useExtensionStore from "@/store/extension"; import {copyText} from "@/utils/helper"; +import useSubscribeStore from "@/store/subscribe"; /** * 订阅提示 * @constructor */ + export const SubscriptionTip = () => { + const {status, remainingSeconds} = useSubscribeStore(); + + // 格式化时间显示 + const formatFullTime = (seconds: number) => { + if (seconds <= 0) return "0h 0m"; + const d = Math.floor(seconds / (3600 * 24)); + const h = Math.floor((seconds % (3600 * 24)) / 3600); + const m = Math.floor((seconds % 3600) / 60); + + if (d > 0) return `${d}d ${h}h ${m}m`; + return `${h}h ${m}m`; + }; + + // 如果是活跃状态(且没取消),则不显示任何提示 + if (status === "active" || status === "") { + return null; + } + + // 根据不同状态配置内容 + const getConfig = () => { + // 1. 试用状态 + if (status === "trial") { + if (remainingSeconds > 0) { + return { + title: `Trial: ${formatFullTime(remainingSeconds)} left — Scan will lock when trial ends`, + desc: "After your free trial ends, the scan function is fully blocked until you subscribe. Subscribe now to lock in scans before time runs out.", + btn: "Upgrade Now", + icon: , + theme: "border-amber-300 bg-amber-50 text-amber-900", + iconBox: "border-amber-300 bg-amber-100 text-amber-700", + btnClass: "bg-amber-600 hover:bg-amber-700 text-white" + }; + } else { + return { + title: "Trial Ended — Scanning Locked", + desc: "Your trial period has expired. All scanning functions and AI insights are now locked. Upgrade to a Pro plan to continue monitoring your store.", + btn: "Unlock Now", + icon: , + theme: "border-rose-300 bg-rose-50 text-rose-900", + iconBox: "border-rose-300 bg-rose-100 text-rose-700", + btnClass: "bg-rose-600 hover:bg-rose-700 text-white" + }; + } + } + + // 2. 支付失败状态 + if (status === "past_due") { + return { + title: "Payment Overdue — Subscription at risk", + desc: "Your last payment was unsuccessful. Please update your payment method to maintain uninterrupted scanning and AI insights.", + btn: "Update Billing", + icon: , + theme: "border-red-300 bg-red-50 text-red-900", + iconBox: "border-red-300 bg-red-100 text-red-700", + btnClass: "bg-red-600 hover:bg-red-700 text-white" + }; + } + + // 3. 已取消订阅但还在有效期内 + if (status === "canceled") { + if (remainingSeconds > 0) { + return { + title: `Subscription Ending: ${formatFullTime(remainingSeconds)} left`, + desc: "You have canceled your subscription. Access to automatic scans and AI reports will be disabled once this period ends.", + btn: "Renew Subscription", + icon: , + theme: "border-slate-300 bg-slate-50 text-slate-900", + iconBox: "border-slate-300 bg-slate-100 text-slate-700", + btnClass: "bg-slate-700 hover:bg-slate-800 text-white" + }; + } else { + return { + title: "Subscription Expired", + desc: "Your subscription period has ended and scans are now locked. Please subscribe again to resume service.", + btn: "Subscribe Now", + icon: , + theme: "border-slate-400 bg-slate-100 text-slate-900", + iconBox: "border-slate-400 bg-slate-200 text-slate-700", + btnClass: "bg-slate-800 hover:bg-slate-900 text-white" + }; + } + } + + return null; + }; + + const config = getConfig(); + if (!config) return null; + return ( -
+
- + className={`mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border transition-colors ${config.iconBox}`}> + {config.icon}
-

- Trial: 23h 1m left — Scan will lock when trial ends +

+ {config.title}

-

- After your free 1-day trial ends, the scan function is fully blocked until you subscribe. - Subscribe now to lock in scans before time runs out. +

+ {config.desc}

- - Upgrade Now - + className={`group inline-flex h-11 shrink-0 items-center justify-center gap-2 rounded-lg px-5 text-sm font-semibold shadow-md transition-all hover:shadow-lg ${config.btnClass}`}> + + {config.btn} +
diff --git a/src/app/dashboard/(home)/page.tsx b/src/app/dashboard/(home)/page.tsx index 562299f..36ea195 100644 --- a/src/app/dashboard/(home)/page.tsx +++ b/src/app/dashboard/(home)/page.tsx @@ -7,6 +7,7 @@ import TopActions from "./_components/result/top-actions"; import TodayMetrics from "./_components/result/today-metrics"; const Page = () => { + return ( <> diff --git a/src/app/dashboard/_components/header.tsx b/src/app/dashboard/_components/header.tsx index 0177910..1d738ae 100644 --- a/src/app/dashboard/_components/header.tsx +++ b/src/app/dashboard/_components/header.tsx @@ -1,11 +1,51 @@ "use client" import Link from "next/link"; -import React from 'react'; -import {CheckCircle2, CreditCard, LayoutGrid, LogOut, SettingsIcon} from "lucide-react"; +import React, {useEffect} from 'react'; +import {AlertCircle, CheckCircle2, Clock, CreditCard, LayoutGrid, LogOut, SettingsIcon, XCircle} from "lucide-react"; import {usePathname} from "next/navigation"; +import useUserStore from "@/store/user"; +import useSubscribeStore from "@/store/subscribe"; +import {formatSecond} from "@/utils/format"; function Header() { + const {status, remainingSeconds} = useSubscribeStore(); + + + // 2. 根据状态获取配置(颜色、图标、文本) + const getStatusConfig = () => { + switch (status) { + case "trial": + return { + label: `Trial · ${formatSecond(remainingSeconds)}`, + className: "border-blue-200 bg-blue-50 text-blue-800 hover:bg-blue-100", + icon: + }; + case "active": + return { + label: "Active", + className: "border-emerald-200 bg-emerald-50 text-emerald-800 hover:bg-emerald-100", + icon: + }; + case "past_due": + return { + label: "Payment Due", + className: "border-amber-200 bg-amber-50 text-amber-800 hover:bg-amber-100", + icon: + }; + case "canceled": + return { + label: remainingSeconds > 0 ? `Ends in ${formatSecond(remainingSeconds)}` : "Expired", + className: "border-slate-200 bg-slate-50 text-slate-800 hover:bg-slate-100", + icon: + }; + default: + return null; // 未加载时不显示 + } + }; + + const config = getStatusConfig(); + return (
@@ -24,11 +64,13 @@ function Header() {
- - - 测试 - + {config && ( + + {config.icon} + {config.label} + + )}
@@ -83,14 +125,16 @@ const NavTabs = () => { * 用户信息 */ const UserMenu = () => { + const userStore = useUserStore(); return (
- 112@qq.com + {userStore.user?.email}
+ title="Sign out" + onClick={() => userStore.logout()}> Sign out
diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index c895ed1..e5017cc 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,4 +1,8 @@ +"use client" + +import useSubscribeStore from "@/store/subscribe"; import Header from "./_components/header"; +import {useEffect} from "react"; interface Props { children: React.ReactNode; @@ -6,6 +10,11 @@ interface Props { export default function DashboardLayout({children}: Props) { + const {init} = useSubscribeStore(); + useEffect(() => { + init(); + }, []); + return (
diff --git a/src/store/subscribe.ts b/src/store/subscribe.ts new file mode 100644 index 0000000..c3c99cd --- /dev/null +++ b/src/store/subscribe.ts @@ -0,0 +1,91 @@ +import {create} from "zustand"; +import {getBrandStatusApi} from "@/api/set"; + +// 订阅状态类型 +type SubscriptionStatus = "trial" | "active" | "past_due" | "canceled" | ""; + +type SubscribeState = { + status: SubscriptionStatus; + endTime: string; // 目标结束时间字符串 + remainingSeconds: number; // 剩余秒数(手动计时用) + init: () => Promise; + _startTimer: () => void; // 内部启动定时器方法 + _timer: any; // 内部定时器引用 +} + +const useSubscribeStore = create((set, get) => ({ + status: "", + endTime: "", + remainingSeconds: 0, + _timer: null, + + /** + * 初始化:获取数据并启动计时 + */ + init: async () => { + try { + const res: any = await getBrandStatusApi(); + const brand = res.brand; + + if (!brand) return; + + // 逻辑优先级:试用结束时间 > 订阅取消时间 + const targetTime = brand.trial_ends_at || brand.subscription_cancel_at || ""; + + set({ + status: (brand.subscription_status as SubscriptionStatus) || "", + endTime: targetTime, + }); + + // 拆分逻辑:数据就绪后,启动内部计时器 + get()._startTimer(); + + } catch (error) { + console.error("Failed to init subscribe store:", error); + } + }, + + /** + * 内部方法:管理定时器生命周期 + */ + _startTimer: () => { + const {_timer, endTime} = get(); + + // 1. 清理旧的定时器,防止叠加 + if (_timer) { + clearInterval(_timer); + set({_timer: null}); + } + + // 2. 如果没有结束时间,直接归零并返回 + if (!endTime) { + set({remainingSeconds: 0}); + return; + } + + // 3. 定义每秒执行的计算逻辑 + const tick = () => { + const now = Math.floor(Date.now() / 1000); + const end = Math.floor(new Date(endTime).getTime() / 1000); + const diff = Math.max(0, end - now); + + set({remainingSeconds: diff}); + + // 如果倒计时结束,清理自己 + if (diff <= 0) { + const currentTimer = get()._timer; + if (currentTimer) clearInterval(currentTimer); + set({_timer: null}); + } + }; + + // 4. 立即执行一次,避免首秒空白 + tick(); + + // 5. 启动新定时器并记录引用 + const newTimer = setInterval(tick, 1000); + set({_timer: newTimer}); + }, +})); + +export default useSubscribeStore; \ No newline at end of file diff --git a/src/store/user.ts b/src/store/user.ts index 2b62ec2..4068bd7 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -6,7 +6,7 @@ type UserState = { user: UserInfo | null, setToken: (token: string) => void; setUser: (user: UserInfo | null) => void; - clearToken: () => void; + logout: () => void; } type UserInfo = { @@ -21,8 +21,8 @@ const useUserStore = create()( token: "", user: null, setToken: (token) => set({token}), - clearToken: () => set({token: ""}), setUser: (user) => set({user}), + logout: () => set({token: "", user: null}), }), { name: "user-storage", diff --git a/src/utils/format.ts b/src/utils/format.ts new file mode 100644 index 0000000..7c6ea5c --- /dev/null +++ b/src/utils/format.ts @@ -0,0 +1,14 @@ +/** + * 格式化秒为hh:mm:ss + */ +export function formatSecond(seconds: number) { + if (seconds <= 0) return "Expired"; + + const d = Math.floor(seconds / (3600 * 24)); + const h = Math.floor((seconds % (3600 * 24)) / 3600); + const m = Math.floor((seconds % 3600) / 60); + + if (d > 0) return `${d}d ${h}h`; // 大于1天显示 天+小时 + if (h > 0) return `${h}h ${m}m`; // 小于1天显示 小时+分钟 + return `<1h`; // 小于1小时显示 <1h +} \ No newline at end of file