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