mirror of https://github.com/openspug/spug
A 添加通知功能
parent
f947775a93
commit
d11c26c5ac
|
@ -13,8 +13,8 @@ class NotifyView(View):
|
||||||
|
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
form, error = JsonParser(
|
form, error = JsonParser(
|
||||||
Argument('id', type=int, help='参数错误')
|
Argument('ids', type=list, help='参数错误')
|
||||||
).parse(request.body)
|
).parse(request.body)
|
||||||
if error is None:
|
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)
|
return json_response(error=error)
|
||||||
|
|
|
@ -1,19 +1,61 @@
|
||||||
|
/**
|
||||||
|
* 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 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 styles from './layout.module.css';
|
||||||
import http from '../libs/http';
|
import http from '../libs/http';
|
||||||
import history from '../libs/history';
|
import history from '../libs/history';
|
||||||
import avatar from './avatar.png';
|
import avatar from './avatar.png';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
export default class extends React.Component {
|
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 = () => {
|
handleLogout = () => {
|
||||||
history.push('/');
|
history.push('/');
|
||||||
http.get('/api/account/logout/')
|
http.get('/api/account/logout/')
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
handleRead = (e, item) => {
|
||||||
const menu = (
|
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>
|
||||||
<Menu.Item disabled>
|
<Menu.Item disabled>
|
||||||
<Icon type="user" style={{marginRight: 10}}/>个人中心
|
<Icon type="user" style={{marginRight: 10}}/>个人中心
|
||||||
|
@ -24,6 +66,40 @@ export default class extends React.Component {
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu>
|
</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 {notifies, read} = this.state;
|
||||||
return (
|
return (
|
||||||
<Layout.Header style={{padding: 0}}>
|
<Layout.Header style={{padding: 0}}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
|
@ -31,13 +107,22 @@ export default class extends React.Component {
|
||||||
<Icon type={this.props.collapsed ? 'menu-unfold' : 'menu-fold'}/>
|
<Icon type={this.props.collapsed ? 'menu-unfold' : 'menu-fold'}/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.right}>
|
<div className={styles.right}>
|
||||||
<Dropdown overlay={menu}>
|
<Dropdown overlay={this.menu}>
|
||||||
<span className={styles.action}>
|
<span className={styles.action}>
|
||||||
<Avatar size="small" src={avatar} style={{marginRight: 8}}/>
|
<Avatar size="small" src={avatar} style={{marginRight: 8}}/>
|
||||||
{localStorage.getItem('nickname')}
|
{localStorage.getItem('nickname')}
|
||||||
</span>
|
</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Layout.Header>
|
</Layout.Header>
|
||||||
)
|
)
|
||||||
|
|
|
@ -67,6 +67,27 @@
|
||||||
.action:hover {
|
.action:hover {
|
||||||
background: rgb(233, 247, 254);
|
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 {
|
.footer {
|
||||||
margin: 48px 0 24px;
|
margin: 48px 0 24px;
|
||||||
|
|
Loading…
Reference in New Issue