imeeting/components/MainLayout/AppHeader.jsx

203 lines
6.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { useState, useEffect } from 'react'
import { Layout, Badge, Avatar, Dropdown, Space, Popover, List, Tabs, Button, Empty, Typography, Segmented, Tooltip } from 'antd'
import { useNavigate } from 'react-router-dom'
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
BellOutlined,
ProjectOutlined,
TeamOutlined,
NotificationOutlined,
MoonOutlined,
SunOutlined,
GlobalOutlined
} from '@ant-design/icons'
import useUserStore from '@/stores/userStore'
import useNotificationStore from '@/stores/notificationStore'
import useThemeStore from '@/stores/themeStore'
import { getNotifications, getUnreadCount, markAsRead, markAllAsRead } from '@/api/notification'
import Toast from '@/components/Toast/Toast'
import './AppHeader.css'
const { Header } = Layout
const { Text } = Typography
function AppHeader({ collapsed, onToggle, showLogo = true }) {
const navigate = useNavigate()
const { user } = useUserStore()
const { unreadCount, fetchUnreadCount, decrementUnreadCount, resetUnreadCount } = useNotificationStore()
const { isDarkMode, toggleTheme } = useThemeStore()
const [notifications, setNotifications] = useState([])
const [loading, setLoading] = useState(false)
const [popoverVisible, setPopoverVisible] = useState(false)
const [lang, setLang] = useState('zh')
useEffect(() => {
if (user) {
fetchUnreadCount()
const timer = setInterval(fetchUnreadCount, 120000)
return () => clearInterval(timer)
}
}, [user])
const fetchNotifications = async () => {
setLoading(true)
try {
const res = await getNotifications({ page: 1, page_size: 5 })
setNotifications(res.data || [])
} catch (error) {
console.error('Fetch notifications error:', error)
} finally {
setLoading(false)
}
}
const handleMarkRead = async (id) => {
try {
await markAsRead(id)
setNotifications(notifications.map(n => n.id === id ? { ...n, is_read: true } : n))
decrementUnreadCount()
} catch (error) {
console.error('Mark read error:', error)
}
}
const handleMarkAllRead = async () => {
try {
await markAllAsRead()
setNotifications(notifications.map(n => ({ ...n, is_read: true })))
resetUnreadCount()
Toast.success('操作成功', '所有通知已标记为已读')
} catch (error) {
console.error('Mark all read error:', error)
}
}
const handleNotificationClick = (n) => {
if (!n.is_read) {
handleMarkRead(n.id)
}
if (n.link) {
navigate(n.link)
setPopoverVisible(false)
}
}
const getCategoryIcon = (category) => {
switch (category) {
case 'project': return <ProjectOutlined style={{ color: '#1890ff' }} />
case 'collaboration': return <TeamOutlined style={{ color: '#52c41a' }} />
default: return <NotificationOutlined style={{ color: '#faad14' }} />
}
}
const notificationContent = (
<div className="notification-popover">
<div className="popover-header">
<span className="title">消息通知</span>
{unreadCount > 0 && (
<Button type="link" size="small" onClick={handleMarkAllRead}>
全部已读
</Button>
)}
</div>
<List
className="notification-list"
loading={loading}
itemLayout="horizontal"
dataSource={notifications}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无新消息" /> }}
renderItem={(item) => (
<List.Item
className={`notification-item ${!item.is_read ? 'unread' : ''}`}
onClick={() => handleNotificationClick(item)}
>
<List.Item.Meta
avatar={<Avatar icon={getCategoryIcon(item.category)} />}
title={<Text strong={!item.is_read}>{item.title}</Text>}
description={
<div>
<div className="content-text">{item.content}</div>
<div className="time">{new Date(item.created_at).toLocaleString('zh-CN')}</div>
</div>
}
/>
</List.Item>
)}
/>
<div className="popover-footer">
<Button type="link" block onClick={() => { navigate('/notifications'); setPopoverVisible(false); }}>
查看全部消息
</Button>
</div>
</div>
)
return (
<Header className="app-header" style={{ paddingLeft: showLogo ? 24 : 0 }}>
{/* 左侧Logo + 折叠按钮 */}
{showLogo && (
<div className="header-left">
{/* Logo 区域 */}
<div className="header-logo">
<img src="/favicon.svg" alt="logo" style={{ width: 32, height: 32, marginRight: 8 }} />
<h2 style={{ margin: 0, color: '#1677ff', fontWeight: 'bold' }}>NexDocus</h2>
</div>
{/* 折叠按钮 */}
<div className="trigger" onClick={onToggle}>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</div>
</div>
)}
{!showLogo && <div />} {/* Spacer if left is empty */}
{/* 右侧:功能按钮 */}
<div className="header-right">
<Space size={16} className="header-actions">
{/* 1. 主题切换 */}
<div className="header-icon-btn" title="切换主题" onClick={toggleTheme}>
{isDarkMode ? <SunOutlined style={{ fontSize: 18 }} /> : <MoonOutlined style={{ fontSize: 18 }} />}
</div>
{/* 2. 语言切换 */}
<Segmented
value={lang}
onChange={setLang}
options={[
{ label: '中', value: 'zh' },
{ label: 'EN', value: 'en' },
]}
style={{ fontWeight: 500 }}
/>
{/* 3. 消息通知 */}
<Popover
content={notificationContent}
trigger="click"
open={popoverVisible}
onOpenChange={(visible) => {
setPopoverVisible(visible)
if (visible) {
fetchNotifications()
}
}}
placement="bottomRight"
overlayClassName="header-notification-popover"
>
<div className="header-icon-btn" title="消息中心">
<Badge count={unreadCount} size="small" offset={[2, -2]}>
<BellOutlined style={{ fontSize: 18 }} />
</Badge>
</div>
</Popover>
</Space>
</div>
</Header>
)
}
export default AppHeader