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 && - } + }