203 lines
6.4 KiB
JavaScript
203 lines
6.4 KiB
JavaScript
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 |