diff --git a/package-lock.json b/package-lock.json
index 267fef8..85148e5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 433108f..4d988c2 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/src/models/Notice.ts b/src/models/Notice.ts
index 1acb1df..c19fb0d 100644
--- a/src/models/Notice.ts
+++ b/src/models/Notice.ts
@@ -17,6 +17,7 @@ export interface Notice {
noticeId: string;
noticeTargetId: string;
noticeTime: string;
+ alreadyRead: boolean;
}
diff --git a/src/pages/HomeView.tsx b/src/pages/HomeView.tsx
index f1aeaf3..7b4fa2b 100644
--- a/src/pages/HomeView.tsx
+++ b/src/pages/HomeView.tsx
@@ -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)'
}}>
-
+
{
+ setKey(key + 1)
+ }, [props]);
+ return (
+
- )
+
+
+ )
}
function HomeView() {
@@ -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
(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: 我的发票
@@ -203,7 +209,7 @@ function HomeView() {
}, {
key: "/reimbursement/approval",
//icon: React.createElement(UserOutlined),
- label: 报销审批,
+ label: 报销审批
}, {
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'
}}>智能财务报销系统
-
+
+
+
-
+ setMessageCount(count)}/>
diff --git a/src/pages/MessageList.tsx b/src/pages/MessageList.tsx
index 19fee7d..9f193ea 100644
--- a/src/pages/MessageList.tsx
+++ b/src/pages/MessageList.tsx
@@ -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([])
+ 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 {
+ notice.data.reimbursement?.reimbursementId + '号报销单(' + notice.data.reimbursement?.reimbursementDepartureName
+ + (notice.data.reimbursement?.roundTrip ? " ⇌ " : " → ") + notice.data.reimbursement?.reimbursementDestinationName + ')'
+ + '『' + notice.data.approvalStaff?.staffName + '』审批不通过'
+ }
{'审批意见:' + notice.data.approvalOpinion}
else
return {
notice.data.reimbursement?.reimbursementId + '号报销单(' + notice.data.reimbursement?.reimbursementDepartureName
- + (notice.data.reimbursement?.roundTrip ? " ⇌ " : " → ") + notice.data.reimbursement?.reimbursementDestinationName+')'
- + getApprovalStep(notice.data.approvalResult)+'『'+notice.data.approvalStaff?.staffName+'』审批通过'
- }
{'审批意见:'+notice.data.approvalOpinion}
-
+ + (notice.data.reimbursement?.roundTrip ? " ⇌ " : " → ") + notice.data.reimbursement?.reimbursementDestinationName + ')'
+ + getApprovalStep(notice.data.approvalResult) + '『' + notice.data.approvalStaff?.staffName + '』审批通过'
+ }
{'审批意见:' + notice.data.approvalOpinion}
break
case 1:
- return '发票被打回'
+ if (notice.data.invoice)
+ return {
+ '一张' + invoiceTypeNameMap.get(notice.data.invoice?.invoiceKind) + '(¥' + notice.data.invoice?.invoiceAmount / 100. + ')被『' + notice.data.rejectStaff?.staffName + '』撤回'
+ }
{'撤回原因:' + notice.data.rejectOpinion}
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() {
(
-
标为已读),
}],
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>
-
+
{getTitle(item)}
{
@@ -210,15 +235,30 @@ function MessageList() {
+ 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)
+ })
+
+ }}>
+
+
)}
/>
} title="我的消息" arrowPointAtCenter onOpenChange={onMessageListOpen}>
-
+
+
+
)
}
diff --git a/src/pages/login/LoginView.tsx b/src/pages/login/LoginView.tsx
index fc97c7a..44b3107 100644
--- a/src/pages/login/LoginView.tsx
+++ b/src/pages/login/LoginView.tsx
@@ -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)
@@ -40,7 +42,7 @@ function LoginView() {
console.log('Failed:', errorInfo);
};
- const afterClose = () =>{
+ const afterClose = () => {
setShowAlert(false)
}
@@ -98,7 +100,7 @@ function LoginView() {
}
placeholder="密码"/>
- {showAlert&&
+ {showAlert &&
- }
+ }