b86a9c2af2
CI/CD Pipeline / Deploy Staging (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Failing after 75h41m6s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 75h41m16s
141 lines
3.9 KiB
TypeScript
141 lines
3.9 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;
|