Files
xiaoxia-saas/apps/web/src/components/layout/Header.tsx
T
xiaoxia 0fcb77b991
CI Build & Deploy Pipeline / Staging E2E Tests (push) Failing after 2m50s
CI Build & Deploy Pipeline / Production Browser E2E (push) Failing after 1437h19m24s
CI Build & Deploy Pipeline / Build Production Web Image (push) Failing after 1437h19m26s
CI Build & Deploy Pipeline / Deploy Production (push) Failing after 1437h19m25s
CI Build & Deploy Pipeline / Build Production API Image (push) Failing after 1437h19m26s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1437h21m16s
CI Build & Deploy Pipeline / Build Staging API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Failing after 1437h51m29s
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
ci: Prettier纳入两层防御体系 (#520)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-07-18 18:08:19 +08:00

128 lines
3.8 KiB
TypeScript

/**
* Phase 1 Header 重构
* 扁平化导航菜单 + 手机端汉堡菜单
*/
import React, { useState } from "react"
import { Avatar, Dropdown, Drawer, Space } from "antd"
import { LogoutOutlined, SettingOutlined, UserOutlined, MenuOutlined } 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 "./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 menuItems: MenuProps["items"] = [
{
key: "profile",
icon: <UserOutlined />,
label: "个人设置",
onClick: () => navigate("/app/profile"),
},
{
key: "subscription",
icon: <SettingOutlined />,
label: "订阅管理",
onClick: () => navigate("/app/subscription"),
},
{ type: "divider" },
{
key: "logout",
icon: <LogoutOutlined />,
label: "退出登录",
onClick: () => logoutMutation.mutateAsync(),
},
]
/** 判断导航项是否激活 */
const isActive = (path: string) => {
// 首页特殊处理:/ 和 /app/dashboard 都算激活
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">
{/* 手机端汉堡菜单按钮 */}
<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"
>
<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