dcd0c56827
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 52s
CI/CD Pipeline / Build Staging API Image (push) Successful in 54s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 36s
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m53s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m0s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m55s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 4m7s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m29s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 4m34s
CI/CD Pipeline / Validate - Style (push) Successful in 4m42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 46s
AI Code Review / AI Code Review (pull_request) Successful in 6m53s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m24s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m15s
CI/CD Pipeline / Validate - Security (push) Successful in 8m57s
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
254 lines
7.8 KiB
TypeScript
254 lines
7.8 KiB
TypeScript
/**
|
|
* Phase 1 Header 重构
|
|
* 扁平化导航菜单 + 手机端汉堡菜单
|
|
* v2: 增加积分徽章、升级会员入口、我的积分入口
|
|
*/
|
|
import React, { useEffect, useState } from "react"
|
|
import { Avatar, Badge, Button, Dropdown, Drawer, Space, Tooltip } from "antd"
|
|
import {
|
|
LogoutOutlined,
|
|
SettingOutlined,
|
|
UserOutlined,
|
|
MenuOutlined,
|
|
CrownFilled,
|
|
ThunderboltOutlined,
|
|
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 { ENABLE_CREDIT_SYSTEM } from "@/config/features"
|
|
import { usePointsStore } from "@/store/pointsStore"
|
|
import "./Header.css"
|
|
|
|
const Header: React.FC = () => {
|
|
const navigate = useNavigate()
|
|
const location = useLocation()
|
|
const user = useAuthStore((state) => state.user)
|
|
const logoutMutation = useLogout()
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
|
|
|
// 积分与会员状态
|
|
const { balance, init } = 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"] = [
|
|
{
|
|
key: "profile",
|
|
icon: <UserOutlined />,
|
|
label: "个人设置",
|
|
onClick: () => navigate("/app/profile"),
|
|
},
|
|
{
|
|
key: "subscription",
|
|
icon: <SettingOutlined />,
|
|
label: "订阅管理",
|
|
onClick: () => navigate("/app/subscription"),
|
|
},
|
|
// 积分系统开关关闭时隐藏积分相关菜单项(代码保留不删除)
|
|
...(ENABLE_CREDIT_SYSTEM
|
|
? [
|
|
{
|
|
key: "points-center",
|
|
icon: <ThunderboltOutlined />,
|
|
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",
|
|
icon: <LogoutOutlined />,
|
|
label: "退出登录",
|
|
onClick: () => logoutMutation.mutateAsync(),
|
|
},
|
|
]
|
|
|
|
/** 判断导航项是否激活 */
|
|
const isActive = (path: string) => {
|
|
if (path === "/app/dashboard") {
|
|
return (
|
|
location.pathname === "/" ||
|
|
location.pathname === "/app" ||
|
|
location.pathname === "/app/dashboard"
|
|
)
|
|
}
|
|
return location.pathname.startsWith(path)
|
|
}
|
|
|
|
return (
|
|
<header className="xx-top-nav">
|
|
<div className="xx-top-nav-inner">
|
|
<button className="xx-brand" type="button" onClick={() => navigate("/app/dashboard")}>
|
|
<span className="xx-logo">🦐</span>
|
|
<span className="xx-brand-text">小虾自动剪辑</span>
|
|
</button>
|
|
|
|
{/* 桌面端导航 */}
|
|
<nav className="xx-nav-links">
|
|
{NAV_ITEMS.map((item) => (
|
|
<button
|
|
key={item.key}
|
|
className={isActive(item.path) ? "active" : ""}
|
|
type="button"
|
|
onClick={() => navigate(item.path)}
|
|
>
|
|
{item.label}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="xx-right-section">
|
|
{/* v2: 积分徽章(顶部显示) */}
|
|
<PointsBadge />
|
|
|
|
{/* v2: 升级会员入口(仅免费用户显示) */}
|
|
{!isMember && (
|
|
<Tooltip
|
|
title={
|
|
ENABLE_CREDIT_SYSTEM
|
|
? "升级会员解锁无限混剪、批量导出,积分 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 />
|
|
</button>
|
|
|
|
<Dropdown menu={{ items: menuItems }} placement="bottomRight">
|
|
<Space className="xx-user-menu">
|
|
<Avatar className="xx-avatar" icon={<UserOutlined />} />
|
|
<span className="xx-username">
|
|
{user?.display_name || user?.username || "小虾用户"}
|
|
</span>
|
|
</Space>
|
|
</Dropdown>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 手机端侧边抽屉导航 */}
|
|
<Drawer
|
|
title="导航菜单"
|
|
placement="left"
|
|
onClose={() => setMobileMenuOpen(false)}
|
|
open={mobileMenuOpen}
|
|
width={260}
|
|
className="xx-mobile-drawer"
|
|
>
|
|
{/* 手机端积分/会员入口 */}
|
|
{balance && (
|
|
<div className="xx-mobile-points-row">
|
|
<PointsBadge />
|
|
{!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
|
|
key={item.key}
|
|
className={`xx-mobile-nav-item ${isActive(item.path) ? "active" : ""}`}
|
|
type="button"
|
|
onClick={() => {
|
|
navigate(item.path)
|
|
setMobileMenuOpen(false)
|
|
}}
|
|
>
|
|
<span className="xx-mobile-nav-icon">{item.icon}</span>
|
|
<span>{item.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</Drawer>
|
|
</header>
|
|
)
|
|
}
|
|
|
|
export default Header
|