This commit is contained in:
zhu
2026-05-09 15:21:39 +08:00
parent 521eea47d2
commit 2c038e8c0c
8 changed files with 297 additions and 38 deletions

View File

@@ -1,6 +1,10 @@
"use client"
import useUserStore from "@/store/user";
import Link from "next/link";
export function Header() {
const userStore = useUserStore();
return (
<header className="sticky top-0 z-30 w-full border-b border-border/60 bg-background/80 backdrop-blur">
<div className="container mx-auto flex h-14 max-w-6xl items-center justify-between px-4 md:px-6">
@@ -17,17 +21,25 @@ export function Header() {
className="rounded-md px-3 py-1.5 text-muted-foreground transition-colors hover:text-foreground">
Pricing
</Link>
<Link
href="/login"
className="rounded-md px-3 py-1.5 text-muted-foreground transition-colors hover:text-foreground"
>
Sign in
</Link>
<Link
href="/signup"
className="ml-1 rounded-md bg-foreground px-3 py-1.5 text-sm font-medium text-background transition-opacity hover:opacity-90">
Start free
</Link>
{
userStore.token ?
<Link
href="/dashboard"
className="ml-1 rounded-md bg-foreground px-3 py-1.5 text-sm font-medium text-background transition-opacity hover:opacity-90">
Dashboard
</Link> : <>
<Link
href="/login"
className="rounded-md px-3 py-1.5 text-muted-foreground transition-colors hover:text-foreground">
Sign in
</Link>
<Link
href="/signup"
className="ml-1 rounded-md bg-foreground px-3 py-1.5 text-sm font-medium text-background transition-opacity hover:opacity-90">
Start free
</Link>
</>
}
</nav>
</div>
</header>

View File

@@ -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: <Clock className="h-5 w-5"/>,
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: <Lock className="h-5 w-5"/>,
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: <AlertTriangle className="h-5 w-5"/>,
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: <Clock className="h-5 w-5"/>,
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: <Lock className="h-5 w-5"/>,
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 (
<div
className={"mb-6 overflow-hidden rounded-xl border-2 shadow-sm border-amber-300 bg-amber-50 text-amber-900 "}>
<div className={`mb-6 overflow-hidden rounded-xl border-2 shadow-sm transition-all ${config.theme}`}>
<div className="flex flex-col gap-4 p-5 sm:flex-row sm:items-center sm:justify-between sm:p-5">
<div className="flex min-w-0 items-start gap-3">
<div
className={`mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-amber-300 bg-amber-100 text-amber-700`}>
<Clock className="h-5 w-5"/>
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}
</div>
<div className="min-w-0">
<p className={`text-base font-semibold leading-tight sm:text-[17px] text-amber-900`}>
Trial: 23h 1m left Scan will lock when trial ends
<p className={`text-base font-semibold leading-tight sm:text-[17px]`}>
{config.title}
</p>
<p className={`mt-1 text-[13px] leading-relaxed `}>
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.
<p className={`mt-1 text-[13px] leading-relaxed opacity-90`}>
{config.desc}
</p>
</div>
</div>
<Link
href="/dashboard/billing"
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 bg-amber-600 text-white hover:bg-amber-700 "}>
<Zap className="h-4 w-4"/>
Upgrade Now
<ArrowRight className="h-4 w-4 "
/>
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}`}>
<Zap className="h-4 w-4 fill-current"/>
{config.btn}
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1"/>
</Link>
</div>
</div>

View File

@@ -7,6 +7,7 @@ import TopActions from "./_components/result/top-actions";
import TodayMetrics from "./_components/result/today-metrics";
const Page = () => {
return (
<>
<InstalledTip/>

View File

@@ -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: <Clock className="h-3 w-3"/>
};
case "active":
return {
label: "Active",
className: "border-emerald-200 bg-emerald-50 text-emerald-800 hover:bg-emerald-100",
icon: <CheckCircle2 className="h-3 w-3"/>
};
case "past_due":
return {
label: "Payment Due",
className: "border-amber-200 bg-amber-50 text-amber-800 hover:bg-amber-100",
icon: <AlertCircle className="h-3 w-3"/>
};
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: <XCircle className="h-3 w-3"/>
};
default:
return null; // 未加载时不显示
}
};
const config = getStatusConfig();
return (
<header className="sticky top-0 z-30 border-b border-border/60 bg-background/85 backdrop-blur-md">
<div className="container mx-auto flex h-14 max-w-6xl items-center justify-between gap-4 px-4 md:px-6">
@@ -24,11 +64,13 @@ function Header() {
</div>
<div className="flex shrink-0 items-center gap-3">
<Link href="/dashboard/billing"
className={`inline-flex h-7 items-center gap-1.5 rounded-full border px-2.5 text-[11px] font-medium transition-colors border-emerald-200 bg-emerald-50 text-emerald-800 hover:bg-emerald-100`}>
<CheckCircle2 className="h-3 w-3"/>
</Link>
{config && (
<Link href="/dashboard/billing"
className={`inline-flex h-7 items-center gap-1.5 rounded-full border px-2.5 text-[11px] font-medium transition-colors ${config.className}`}>
{config.icon}
{config.label}
</Link>
)}
<span className="hidden h-5 w-px bg-border/60 md:inline-block" aria-hidden="true"/>
<UserMenu/>
</div>
@@ -83,14 +125,16 @@ const NavTabs = () => {
* 用户信息
*/
const UserMenu = () => {
const userStore = useUserStore();
return (
<div className="flex items-center gap-2">
<div className="hidden max-w-50 truncate text-xs text-muted-foreground md:inline">
112@qq.com
{userStore.user?.email}
</div>
<div
className="cursor-pointer inline-flex h-7 items-center gap-1 rounded-md px-2 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
title="Sign out">
title="Sign out"
onClick={() => userStore.logout()}>
<LogOut className="h-3 w-3"/>
<span className="hidden sm:inline">Sign out</span>
</div>

View File

@@ -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 (
<div className="storeai-dashboard relative min-h-screen bg-background">
<Header/>

91
src/store/subscribe.ts Normal file
View File

@@ -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<void>;
_startTimer: () => void; // 内部启动定时器方法
_timer: any; // 内部定时器引用
}
const useSubscribeStore = create<SubscribeState>((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;

View File

@@ -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<UserState>()(
token: "",
user: null,
setToken: (token) => set({token}),
clearToken: () => set({token: ""}),
setUser: (user) => set({user}),
logout: () => set({token: "", user: null}),
}),
{
name: "user-storage",

14
src/utils/format.ts Normal file
View File

@@ -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
}