mirror of https://github.com/openspug/spug
A 添加通知功能
parent
f947775a93
commit
d11c26c5ac
|
@ -13,8 +13,8 @@ class NotifyView(View):
|
|||
|
||||
def patch(self, request):
|
||||
form, error = JsonParser(
|
||||
Argument('id', type=int, help='参数错误')
|
||||
Argument('ids', type=list, help='参数错误')
|
||||
).parse(request.body)
|
||||
if error is None:
|
||||
Notify.objects.filter(pk=form.id).update(unread=False)
|
||||
Notify.objects.filter(id__in=form.ids).update(unread=False)
|
||||
return json_response(error=error)
|
||||
|
|
|
@ -1,29 +1,105 @@
|
|||
/**
|
||||
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Layout, Dropdown, Menu, Icon, Avatar } from 'antd';
|
||||
import { Layout, Dropdown, Menu, List, Icon, Badge, Avatar } from 'antd';
|
||||
import styles from './layout.module.css';
|
||||
import http from '../libs/http';
|
||||
import history from '../libs/history';
|
||||
import avatar from './avatar.png';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
export default class extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.inerval = null;
|
||||
this.state = {
|
||||
loading: true,
|
||||
notifies: [],
|
||||
read: []
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetch();
|
||||
this.interval = setInterval(this.fetch, 30000)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.interval && clearInterval(this.interval)
|
||||
}
|
||||
|
||||
fetch = () => {
|
||||
this.setState({loading: true});
|
||||
http.get('/api/notify/')
|
||||
.then(res => this.setState({notifies: res, read: []}))
|
||||
.finally(() => this.setState({loading: false}))
|
||||
};
|
||||
|
||||
handleLogout = () => {
|
||||
history.push('/');
|
||||
http.get('/api/account/logout/')
|
||||
};
|
||||
|
||||
handleRead = (e, item) => {
|
||||
e.stopPropagation();
|
||||
this.state.read.push(item.id);
|
||||
this.setState({read: this.state.read});
|
||||
http.patch('/api/notify/', {ids: [item.id]})
|
||||
};
|
||||
|
||||
handleReadAll = () => {
|
||||
const ids = this.state.notifies.map(x => x.id);
|
||||
this.setState({read: ids});
|
||||
http.patch('/api/notify/', {ids})
|
||||
};
|
||||
menu = (
|
||||
<Menu>
|
||||
<Menu.Item disabled>
|
||||
<Icon type="user" style={{marginRight: 10}}/>个人中心
|
||||
</Menu.Item>
|
||||
<Menu.Divider/>
|
||||
<Menu.Item onClick={this.handleLogout}>
|
||||
<Icon type="logout" style={{marginRight: 10}}/>退出登录
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
notify = () => (
|
||||
<Menu className={styles.notify}>
|
||||
<Menu.Item style={{padding: 0, whiteSpace: 'unset'}}>
|
||||
<List
|
||||
loading={this.state.loading}
|
||||
style={{maxHeight: 500, overflow: 'scroll'}}
|
||||
itemLayout="horizontal"
|
||||
dataSource={this.state.notifies}
|
||||
renderItem={item => (
|
||||
<List.Item className={styles.notifyItem} onClick={e => this.handleRead(e, item)}>
|
||||
<List.Item.Meta
|
||||
style={{opacity: this.state.read.includes(item.id) ? 0.4 : 1}}
|
||||
avatar={<Icon type={item.source} style={{fontSize: 24, color: '#1890ff'}}/>}
|
||||
title={<span style={{fontWeight: 400, color: '#404040'}}>{item.title}</span>}
|
||||
description={[
|
||||
<div key="1" style={{fontSize: 12}}>{item.content}</div>,
|
||||
<div key="2" style={{fontSize: 12}}>{moment(item['created_at']).fromNow()}</div>
|
||||
]}/>
|
||||
</List.Item>
|
||||
)}/>
|
||||
{this.state.notifies.length === 0 && (
|
||||
<div>
|
||||
<img src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg" alt="not found"/>
|
||||
<div>暂无未读通知</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.notifyFooter} onClick={() => this.handleReadAll()}>全部 已读</div>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
render() {
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item disabled>
|
||||
<Icon type="user" style={{marginRight: 10}}/>个人中心
|
||||
</Menu.Item>
|
||||
<Menu.Divider/>
|
||||
<Menu.Item onClick={this.handleLogout}>
|
||||
<Icon type="logout" style={{marginRight: 10}}/>退出登录
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
const {notifies, read} = this.state;
|
||||
return (
|
||||
<Layout.Header style={{padding: 0}}>
|
||||
<div className={styles.header}>
|
||||
|
@ -31,13 +107,22 @@ export default class extends React.Component {
|
|||
<Icon type={this.props.collapsed ? 'menu-unfold' : 'menu-fold'}/>
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
<Dropdown overlay={menu}>
|
||||
<Dropdown overlay={this.menu}>
|
||||
<span className={styles.action}>
|
||||
<Avatar size="small" src={avatar} style={{marginRight: 8}}/>
|
||||
{localStorage.getItem('nickname')}
|
||||
</span>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
<Dropdown overlay={this.notify} trigger={['click']}>
|
||||
<span className={styles.trigger}>
|
||||
<Badge count={notifies.length - read.length}>
|
||||
<Icon type="notification" style={{fontSize: 16}}/>
|
||||
</Badge>
|
||||
</span>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Header>
|
||||
)
|
||||
|
|
|
@ -67,6 +67,27 @@
|
|||
.action:hover {
|
||||
background: rgb(233, 247, 254);
|
||||
}
|
||||
.notify {
|
||||
width: 350px;
|
||||
padding: 0;
|
||||
}
|
||||
.notify :global(.ant-dropdown-menu-item:hover) {
|
||||
background-color: #fff;
|
||||
}
|
||||
.notifyItem {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
.notifyItem:hover {
|
||||
background-color: rgb(233, 247, 254);
|
||||
}
|
||||
.notifyFooter {
|
||||
line-height: 46px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 48px 0 24px;
|
||||
|
|
Loading…
Reference in New Issue