基本完成消息通知
parent
e71ca15397
commit
06a283390e
|
@ -21,6 +21,7 @@
|
|||
"axios": "^1.2.1",
|
||||
"echarts": "^5.4.1",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"history": "^5.3.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -30,6 +31,9 @@
|
|||
"redux-persist": "^6.0.0",
|
||||
"typescript": "^4.9.4",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/event-source-polyfill": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
|
@ -4033,6 +4037,12 @@
|
|||
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.0.tgz",
|
||||
"integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ=="
|
||||
},
|
||||
"node_modules/@types/event-source-polyfill": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/event-source-polyfill/-/event-source-polyfill-1.0.0.tgz",
|
||||
"integrity": "sha512-b8O8/rg7NIW0iJ8i9MNDBZqPljHA+b7AjC3QFqH3dSyW6vgrl3oBgyIv5dw2fibh5enHHDkkPZG5PHza7U4NRw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.15.tgz",
|
||||
|
@ -7679,6 +7689,11 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/event-source-polyfill": {
|
||||
"version": "1.0.31",
|
||||
"resolved": "https://registry.npmmirror.com/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz",
|
||||
"integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA=="
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
|
@ -20152,6 +20167,12 @@
|
|||
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.0.tgz",
|
||||
"integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ=="
|
||||
},
|
||||
"@types/event-source-polyfill": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/event-source-polyfill/-/event-source-polyfill-1.0.0.tgz",
|
||||
"integrity": "sha512-b8O8/rg7NIW0iJ8i9MNDBZqPljHA+b7AjC3QFqH3dSyW6vgrl3oBgyIv5dw2fibh5enHHDkkPZG5PHza7U4NRw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/express": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.15.tgz",
|
||||
|
@ -23080,6 +23101,11 @@
|
|||
"resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
|
||||
},
|
||||
"event-source-polyfill": {
|
||||
"version": "1.0.31",
|
||||
"resolved": "https://registry.npmmirror.com/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz",
|
||||
"integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA=="
|
||||
},
|
||||
"eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"axios": "^1.2.1",
|
||||
"echarts": "^5.4.1",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"history": "^5.3.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -49,5 +50,8 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/event-source-polyfill": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ export interface Notice {
|
|||
noticeId: string;
|
||||
noticeTargetId: string;
|
||||
noticeTime: string;
|
||||
alreadyRead: boolean;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
import React, {useState, useEffect} from 'react';
|
||||
import {UploadOutlined, UserOutlined, BellOutlined, CloseOutlined} from '@ant-design/icons';
|
||||
import {Layout, Menu, theme, Typography, Button, Dropdown, MenuProps, Popover} from 'antd';
|
||||
import {Layout, Menu, theme, Typography, Button, Dropdown, MenuProps, Popover, Badge} from 'antd';
|
||||
import {useAppDispatch, useAppSelector} from "../models/hooks";
|
||||
import {getStaff, getToken, setStaff, setToken, store} from "../models/store";
|
||||
import {Link, Outlet, useLocation, useNavigate} from "react-router-dom";
|
||||
import Icon from '@ant-design/icons';
|
||||
import {Logo} from "../assets/icons";
|
||||
import axiosInstance from "../utils/axiosInstance";
|
||||
import axiosInstance, {baseUrl} from "../utils/axiosInstance";
|
||||
import {Department, Staff, Token} from "../models/Staff";
|
||||
import MessageList from "./MessageList";
|
||||
import {NativeEventSource, EventSourcePolyfill} from 'event-source-polyfill';
|
||||
import {NoticeResponse} from "../models/Notice";
|
||||
import qs from "qs";
|
||||
|
||||
const {Title, Text, Paragraph} = Typography;
|
||||
const {Header, Content, Footer, Sider} = Layout;
|
||||
|
||||
|
||||
function HeaderBar(props: any) {
|
||||
function HeaderBar(props: { messageCount: number, onMessageCountChange: (count: number) => void }) {
|
||||
const {
|
||||
token: {colorBgContainer, colorPrimary},
|
||||
} = theme.useToken();
|
||||
|
@ -90,7 +93,7 @@ function HeaderBar(props: any) {
|
|||
boxShadow: '0px 6px 16px 0px rgba(0, 0, 0, 0.08)'
|
||||
}}>
|
||||
<div style={{marginLeft: 20}}>
|
||||
<MessageList/>
|
||||
<MessageList count={props.messageCount} onCountChange={props.onMessageCountChange}/>
|
||||
</div>
|
||||
<div style={{
|
||||
height: '100%',
|
||||
|
@ -121,17 +124,20 @@ function HeaderBar(props: any) {
|
|||
}
|
||||
|
||||
function MainMenu(props: any) {
|
||||
console.log(props)
|
||||
if (props.defaultSelectedKeys[0] === '')
|
||||
return null
|
||||
else
|
||||
const [key, setKey] = useState(1)
|
||||
useEffect(() => {
|
||||
setKey(key + 1)
|
||||
}, [props]);
|
||||
return (
|
||||
<div key={key} style={{width: '100%'}}>
|
||||
<Menu
|
||||
style={{borderInlineEnd: 'unset'}}
|
||||
style={{width: '100%', borderInlineEnd: 'unset'}}
|
||||
mode="inline"
|
||||
selectedKeys={props.defaultSelectedKeys}
|
||||
items={props.items}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -141,54 +147,54 @@ function HomeView() {
|
|||
const location = useLocation()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const [messageCount, setMessageCount] = useState([])
|
||||
const [messageCount, setMessageCount] = useState(0)
|
||||
const [reimbursementCount, setReimbursementCount] = useState(0)
|
||||
const [badgeTop, setBadgeTop] = useState(66 + 3 * 44)
|
||||
const [eventSource, setEventSource] = useState<EventSourcePolyfill | null>(null)
|
||||
|
||||
// 新建Sse连接
|
||||
const createSseConnect = () => {
|
||||
if (window.EventSource) {
|
||||
if (eventSource === null) {
|
||||
let source = new EventSourcePolyfill(baseUrl + 'common/notice/listen', {
|
||||
headers: {
|
||||
Authorization: "Bearer " + store.getState().token.accessToken
|
||||
},
|
||||
heartbeatTimeout: 21600000
|
||||
})
|
||||
|
||||
let source = new EventSource('http://localhost:3000/api1/sse/createConnect?clientId=001')
|
||||
|
||||
// 监听打开事件
|
||||
source.addEventListener('open', (e) => {
|
||||
console.log("打开连接 onopen==>", e)
|
||||
console.log("open==>", e)
|
||||
})
|
||||
|
||||
// 监听消息事件
|
||||
source.addEventListener("message", (e) => {
|
||||
|
||||
source.addEventListener("init", (e) => {
|
||||
console.log("init==>", e)
|
||||
})
|
||||
|
||||
source.addEventListener("reimbursement", (e) => {
|
||||
console.log("reimbursement==>", e)
|
||||
// @ts-ignore
|
||||
setReimbursementCount(reimbursementCount + Number(e.data))
|
||||
})
|
||||
|
||||
source.addEventListener("notice", (e) => {
|
||||
console.log("notice==>", e)
|
||||
// @ts-ignore
|
||||
setMessageCount(messageCount + Number(e.data))
|
||||
})
|
||||
|
||||
// 监听错误事件
|
||||
source.addEventListener("error", (e) => {
|
||||
|
||||
console.log("error==>", e)
|
||||
})
|
||||
|
||||
// 关闭连接
|
||||
source.close = () => {
|
||||
console.log("source.close")
|
||||
}
|
||||
|
||||
} else {
|
||||
alert("该浏览器不支持SSE")
|
||||
}
|
||||
setEventSource(source)
|
||||
}
|
||||
|
||||
// 获取系统消息
|
||||
const getSystemMessage = () => {
|
||||
// 发送网络请求
|
||||
axiosInstance.post(`http://localhost:3000/api1/sse/broadcast`).then(
|
||||
response => {
|
||||
|
||||
},
|
||||
error => {
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
let items = [{
|
||||
const items = [{
|
||||
key: "/invoice/mine",
|
||||
//icon: React.createElement(UserOutlined),
|
||||
label: <Link to="/invoice/mine">我的发票</Link>
|
||||
|
@ -203,7 +209,7 @@ function HomeView() {
|
|||
}, {
|
||||
key: "/reimbursement/approval",
|
||||
//icon: React.createElement(UserOutlined),
|
||||
label: <Link to="/reimbursement/approval">报销审批</Link>,
|
||||
label: <Link to="/reimbursement/approval">报销审批</Link>
|
||||
}, {
|
||||
key: "/stat",
|
||||
//icon: React.createElement(UserOutlined),
|
||||
|
@ -236,21 +242,59 @@ function HomeView() {
|
|||
navigate('/invoice/mine')
|
||||
}
|
||||
|
||||
let reimbursementStatus: number[] = []
|
||||
|
||||
if (token.staffId === 'manager') {
|
||||
setBadgeTop(66 + 0 * 44)
|
||||
setMenuItems(items.slice(3, 6))
|
||||
reimbursementStatus.push(4)
|
||||
} else if (staff.managingDepartment) {
|
||||
setBadgeTop(66 + 3 * 44)
|
||||
if (staff.managingDepartment.departmentId === 1) {
|
||||
setMenuItems(items.slice(0, 5))
|
||||
reimbursementStatus.push(3)
|
||||
} else {
|
||||
setMenuItems(items.slice(0, 4))
|
||||
reimbursementStatus.push(1)
|
||||
}
|
||||
} else {
|
||||
if (staff.staffDepartments && staff.staffDepartments.find((value, index, obj) => value.departmentId === 1))
|
||||
setBadgeTop(66 + 3 * 44)
|
||||
if (staff.staffDepartments && staff.staffDepartments.find((value, index, obj) => value.departmentId === 1)) {
|
||||
setMenuItems(items.slice(0, 5))
|
||||
else
|
||||
reimbursementStatus.push(2)
|
||||
} else
|
||||
setMenuItems(items.slice(0, 2))
|
||||
}
|
||||
|
||||
axiosInstance({
|
||||
url: 'common/notice',
|
||||
method: 'get',
|
||||
params: {
|
||||
pageNum: 0,
|
||||
pageSize: 100
|
||||
}
|
||||
}).then(response => {
|
||||
const data: NoticeResponse = response.data
|
||||
let unreadCount = 0
|
||||
for (const notice of data.records)
|
||||
if (!notice.alreadyRead) unreadCount++
|
||||
setMessageCount(unreadCount)
|
||||
}).catch(function (error) {
|
||||
console.log(error)
|
||||
})
|
||||
|
||||
axiosInstance({
|
||||
url: 'approval/reimbursement?'+ qs.stringify({
|
||||
pageNum: 0,
|
||||
pageSize: 1,
|
||||
reimbursementStatus: reimbursementStatus
|
||||
}, {skipNulls: true, arrayFormat: 'indices'}),
|
||||
method: 'get'
|
||||
}).then(response => {
|
||||
setReimbursementCount(response.data.total)
|
||||
}).catch(function (error) {
|
||||
console.log(error)
|
||||
})
|
||||
}).catch(function (error) {
|
||||
console.log(error)
|
||||
})
|
||||
|
@ -261,7 +305,7 @@ function HomeView() {
|
|||
navigate("/login")
|
||||
}
|
||||
getStaffInfo()
|
||||
|
||||
createSseConnect()
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
setDefaultSelectedKeys([location.pathname])
|
||||
|
@ -299,10 +343,26 @@ function HomeView() {
|
|||
marginLeft: '4px'
|
||||
}}>智能财务报销系统</span>
|
||||
</div>
|
||||
<div style={{width: '100%', display: "flex"}}>
|
||||
<MainMenu items={menuItems} defaultSelectedKeys={defaultSelectedKeys}/>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
right: 14,
|
||||
top: badgeTop,
|
||||
height: 44,
|
||||
display: "flex",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<Badge count={reimbursementCount}/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</Sider>
|
||||
<Layout>
|
||||
<HeaderBar/>
|
||||
<HeaderBar messageCount={messageCount} onMessageCountChange={(count) => setMessageCount(count)}/>
|
||||
<Content style={{margin: '0', overflowY: "auto"}}>
|
||||
<Outlet/>
|
||||
</Content>
|
||||
|
|
|
@ -7,14 +7,16 @@ import 'dayjs/locale/zh-cn'
|
|||
import axiosInstance from "../utils/axiosInstance";
|
||||
import {setStaff, setToken} from "../models/store";
|
||||
import {Staff} from "../models/Staff";
|
||||
import {invoiceTypeNameMap} from "../models/Invoice";
|
||||
|
||||
const {Text, Title, Paragraph} = Typography
|
||||
const relativeTime = require('dayjs/plugin/relativeTime')
|
||||
dayjs.locale('zh-cn')
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
function MessageList() {
|
||||
function MessageList(props: { count: number, onCountChange: (count: number) => void}) {
|
||||
const [list, setList] = useState<Notice[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const getTitle = (notice: Notice) => {
|
||||
switch (notice.data.noticeType) {
|
||||
|
@ -27,7 +29,7 @@ function MessageList() {
|
|||
return '报销单状态更新'
|
||||
break
|
||||
case 1:
|
||||
return '发票被打回'
|
||||
return '发票被撤回'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -54,22 +56,29 @@ function MessageList() {
|
|||
switch (notice.data.noticeType) {
|
||||
case 0:
|
||||
if (notice.data.approvalResult === 5)
|
||||
return '报销单审批未通过'
|
||||
return <Paragraph>{
|
||||
notice.data.reimbursement?.reimbursementId + '号报销单(' + notice.data.reimbursement?.reimbursementDepartureName
|
||||
+ (notice.data.reimbursement?.roundTrip ? " ⇌ " : " → ") + notice.data.reimbursement?.reimbursementDestinationName + ')'
|
||||
+ '『' + notice.data.approvalStaff?.staffName + '』审批不通过'
|
||||
}<br/>{'审批意见:' + notice.data.approvalOpinion}</Paragraph>
|
||||
else
|
||||
return <Paragraph>{
|
||||
notice.data.reimbursement?.reimbursementId + '号报销单(' + notice.data.reimbursement?.reimbursementDepartureName
|
||||
+ (notice.data.reimbursement?.roundTrip ? " ⇌ " : " → ") + notice.data.reimbursement?.reimbursementDestinationName + ')'
|
||||
+ getApprovalStep(notice.data.approvalResult) + '『' + notice.data.approvalStaff?.staffName + '』审批通过'
|
||||
}<br/>{'审批意见:' + notice.data.approvalOpinion}</Paragraph>
|
||||
|
||||
break
|
||||
case 1:
|
||||
return '发票被打回'
|
||||
if (notice.data.invoice)
|
||||
return <Paragraph>{
|
||||
'一张' + invoiceTypeNameMap.get(notice.data.invoice?.invoiceKind) + '(¥' + notice.data.invoice?.invoiceAmount / 100. + ')被『' + notice.data.rejectStaff?.staffName + '』撤回'
|
||||
}<br/>{'撤回原因:' + notice.data.rejectOpinion}</Paragraph>
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const onMessageListOpen = () => {
|
||||
setLoading(true)
|
||||
axiosInstance({
|
||||
url: 'common/notice',
|
||||
method: 'get',
|
||||
|
@ -81,6 +90,7 @@ function MessageList() {
|
|||
console.log(response.data)
|
||||
const data: NoticeResponse = response.data
|
||||
setList(data.records)
|
||||
setLoading(false)
|
||||
}).catch(function (error) {
|
||||
console.log(error)
|
||||
})
|
||||
|
@ -163,6 +173,7 @@ function MessageList() {
|
|||
<List
|
||||
style={{width: 400}}
|
||||
itemLayout="horizontal"
|
||||
loading={loading}
|
||||
dataSource={list}
|
||||
renderItem={(item) => (
|
||||
<div style={{
|
||||
|
@ -180,22 +191,36 @@ function MessageList() {
|
|||
whiteSpace: "unset",
|
||||
textAlign: "unset"
|
||||
}}>
|
||||
<Dropdown menu={{
|
||||
<Dropdown disabled={item.alreadyRead} menu={{
|
||||
items: [
|
||||
{
|
||||
key: '1',
|
||||
label: (<span>标为已读</span>),
|
||||
}],
|
||||
onClick: ({key}) => {
|
||||
|
||||
axiosInstance({
|
||||
url: 'common/notice/' + item.noticeId + '/read',
|
||||
method: 'put',
|
||||
}).then(response => {
|
||||
props.onCountChange(props.count-1)
|
||||
setList(list.map((value, index, array) => {
|
||||
if (value === item)
|
||||
value.alreadyRead = true
|
||||
return value
|
||||
}))
|
||||
}).catch(function (error) {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
}} placement="top" arrow>
|
||||
<div>
|
||||
<Badge status="processing"/>
|
||||
<Badge status="processing" style={{
|
||||
display: item.alreadyRead ? 'none' : 'unset',
|
||||
marginRight: 10
|
||||
}}/>
|
||||
<span style={{
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
marginLeft: 10
|
||||
}}>{getTitle(item)} </span>
|
||||
<Divider type="vertical"/>
|
||||
<Text>{
|
||||
|
@ -210,15 +235,30 @@ function MessageList() {
|
|||
<Button style={{
|
||||
marginLeft: -30
|
||||
}} shape='circle'
|
||||
type='text'><CloseOutlined/></Button>
|
||||
type='text' onClick={() => {
|
||||
axiosInstance({
|
||||
url: 'common/notice/' + item.noticeId,
|
||||
method: 'delete',
|
||||
}).then(response => {
|
||||
setList(list.filter(value => value !== item))
|
||||
if(!item.alreadyRead) props.onCountChange(props.count-1)
|
||||
}).catch(function (error) {
|
||||
console.log(error)
|
||||
})
|
||||
|
||||
}}>
|
||||
<CloseOutlined/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
)}
|
||||
/>
|
||||
} title="我的消息" arrowPointAtCenter onOpenChange={onMessageListOpen}>
|
||||
<Button type="text" shape="circle">
|
||||
<Badge count={props.count} size="small" offset={[-10, 10]}>
|
||||
<Button type="text" shape="circle" size="large">
|
||||
<BellOutlined/>
|
||||
</Button>
|
||||
</Badge>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -27,8 +27,10 @@ function LoginView() {
|
|||
refreshToken: response.data.refreshToken,
|
||||
clientSecret: response.data.clientSecret
|
||||
}))
|
||||
//models.commit('setStaff', response.data.data)
|
||||
navigate('/')
|
||||
if (values.staffId === 'manager')
|
||||
navigate('/reimbursement/approval')
|
||||
else
|
||||
navigate('/invoice/mine')
|
||||
}).catch(function (error) {
|
||||
console.log(error);
|
||||
setAlertMessage(error.response.data.msg)
|
||||
|
|
Loading…
Reference in New Issue