feat(points): update apps/web/src/components/layout/Header.tsx

This commit is contained in:
2026-09-15 08:38:09 +08:00
parent 5579a40468
commit b7de70fa7d
+129 -4
View File
@@ -1,15 +1,27 @@
/**
* Phase 1 Header 重构
* 扁平化导航菜单 + 手机端汉堡菜单
* v2: 增加积分徽章、升级会员入口、我的积分入口
*/
import React, { useState } from "react"
import { Avatar, Dropdown, Drawer, Space } from "antd"
import { LogoutOutlined, SettingOutlined, UserOutlined, MenuOutlined } from "@ant-design/icons"
import React, { useEffect, useState } from "react"
import { Avatar, Badge, Button, Dropdown, Drawer, Space, Tooltip } from "antd"
import {
LogoutOutlined,
SettingOutlined,
UserOutlined,
MenuOutlined,
CrownFilled,
DiamondOutlined,
HistoryOutlined,
WalletOutlined,
} from "@ant-design/icons"
import { useLocation, useNavigate } from "react-router-dom"
import { useAuthStore } from "@/store/authStore"
import { useLogout } from "@/hooks/useAuth"
import type { MenuProps } from "antd"
import { NAV_ITEMS } from "@/config/navigation"
import PointsBadge from "@/components/common/PointsBadge"
import { usePointsStore } from "@/store/pointsStore"
import "./Header.css"
const Header: React.FC = () => {
@@ -19,6 +31,18 @@ const Header: React.FC = () => {
const logoutMutation = useLogout()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
// 积分与会员状态
const { balance, init, loading } = usePointsStore()
useEffect(() => {
init()
// 每 60s 刷新一次(余额可能变化)
const t = setInterval(() => init(), 60_000)
return () => clearInterval(t)
}, [init])
const isMember = !!balance?.is_member
const memberType = balance?.member_type ?? null
/** 用户下拉菜单 */
const menuItems: MenuProps["items"] = [
{
@@ -33,6 +57,30 @@ const Header: React.FC = () => {
label: "订阅管理",
onClick: () => navigate("/app/subscription"),
},
// v2: 我的积分入口
{
key: "points-center",
icon: <DiamondOutlined />,
label: (
<Space>
我的积分
{balance && <span style={{ color: "#8b5cf6", fontWeight: 700 }}>{balance.balance}</span>}
</Space>
),
onClick: () => navigate("/app/points"),
},
{
key: "points-history",
icon: <HistoryOutlined />,
label: "积分明细",
onClick: () => navigate("/app/points/transactions"),
},
{
key: "recharge",
icon: <WalletOutlined />,
label: "充值积分",
onClick: () => navigate("/app/points/recharge"),
},
{ type: "divider" },
{
key: "logout",
@@ -44,7 +92,6 @@ const Header: React.FC = () => {
/** 判断导航项是否激活 */
const isActive = (path: string) => {
// 首页特殊处理:/ 和 /app/dashboard 都算激活
if (path === "/app/dashboard") {
return (
location.pathname === "/" ||
@@ -78,6 +125,53 @@ const Header: React.FC = () => {
</nav>
<div className="xx-right-section">
{/* v2: 积分徽章(顶部显示) */}
{balance && (
<PointsBadge
balance={balance.balance}
onClick={() => navigate("/app/points")}
className="xx-header-points"
/>
)}
{/* v2: 升级会员入口(仅免费用户显示) */}
{!isMember && (
<Tooltip title="升级会员解锁无限混剪、批量导出,积分 8 折起">
<Button
type="primary"
size="small"
icon={<CrownFilled />}
className="xx-upgrade-btn"
onClick={() => navigate("/app/subscription")}
>
升级会员
</Button>
</Tooltip>
)}
{/* v2: 付费会员显示金冠标识 */}
{isMember && (
<Tooltip
title={`${memberType === "yearly" ? "年卡" : memberType === "quarterly" ? "季卡" : "月卡"}会员${
balance?.member_expires_at
? ` · 到期 ${new Date(balance.member_expires_at).toLocaleDateString("zh-CN")}`
: ""
}`}
>
<Badge dot color="gold">
<Button
type="text"
size="small"
icon={<CrownFilled style={{ color: "#f59e0b" }} />}
className="xx-crown-btn"
onClick={() => navigate("/app/subscription")}
>
{memberType === "yearly" ? "年卡" : memberType === "quarterly" ? "季卡" : "会员"}
</Button>
</Badge>
</Tooltip>
)}
{/* 手机端汉堡菜单按钮 */}
<button className="xx-hamburger" type="button" onClick={() => setMobileMenuOpen(true)}>
<MenuOutlined />
@@ -103,6 +197,37 @@ const Header: React.FC = () => {
width={260}
className="xx-mobile-drawer"
>
{/* 手机端积分/会员入口 */}
{balance && (
<div className="xx-mobile-points-row">
<PointsBadge
balance={balance.balance}
onClick={() => {
navigate("/app/points")
setMobileMenuOpen(false)
}}
/>
{!isMember ? (
<Button
size="small"
type="primary"
icon={<CrownFilled />}
onClick={() => {
navigate("/app/subscription")
setMobileMenuOpen(false)
}}
>
升级
</Button>
) : (
<span className="xx-mobile-member-tag">
<CrownFilled style={{ color: "#f59e0b" }} />{" "}
{memberType === "yearly" ? "年卡" : memberType === "quarterly" ? "季卡" : "月卡"}
</span>
)}
</div>
)}
<div className="xx-mobile-nav">
{NAV_ITEMS.map((item) => (
<button