From f59e929843d8cfdedce20170d8ac2d7155117ab6 Mon Sep 17 00:00:00 2001 From: Eamon <17516219072@163.com> Date: Fri, 4 Sep 2026 13:21:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=B8=AA=E4=BA=BA=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lock | 3 + package.json | 3 +- src/admin/clients.tsx | 16 +- src/admin/dashboard/mock.ts | 38 ---- src/admin/dashboard/page.tsx | 179 +++++++++--------- src/admin/effects.tsx | 53 +----- src/admin/layout-context.tsx | 27 --- src/admin/layout.tsx | 5 +- src/admin/refundOrders/page.tsx | 2 +- src/api/dashboard.ts | 44 +---- src/api/token.ts | 2 + src/api/user.ts | 23 ++- src/components/authGuard.tsx | 11 +- .../composites/dialogs/changePwdDialog.tsx | 170 +++++++++++++++++ .../composites/dialogs/editInfoDialog.tsx | 138 +++++--------- src/components/profileOrLogin.tsx | 15 +- src/components/stores/app.ts | 11 ++ src/components/stores/client.ts | 54 ++++++ src/components/stores/dashboard.ts | 41 ++++ src/components/stores/layout.ts | 20 ++ src/components/stores/profile.ts | 34 ++++ src/home/header.tsx | 20 +- src/home/index.tsx | 7 +- src/home/product/productGrid.tsx | 13 +- src/lib/hooks/useMediaQuery.ts | 36 ---- src/lib/models/dashboard.ts | 58 ++++++ src/main.tsx | 5 + 27 files changed, 614 insertions(+), 414 deletions(-) delete mode 100644 src/admin/dashboard/mock.ts delete mode 100644 src/admin/layout-context.tsx create mode 100644 src/components/composites/dialogs/changePwdDialog.tsx create mode 100644 src/components/stores/app.ts create mode 100644 src/components/stores/client.ts create mode 100644 src/components/stores/dashboard.ts create mode 100644 src/components/stores/layout.ts create mode 100644 src/components/stores/profile.ts delete mode 100644 src/lib/hooks/useMediaQuery.ts diff --git a/bun.lock b/bun.lock index 9beb093..3562882 100644 --- a/bun.lock +++ b/bun.lock @@ -27,6 +27,7 @@ "tailwindcss": "^4.3.0", "tw-animate-css": "^1.4.0", "zod": "^4.4.3", + "zustand": "^5.0.15", }, "devDependencies": { "@biomejs/biome": "2.4.16", @@ -1258,6 +1259,8 @@ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "zustand": ["zustand@5.0.15", "https://registry.npmmirror.com/zustand/-/zustand-5.0.15.tgz", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A=="], + "zwitch": ["zwitch@2.0.4", "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "@dotenvx/dotenvx/commander": ["commander@11.1.0", "https://registry.npmmirror.com/commander/-/commander-11.1.0.tgz", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], diff --git a/package.json b/package.json index 8e4a25f..b849abd 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.0", "tw-animate-css": "^1.4.0", - "zod": "^4.4.3" + "zod": "^4.4.3", + "zustand": "^5.0.15" }, "devDependencies": { "@biomejs/biome": "2.4.16", diff --git a/src/admin/clients.tsx b/src/admin/clients.tsx index 20965dc..dc2250f 100644 --- a/src/admin/clients.tsx +++ b/src/admin/clients.tsx @@ -18,6 +18,7 @@ import { import { type ReactNode, useState } from "react" import { Link } from "react-router-dom" import ProfileOrLogin from "@/components/profileOrLogin" +import { useLayoutStore } from "@/components/stores/layout" import { Button } from "@/components/ui/button" import { Tooltip, @@ -27,11 +28,10 @@ import { } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" import logoAvatar from "./assets/logo-avatar.svg" -import { useLayout } from "./layout-context" import { AdminNavMenu } from "./nav-menu" export function Shell(props: { children: ReactNode }) { - const { navbar } = useLayout() + const navbar = useLayoutStore(state => state.navbar) return (
state.navbar) return (
state.navbar) + const setNavbar = useLayoutStore(state => state.setNavbar) return ( - {managerQrUrl && ( -
- -
- )} -
-
-

手机号/用户名

- {phone || accountInfo.phone} + {accountInfo.phone || phone}

@@ -227,7 +221,7 @@ export default function DashboardPage() { ********
-

退款订单管理

+

退货订单管理

> { - return fetchHttpBalance() -} - -export async function fetchDashboardConsumption( - params: Record, -): Promise>> { - return await callByUser>( - "/api/course/v1/order/UserConsumeStatistics", - params, - "GET", - ) -} - -// TODO: 账号信息/IP 统计接口路径待后端确认 -export async function fetchDashboardAccountInfo(): Promise< - ApiResponse> +export async function fetchApiIndex(): Promise< + ApiResponse > { - return await callByUser>( - "/api/baseinfo/v1/user/Info", - undefined, - "GET", - ) -} - -export async function fetchDashboardIpStats(): Promise< - ApiResponse> -> { - return await callByUser>( - "/api/course/v1/order/IpAccountStatistics", + const res = await callPublic( + "/user/ApiIndex", undefined, "GET", ) + if (!res.success) return res + // 兼容后端返回单对象或数组两种格式 + const item = Array.isArray(res.data) ? res.data[0] : res.data + return { success: true, data: item ?? null } } diff --git a/src/api/token.ts b/src/api/token.ts index 041a9ca..59e6706 100644 --- a/src/api/token.ts +++ b/src/api/token.ts @@ -3,6 +3,8 @@ export const AUTH_EVENT = "auth-change" const TOKEN_KEY = "auth_token" const USER_KEY = "auth_user" +export type UserInfoRecord = Record + export function readCookie(name: string): string | null { const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`)) return match ? decodeURIComponent(match[1]) : null diff --git a/src/api/user.ts b/src/api/user.ts index 52e6e1b..2324520 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,6 +1,6 @@ import type { ApiResponse } from "@/lib/api" import type { UserItem, UserQueryParams } from "@/lib/models/user" -import { callByUser } from "./base" +import { callByUser, callPublic } from "./base" // ==================== 用户管理(.NET /api/baseinfo/v1/user/*) ==================== @@ -42,3 +42,24 @@ export async function updateUserProfile( data, ) } + +// ==================== 个人中心资料/密码(.NET /user/*,cookie 会话) ==================== + +// 编辑资料:老项目 /user/UpdateInfo(QQ/Wx/TaoBao/Email) +export async function updateUserInfo(data: { + QQ: string + Wx: string + TaoBao: string + Email: string +}): Promise> { + return await callPublic("/user/UpdateInfo", data) +} + +// 修改密码:老项目 /user/UpdatePwd(OldPwd/NewPwd/ConfirmPwd) +export async function updateUserPwd(data: { + OldPwd: string + NewPwd: string + ConfirmPwd: string +}): Promise> { + return await callPublic("/user/UpdatePwd", data) +} diff --git a/src/components/authGuard.tsx b/src/components/authGuard.tsx index cda6f63..bb57187 100644 --- a/src/components/authGuard.tsx +++ b/src/components/authGuard.tsx @@ -1,16 +1,9 @@ -import { useEffect, useState } from "react" import { Navigate, useLocation } from "react-router-dom" -import { AUTH_EVENT, isAuthed } from "@/api/token" +import { useProfileStore } from "@/components/stores/profile" export default function AuthGuard({ children }: { children: React.ReactNode }) { const location = useLocation() - const [isValid, setIsValid] = useState(isAuthed) - - useEffect(() => { - const update = () => setIsValid(isAuthed()) - window.addEventListener(AUTH_EVENT, update) - return () => window.removeEventListener(AUTH_EVENT, update) - }, []) + const isValid = useProfileStore(state => state.token !== null) if (!isValid) { return ( diff --git a/src/components/composites/dialogs/changePwdDialog.tsx b/src/components/composites/dialogs/changePwdDialog.tsx new file mode 100644 index 0000000..e139340 --- /dev/null +++ b/src/components/composites/dialogs/changePwdDialog.tsx @@ -0,0 +1,170 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Eye, EyeOff } from "lucide-react" +import { useState } from "react" +import { Controller, useForm } from "react-hook-form" +import { toast } from "sonner" +import { z } from "zod" +import { updateUserPwd } from "@/api/user" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { FieldError } from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" + +type ChangePwdValues = { + oldPwd: string + newPwd: string + confirmPwd: string +} + +const changePwdSchema = z + .object({ + oldPwd: z.string().min(1, "请输入原密码"), + newPwd: z.string().min(1, "请输入新密码"), + confirmPwd: z.string().min(1, "请再次输入新密码"), + }) + .refine(v => v.newPwd === v.confirmPwd, { + message: "两次输入的密码不一致", + path: ["confirmPwd"], + }) + +function PasswordInput(props: { + value: string + onChange: (value: string) => void + placeholder?: string +}) { + const [show, setShow] = useState(false) + return ( +
+ props.onChange(e.target.value)} + type={show ? "text" : "password"} + placeholder={props.placeholder} + className="h-11 bg-gray-50 border-gray-200 pr-10" + /> + +
+ ) +} + +export function ChangePwdDialog({ + open, + onOpenChange, +}: { + open: boolean + onOpenChange: (open: boolean) => void +}) { + const [submitting, setSubmitting] = useState(false) + + const form = useForm({ + resolver: zodResolver(changePwdSchema), + defaultValues: { oldPwd: "", newPwd: "", confirmPwd: "" }, + }) + + const handleOpenChange = (next: boolean) => { + onOpenChange(next) + if (!next) form.reset() + } + + const onSubmit = async (data: ChangePwdValues) => { + setSubmitting(true) + const res = await updateUserPwd({ + OldPwd: data.oldPwd, + NewPwd: data.newPwd, + ConfirmPwd: data.confirmPwd, + }) + setSubmitting(false) + + if (!res.success) { + toast.error(res.message) + return + } + toast.success("密码修改成功") + handleOpenChange(false) + } + + return ( + + + + + 修改密码 + + + +
+
+ + ( + <> + + {fieldState.error?.message} + + )} + /> +
+ +
+ + ( + <> + + {fieldState.error?.message} + + )} + /> +
+ +
+ + ( + <> + + {fieldState.error?.message} + + )} + /> +
+ + +
+
+
+ ) +} diff --git a/src/components/composites/dialogs/editInfoDialog.tsx b/src/components/composites/dialogs/editInfoDialog.tsx index e737d25..810aeaf 100644 --- a/src/components/composites/dialogs/editInfoDialog.tsx +++ b/src/components/composites/dialogs/editInfoDialog.tsx @@ -1,8 +1,9 @@ import { zodResolver } from "@hookform/resolvers/zod" -import { Eye, EyeOff } from "lucide-react" import { useEffect, useState } from "react" import { Controller, useForm } from "react-hook-form" +import { toast } from "sonner" import { z } from "zod" +import { updateUserInfo } from "@/api/user" import { Button } from "@/components/ui/button" import { Dialog, @@ -15,51 +16,45 @@ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" export type EditInfoValues = { - username: string - taobao: string - password: string - email: string - wechat: string qq: string + wechat: string + taobao: string + email: string } const editInfoSchema = z.object({ - username: z.string().trim().min(1, "请输入用户名"), + qq: z.string(), + wechat: z.string(), taobao: z.string(), - password: z.string().min(1, "请输入密码"), email: z .string() .refine( v => !v || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), "请输入正确的邮箱", ), - wechat: z.string(), - qq: z.string(), }) interface EditInfoDialogProps { open: boolean onOpenChange: (open: boolean) => void initialData?: EditInfoValues - onSave?: (data: EditInfoValues) => void + onSaved?: () => void } const DEFAULT_INITIAL_DATA: EditInfoValues = { - username: "191****1234", - taobao: "191****1234", - password: "********", - email: "123456123@qq.com", - wechat: "", qq: "", + wechat: "", + taobao: "", + email: "", } export function EditInfoDialog({ open, onOpenChange, initialData = DEFAULT_INITIAL_DATA, - onSave, + onSaved, }: EditInfoDialogProps) { - const [showPassword, setShowPassword] = useState(false) + const [submitting, setSubmitting] = useState(false) const form = useForm({ resolver: zodResolver(editInfoSchema), @@ -72,9 +67,23 @@ export function EditInfoDialog({ } }, [open, initialData, form]) - const onSubmit = (data: EditInfoValues) => { - onSave?.(data) + const onSubmit = async (data: EditInfoValues) => { + setSubmitting(true) + const res = await updateUserInfo({ + QQ: data.qq, + Wx: data.wechat, + TaoBao: data.taobao, + Email: data.email, + }) + setSubmitting(false) + + if (!res.success) { + toast.error(res.message) + return + } + toast.success("保存成功") onOpenChange(false) + onSaved?.() } return ( @@ -88,18 +97,23 @@ export function EditInfoDialog({
- + ( - <> - - {fieldState.error?.message} - + render={({ field }) => ( + + )} + /> +
+ +
+ + ( + )} />
@@ -115,37 +129,6 @@ export function EditInfoDialog({ />
-
- - ( - <> -
- - -
- {fieldState.error?.message} - - )} - /> -
-
-
- - ( - - )} - /> -
- -
- - ( - - )} - /> -
- diff --git a/src/components/profileOrLogin.tsx b/src/components/profileOrLogin.tsx index 1596fe8..ee7b738 100644 --- a/src/components/profileOrLogin.tsx +++ b/src/components/profileOrLogin.tsx @@ -1,22 +1,11 @@ -import { useEffect, useState } from "react" import { NavLink, useNavigate } from "react-router-dom" import { logout } from "@/api/auth" -import { AUTH_EVENT, isAuthed } from "@/api/token" +import { useProfileStore } from "@/components/stores/profile" export default function ProfileOrLogin() { - const [authed, setAuthed] = useState(isAuthed) + const authed = useProfileStore(state => state.token !== null) const navigate = useNavigate() - useEffect(() => { - const update = () => setAuthed(isAuthed()) - window.addEventListener("focus", update) - window.addEventListener(AUTH_EVENT, update) - return () => { - window.removeEventListener("focus", update) - window.removeEventListener(AUTH_EVENT, update) - } - }, []) - const handleLogout = async () => { await logout() } diff --git a/src/components/stores/app.ts b/src/components/stores/app.ts new file mode 100644 index 0000000..64e7775 --- /dev/null +++ b/src/components/stores/app.ts @@ -0,0 +1,11 @@ +import { create } from "zustand" +import { API_BASE_URL } from "@/lib/api" + +// ==================== 应用配置 ==================== +// 后端地址等运行时配置统一从 store 读取,初始值来自 import.meta.env(见 lib/api.ts) + +type AppState = { + apiUrl: string +} + +export const useAppStore = create()(() => ({ apiUrl: API_BASE_URL })) diff --git a/src/components/stores/client.ts b/src/components/stores/client.ts new file mode 100644 index 0000000..f7da7f5 --- /dev/null +++ b/src/components/stores/client.ts @@ -0,0 +1,54 @@ +import { create } from "zustand" + + +type ClientState = { + breakpoint: { + sm: boolean + md: boolean + lg: boolean + xl: boolean + } +} + +type ClientActions = { + setBreakpoints: (breakpoints: Partial) => void +} + +export type ClientStore = ClientState & ClientActions + +export const useClientStore = create()(set => ({ + breakpoint: { sm: false, md: false, lg: false, xl: false }, + setBreakpoints: breakpoints => + set(state => ({ + breakpoint: { ...state.breakpoint, ...breakpoints }, + })), +})) + +let initialized = false + +// 应用入口调用一次,开始监听窗口断点变化 +export function initClientStore() { + if (initialized) return + initialized = true + + const queries = { + sm: window.matchMedia("(width >= 40rem)"), + md: window.matchMedia("(width >= 48rem)"), + lg: window.matchMedia("(width >= 64rem)"), + xl: window.matchMedia("(width >= 80rem)"), + } + const setBreakpoints = useClientStore.getState().setBreakpoints + + setBreakpoints({ + sm: queries.sm.matches, + md: queries.md.matches, + lg: queries.lg.matches, + xl: queries.xl.matches, + }) + + for (const key of Object.keys(queries) as (keyof typeof queries)[]) { + queries[key].addEventListener("change", e => { + setBreakpoints({ [key]: e.matches }) + }) + } +} diff --git a/src/components/stores/dashboard.ts b/src/components/stores/dashboard.ts new file mode 100644 index 0000000..cc945da --- /dev/null +++ b/src/components/stores/dashboard.ts @@ -0,0 +1,41 @@ +import { create } from "zustand" +import { fetchAgreementStatus } from "@/api/agreement" +import { fetchApiIndex } from "@/api/dashboard" +import type { ApiIndexItem } from "@/lib/models/dashboard" + +// ==================== 个人中心账户总览 ==================== +// 数据源:.NET /user/ApiIndex;协议签署状态:php-api /tencent/index/get_is_upload + +export type DashboardState = { + data: ApiIndexItem | null + agreementSigned: boolean | null + loading: boolean + error: string | null +} + +type DashboardActions = { + refresh: () => Promise + refreshAgreement: () => Promise +} + +export type DashboardStore = DashboardState & DashboardActions + +export const useDashboardStore = create()(set => ({ + data: null, + agreementSigned: null, + loading: false, + error: null, + refresh: async () => { + set({ loading: true }) + const res = await fetchApiIndex() + if (res.success) { + set({ data: res.data, error: null, loading: false }) + } else { + set({ error: res.message, loading: false }) + } + }, + refreshAgreement: async () => { + const res = await fetchAgreementStatus() + if (res.success) set({ agreementSigned: res.data }) + }, +})) diff --git a/src/components/stores/layout.ts b/src/components/stores/layout.ts new file mode 100644 index 0000000..5d657d1 --- /dev/null +++ b/src/components/stores/layout.ts @@ -0,0 +1,20 @@ +import { create } from "zustand" + + + +type LayoutState = { + navbar: boolean +} + +type LayoutActions = { + setNavbar: (navbar: boolean) => void + toggleNavbar: () => void +} + +export type LayoutStore = LayoutState & LayoutActions + +export const useLayoutStore = create()(set => ({ + navbar: false, + setNavbar: navbar => set({ navbar }), + toggleNavbar: () => set(state => ({ navbar: !state.navbar })), +})) diff --git a/src/components/stores/profile.ts b/src/components/stores/profile.ts new file mode 100644 index 0000000..974eb36 --- /dev/null +++ b/src/components/stores/profile.ts @@ -0,0 +1,34 @@ +import { create } from "zustand" +import { + AUTH_EVENT, + getUserInfo, + getUserToken, + type UserInfoRecord, +} from "@/api/token" + + +export type ProfileState = { + token: string | null + userInfo: UserInfoRecord | null +} + +type ProfileActions = { + refreshFromStorage: () => void +} + +export type ProfileStore = ProfileState & ProfileActions + +export const useProfileStore = create()(set => ({ + token: getUserToken(), + userInfo: getUserInfo(), + refreshFromStorage: () => { + set({ token: getUserToken(), userInfo: getUserInfo() }) + }, +})) + +// 应用入口调用一次,监听登录态变化(含跨标签页 focus 同步) +export function initProfileStore() { + const sync = () => useProfileStore.getState().refreshFromStorage() + window.addEventListener(AUTH_EVENT, sync) + window.addEventListener("focus", sync) +} diff --git a/src/home/header.tsx b/src/home/header.tsx index 3e64112..7d0ecd4 100644 --- a/src/home/header.tsx +++ b/src/home/header.tsx @@ -1,17 +1,18 @@ import { Menu, User } from "lucide-react" -import { useEffect, useRef, useState, useSyncExternalStore } from "react" +import { useRef, useState, useSyncExternalStore } from "react" import { NavLink, useNavigate } from "react-router-dom" -import { AUTH_EVENT, isAuthed } from "@/api/token" import ProductDropdown from "@/components/productDropdown" import ProfileOrLogin from "@/components/profileOrLogin" +import { useClientStore } from "@/components/stores/client" +import { useProfileStore } from "@/components/stores/profile" import Wrap from "@/components/wrap" -import { useMediaQuery } from "@/lib/hooks/useMediaQuery" import { cn } from "@/lib/utils" import MobileMenu from "./_components/header/menu-mobile" export default function Header() { const navigate = useNavigate() - const isWideDesktop = useMediaQuery("(min-width: 80rem)") + const authed = useProfileStore(state => state.token !== null) + const isWideDesktop = useClientStore(state => state.breakpoint.xl) const scroll = useSyncExternalStore( callback => { window.addEventListener("scroll", callback) @@ -30,17 +31,6 @@ export default function Header() { // 移动端状态 const [mobileMenu, setMobileMenu] = useState(false) - const [authed, setAuthed] = useState(isAuthed) - - useEffect(() => { - const update = () => setAuthed(isAuthed()) - window.addEventListener("focus", update) - window.addEventListener(AUTH_EVENT, update) - return () => { - window.removeEventListener("focus", update) - window.removeEventListener(AUTH_EVENT, update) - } - }, []) return (
diff --git a/src/home/index.tsx b/src/home/index.tsx index 6920201..19e738c 100644 --- a/src/home/index.tsx +++ b/src/home/index.tsx @@ -2,8 +2,8 @@ import { ChevronLeft, ChevronRight } from "lucide-react" import type { ReactNode } from "react" import { useRef, useState } from "react" import { useNavigate } from "react-router-dom" +import { useClientStore } from "@/components/stores/client" import Wrap from "@/components/wrap" -import { useIsMobile } from "@/lib/hooks/useMediaQuery" import banner from "./_assets/banner.webp" import s1 from "./_assets/s1.webp" import s2 from "./_assets/s2.webp" @@ -22,14 +22,15 @@ import Common from "./_components/header/common" export default function HomePage() { const navigate = useNavigate() + // 免费试用:无论是否登录都进入产品购买页(下单时才校验登录) const handleFreeTrial = () => { - navigate("/login") + navigate("/product") } const scrollContainerRef = useRef(null) const [showLeftArrow, setShowLeftArrow] = useState(false) const [showRightArrow, setShowRightArrow] = useState(true) - const isMobile = useIsMobile() + const isMobile = useClientStore(state => !state.breakpoint.md) const scroll = (direction: "left" | "right") => { if (!scrollContainerRef.current) return diff --git a/src/home/product/productGrid.tsx b/src/home/product/productGrid.tsx index fcfa680..666af5f 100644 --- a/src/home/product/productGrid.tsx +++ b/src/home/product/productGrid.tsx @@ -1,4 +1,5 @@ import { useNavigate } from "react-router-dom" +import { useProfileStore } from "@/components/stores/profile" import { Button } from "@/components/ui/button" import type { Product } from "@/lib/models/product" import vector from "../_assets/vector.webp" @@ -9,6 +10,16 @@ type ProductGridProps = { export default function ProductGrid({ products }: ProductGridProps) { const navigate = useNavigate() + const authed = useProfileStore(state => state.token !== null) + + const handleBuy = (product: Product) => { + // 未登录先登录,已登录进入购买确认页 + if (!authed) { + navigate("/login") + return + } + navigate("/product/buy", { state: { product } }) + } return (
@@ -75,7 +86,7 @@ export default function ProductGrid({ products }: ProductGridProps) { diff --git a/src/lib/hooks/useMediaQuery.ts b/src/lib/hooks/useMediaQuery.ts deleted file mode 100644 index fd7c032..0000000 --- a/src/lib/hooks/useMediaQuery.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useSyncExternalStore } from "react" - -const subscribeMediaQuery = (query: string) => (callback: () => void) => { - if (typeof window === "undefined") return () => {} - const mql = window.matchMedia(query) - mql.addEventListener("change", callback) - return () => mql.removeEventListener("change", callback) -} - -const getMediaQuerySnapshot = (query: string) => () => { - if (typeof window === "undefined") return false - return window.matchMedia(query).matches -} - -export function useMediaQuery(query: string): boolean { - return useSyncExternalStore( - subscribeMediaQuery(query), - getMediaQuerySnapshot(query), - () => false, - ) -} - -// 移动端:< 768px(对应 Tailwind md 断点以下) -export function useIsMobile(): boolean { - return useMediaQuery("(max-width: 47.999rem)") -} - -// 桌面端:>= 768px -export function useIsDesktop(): boolean { - return useMediaQuery("(min-width: 48rem)") -} - -// 窄桌面/平板横屏:< 1280px(对应 Tailwind xl 断点以下) -export function useIsNarrowDesktop(): boolean { - return useMediaQuery("(max-width: 79.999rem)") -} diff --git a/src/lib/models/dashboard.ts b/src/lib/models/dashboard.ts index 72a4a50..787b030 100644 --- a/src/lib/models/dashboard.ts +++ b/src/lib/models/dashboard.ts @@ -1,3 +1,61 @@ +// ==================== /user/ApiIndex 账户总览接口返回结构 ==================== + +export type ApiIndexUserModel = { + apikey: string + TenantId: number + agent_id: number + jgday: number + LoginCode: string + Password: string + Name: string + Phone: string + Enabled: number + LastLoginDate: string + Sex: number + CreateType: number + ProductAccountCount: number + ExpiredProductAccountCount: number + RestAmount: number + ju_money: number + ConsumeAmount: number + Wx: string + QQ: string + Email: string + TestCountLimit: number + UseTestCount: number + ManagerId: number + ManagerName: string + id_code: string + is_verify: number + is_agent: number + parent_id: number + CreateTime: string + UpdateTime: string + DeleteTag: number + Id: number +} + +export type ApiIndexAccountModel = { + TotalCount: number + ExpriedCount: number +} + +export type ApiIndexStatistic = { + TodayExpend: number + TodayRefund: number + TodayCharege: number + MonthExpend: number + MonthRefund: number + MonthCharege: number + YearExpend: number +} + +export type ApiIndexItem = { + UserModel: ApiIndexUserModel + AccountModel: ApiIndexAccountModel + Statistic: ApiIndexStatistic +} + export type DashboardBalance = { balance: number hCoin: number diff --git a/src/main.tsx b/src/main.tsx index 8dc9279..4d5d440 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,6 +2,11 @@ import { StrictMode } from "react" import { createRoot } from "react-dom/client" import "./index.css" import App from "./App.tsx" +import { initClientStore } from "./components/stores/client" +import { initProfileStore } from "./components/stores/profile" + +initClientStore() +initProfileStore() const rootElement = document.getElementById("root") if (!rootElement) throw new Error("Failed to find root element")