提交 066986f7 authored 作者: 王鹏飞's avatar 王鹏飞

chore: update

上级 4aaa63c5
......@@ -3,5 +3,8 @@ module.exports = {
env: { browser: true, es2020: true },
extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:react-hooks/recommended'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } }
settings: { react: { version: '18.2' } },
rules: {
'react/prop-types': 'off'
}
}
import axios from '@/utils/axios'
// 获取用户
export function getUser() {
return axios.post('/api/user/user/getUserInfo')
}
// 获取菜单
export function getMenu() {
return axios.post('/api/system/power/getMenuList')
}
// 获取STSToken
export function getSTSToken(data) {
return axios.post('/api/common/OssUpload/getUploadToken', data)
}
import axios from '@/utils/axios'
// 获取未读消息条数
export function getMessageCount(data) {
return axios.post('/api/message/message/getMessageCount', data)
}
// 获取消息列表
export function getMessageList(data) {
return axios.post('/api/message/message/getMessageList', data)
}
// 设置部分已读
export function setMessageRead(data) {
return axios.post('/api/message/message/setOneRead', data)
}
// 设置全部已读
export function setMessageAllRead() {
return axios.post('/api/message/message/setRead')
}
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { Table, Row, Col, Form, Button, Space } from 'antd';
import IconReset from '@/assets/images/icon/reset.png';
import IconReload from '@/assets/images/icon/reload.png';
import IconFilter from '@/assets/images/icon/filter.png';
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react'
import { Table, Row, Col, Form, Button, Space } from 'antd'
import IconReset from '@/assets/images/icon/reset.png'
import IconReload from '@/assets/images/icon/reload.png'
import IconFilter from '@/assets/images/icon/filter.png'
const AppList = forwardRef((props, ref) => {
const { remote = {}, filters = [], filterAside, ...rest } = props;
const [data, setData] = useState({ total: 0, list: [] });
const [page, setPage] = useState({ current: 1, pageSize: 10 });
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const { remote = {}, filters = [], filterAside, ...rest } = props
const [data, setData] = useState({ total: 0, list: [] })
const [page, setPage] = useState({ current: 1, pageSize: 10 })
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
// 分页配置
const pagination = {
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '30', '40'],
showTotal: (total) => `共${total}条数据`,
onChange: handlePageChange,
};
showTotal: total => `共${total}条数据`,
onChange: handlePageChange
}
// 获取列表数据
async function fetchData() {
let { request, beforeRequest, afterRequest } = remote;
if (!request) return;
setLoading(true);
let params = { page: page.current, page_size: page.pageSize, ...form.getFieldsValue() };
let { request, beforeRequest, afterRequest } = remote
if (!request) return
setLoading(true)
let params = { page: page.current, page_size: page.pageSize, ...form.getFieldsValue() }
if (beforeRequest) {
params = await beforeRequest(params);
params = await beforeRequest(params)
}
const res = await request(params);
const { list = [], total = 0 } = afterRequest
? await afterRequest(res.data, params)
: res.data || {};
setData({ list, total });
setLoading(false);
const res = await request(params)
const { list = [], total = 0 } = afterRequest ? await afterRequest(res.data, params) : res.data || {}
setData({ list, total })
setLoading(false)
}
useEffect(() => {
fetchData();
}, [page]);
fetchData()
}, [page])
// 筛选
async function handleSearch() {
setPage({ ...page, current: 1 });
setPage({ ...page, current: 1 })
}
// 重置
async function handleReset() {
// 清空筛选条件
form.resetFields();
setPage({ ...page, current: 1 });
form.resetFields()
setPage({ ...page, current: 1 })
}
// 刷新
async function handleReload(isForce = false) {
isForce ? await handleSearch() : await fetchData();
isForce ? await handleSearch() : await fetchData()
}
// 分页改变
async function handlePageChange(current, pageSize) {
setPage({ current, pageSize });
setPage({ current, pageSize })
}
const onValuesChange = (changedValues, allValues) => {
const [value] = Object.values(changedValues);
!value && handleReload();
};
const [value] = Object.values(changedValues)
!value && handleReload()
}
useImperativeHandle(ref, () => {
return { handleSearch, handleReset, handleReload };
});
return { handleSearch, handleReset, handleReload }
})
const filterElement = (
<div className='app-list-filter' style={{ marginBottom: 20 }}>
<div className='app-list-filter' style={{ marginBottom: 20 }}>
<Row justify='space-between'>
<div className="app-list-filter" style={{ marginBottom: 20 }}>
<div className="app-list-filter" style={{ marginBottom: 20 }}>
<Row justify="space-between">
<Col>
<Form layout='inline' autoComplete='off' form={form} onValuesChange={onValuesChange}>
{filters.map((item) => (
<Form layout="inline" autoComplete="off" form={form} onValuesChange={onValuesChange}>
{filters.map(item => (
<Form.Item label={item.label} name={item.name} key={item.label}>
{item.element}
</Form.Item>
))}
<Space>
<Button
type='primary'
htmlType='button'
ghost
icon={<Icon src={IconFilter} />}
onClick={handleSearch}
>
<Button type="primary" htmlType="button" ghost icon={<Icon src={IconFilter} />} onClick={handleSearch}>
筛选
</Button>
<Button
type='primary'
htmlType='button'
ghost
icon={<Icon src={IconReset} />}
onClick={handleReset}
>
<Button type="primary" htmlType="button" ghost icon={<Icon src={IconReset} />} onClick={handleReset}>
重置
</Button>
<Button
type='primary'
htmlType='button'
ghost
icon={<Icon src={IconReload} />}
onClick={handleReload}
>
<Button type="primary" htmlType="button" ghost icon={<Icon src={IconReload} />} onClick={handleReload}>
刷新
</Button>
</Space>
......@@ -111,26 +91,20 @@ const AppList = forwardRef((props, ref) => {
</Row>
</div>
</div>
);
)
return (
<div className='app-list'>
<div className="app-list">
{filters.length > 0 && filterElement}
<div className='app-list-table'>
<Table
rowKey='id'
dataSource={data.list}
loading={loading}
pagination={pagination}
{...rest}
></Table>
<div className="app-list-table">
<Table rowKey="id" dataSource={data.list} loading={loading} pagination={pagination} {...rest}></Table>
</div>
</div>
);
});
)
})
const Icon = ({ src }) => {
const IconStyle = { height: '12px', objectFit: 'contain' };
return <img src={src} style={IconStyle} />;
};
const IconStyle = { height: '12px', objectFit: 'contain' }
return <img src={src} style={IconStyle} />
}
export default AppList;
export default AppList
import React, { useState, useEffect } from 'react';
import { Button, Row, Col, Menu } from 'antd';
import { useLocation, useNavigate, Link } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { LeftOutlined } from '@ant-design/icons';
const BreadCrumbMenu = () => {
const location = useLocation();
const navigate = useNavigate();
const [breadcrumbList, setBreadcrumbList] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
const { menuRouter } = useSelector((state) => state.user);
const [isShowMenu, setisShowMenu] = useState(true);
const [isShowBack, setIsShowBack] = useState(false);
const arr = ['/teacher', '/userinfo'];
const transLinkMenu = (route, bool) => {
return bool ? <span>{route.title}</span> : <Link to={route.path}>{route.title}</Link>;
};
useEffect(() => {
const pathnameArr = location.pathname.split('/');
if (pathnameArr.length > 0) {
const firstKey = pathnameArr[1];
const secondKey = pathnameArr[2];
setSelectedKeys([`/${firstKey}/${secondKey}`]);
if (secondKey) {
const breadMenu = menuRouter.filter((item) => item.path === `/${firstKey}`);
if (breadMenu && breadMenu.length && breadMenu[0].children.length) {
const menuTemp = [];
breadMenu[0].children.forEach((cItem, cindex) => {
menuTemp.push({
label: transLinkMenu(cItem),
key: cItem.path,
});
});
setBreadcrumbList(menuTemp);
}
}
}
}, [location.pathname]);
useEffect(() => {
if (
window.location.pathname === '/books/management/add-edit' ||
window.location.pathname === '/setting/help/addedit' ||
window.location.pathname === '/books/management/chapter' ||
window.location.pathname === '/userinfo' ||
window.location.pathname === '/books/audit/detail' ||
window.location.pathname === '/books/sale/discussP' ||
window.location.pathname === '/books/sale/discuss-detail' ||
window.location.pathname === '/member/list/detail' ||
window.location.pathname === '/books/sale/detail'
) {
setIsShowBack(true);
} else {
setIsShowBack(false);
}
}, [location.pathname]); // 将条件判断移到依赖数组中
return (
<div className='crumb-bread-list'>
<div className='crumb-bread-menu'>
<Row style={{ alignItems: 'center' }}>
<Col span={isShowBack} className='breadmenu-container-box'>
<div className='backTo'>
{isShowBack ? (
<Col>
<Button type='text' icon={<LeftOutlined />} onClick={() => navigate(-1)}>
返回
</Button>
</Col>
) : null}
</div>
{isShowMenu && (
<Menu items={breadcrumbList} mode='horizontal' selectedKeys={selectedKeys} />
)}
</Col>
</Row>
</div>
</div>
);
};
export default BreadCrumbMenu;
import { Menu, Button } from 'antd'
import { LeftOutlined } from '@ant-design/icons'
import { useLocation, useNavigate } from 'react-router-dom'
export default function Breadcrumb({ menuList }) {
const { pathname } = useLocation()
const navigate = useNavigate()
const currentMenusList =
menuList.find(item => {
const regExp = new RegExp(`^${item.path}`)
return regExp.test(pathname)
})?.children || []
const selectedKeys = currentMenusList
.filter(item => {
const regExp = new RegExp(`^${item.path}`)
return regExp.test(pathname)
})
.map(item => item.key)
// const hasBack = currentMenusList.find(item => {
// const regExp = new RegExp(`^${item.path}`)
// return regExp.test(pathname) && item.path !== pathname
// })
const hasBack = [
'/books/management/add-edit',
'/setting/help/addedit',
'/books/management/chapter',
'/userinfo',
'/books/audit/detail',
'/books/sale/discussP',
'/books/sale/discuss-detail',
'/member/list/detail',
'/books/sale/detail'
].includes(pathname)
return (
<div className="layout-breadcrumb">
<div>
{hasBack && (
<Button type="text" icon={<LeftOutlined />} onClick={() => navigate(-1)}>
返回
</Button>
)}
</div>
<Menu className="menu" mode="horizontal" items={currentMenusList} selectedKeys={selectedKeys} style={{ border: 'none' }} />
</div>
)
}
import { Button } from 'antd'
import { LoginOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
export default function Logout() {
const navigator = useNavigate()
const handleLogout = () => {
localStorage.clear()
navigator('/login')
}
return <Button style={{ border: 'none', background: '#f6f6f6' }} icon={<LoginOutlined />} onClick={handleLogout}></Button>
}
import React, { useEffect, useState, memo } from 'react';
import { LeftOutlined } from '@ant-design/icons';
import { Space, Menu } from 'antd';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { findTreeElementByKey } from '@/utils/common.js';
const UserMenu = (props) => {
const { flag, breadmenu, menuList } = props;
const location = useLocation();
const [selectedKeys, setSelectedKeys] = useState([]);
const transLinkComponent = (route, bool) => {
return bool ? <span>{route.meta.title}</span> : <Link to={route.path}>{route.meta.title}</Link>;
{
/* <span onClick={() => navigatorTo(route)}>{ route.meta.title }</span> */
}
};
const generateMenuItems = (routes, children = 'children') => {
return routes.reduce((menuItems, route) => {
if (route.hidden) {
return menuItems;
}
let menuItem = null;
menuItem = {
title: route.meta.title,
key: route.path,
};
if (route[children] && route[children].length > 0) {
const childrenItems = generateMenuItems(route.children);
if (childrenItems.length > 0) {
menuItem[children] = childrenItems;
menuItem.label = transLinkComponent(route, true);
} else {
menuItem.label = transLinkComponent(route);
}
} else {
menuItem.label = transLinkComponent(route);
}
return [...menuItems, menuItem];
}, []);
};
// 设置默认高亮
useEffect(() => {
const selectKeysItems = location.pathname.split('/');
if (breadmenu) {
// selectKeysItems.shift();
// if (selectKeysItems.length >=3) selectKeysItems.pop();
const lightMenu = selectKeysItems.slice(0, 3);
// const selectPath = selectKeysItems.map(item => `/${item}`);
setSelectedKeys(lightMenu.join('/'));
} else {
// selectKeysItems.shift();
// const selectPath = selectKeysItems.map(item => `/${item}`);
const lightMenu = selectKeysItems.slice(0, 2);
setSelectedKeys(lightMenu.join('/'));
// setSelectedKeys(selectPath);
}
}, [location.pathname]);
const [newMenus, setNewMenus] = useState([]);
const transLinkMenu = (route, bool) => {
return bool ? <span>{route.title}</span> : <Link to={route.path}>{route.title}</Link>;
};
const toAntdMenus = (menus, depth = 1) => {
return menus.map((item) => {
const newItem = {
label: depth === 1 ? transLinkMenu(item, true) : transLinkMenu(item),
key: item.path,
};
if (depth < 2 && item.children.length) {
newItem.children = toAntdMenus(item.children, depth + 1);
}
return newItem;
});
};
useEffect(() => {
if (menuList && menuList.length) {
const newMenusArr = toAntdMenus(menuList);
setNewMenus(newMenusArr);
}
}, [menuList]);
import { Menu } from 'antd'
import { useLocation } from 'react-router-dom'
export default function AppMenu({ menuList }) {
const { pathname } = useLocation()
const selectedKeys = menuList
.filter(item => {
const regExp = new RegExp(`^${item.path}`)
return regExp.test(pathname)
})
.map(item => item.key)
return (
<div className='header-menu'>
<div className="header-menu">
<Menu
items={newMenus}
mode='horizontal'
className="menu"
mode="horizontal"
items={menuList}
selectedKeys={selectedKeys}
style={{ border: 'none' }}
builtinPlacements={{
bottomLeft: {
points: ['tc', 'bc'], // 子菜单的 "上中" 和 对应菜单的title "下中" 对齐。
overflow: {
adjustX: 10,
adjustY: 10,
},
offset: [0, 3], //让下拉菜单位移
},
overflow: { adjustX: 10, adjustY: 10 },
offset: [0, 3] //让下拉菜单位移
}
}}
></Menu>
/>
</div>
);
};
export default memo(UserMenu);
)
}
import { Badge, Button, Drawer, Row, Col } from 'antd'
import { BellOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
import { getMessageList, getMessageCount, setMessageRead, setMessageAllRead } from '@/api/message'
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
function Message() {
const [open, setOpen] = useState(false)
const [dot, setDot] = useState(false)
const getMessageNum = async () => {
const {
data: { num = 0 }
} = await getMessageCount()
setDot(num !== 0)
}
useEffect(() => {
getMessageNum()
}, [])
const handleRead = () => {
getMessageNum()
setOpen(false)
}
return (
<>
<Button onClick={() => setOpen(true)} style={{ border: 'none', background: '#f6f6f6' }}>
<Badge dot={dot}>
<BellOutlined />
</Badge>
</Button>
<MessageDraw open={open} onClose={() => setOpen(false)} onRead={handleRead} />
</>
)
}
function MessageDraw({ onRead, ...rest }) {
const handleReadAll = async () => {
await setMessageAllRead()
onRead()
}
return (
<Drawer
{...rest}
destroyOnClose
closeIcon={<MenuUnfoldOutlined />}
extra={
<span style={{ fontSize: '14px', color: '#999', cursor: 'pointer' }} onClick={handleReadAll}>
全部已读
</span>
}>
<MessageList onRead={onRead} />
</Drawer>
)
}
function MessageList({ onRead }) {
const navigator = useNavigate()
const [messageList, setMessageList] = useState([])
const getMessage = async () => {
const {
data: { list = [] }
} = await getMessageList({ page: 1, page_size: 10 })
setMessageList(list)
}
const handleRead = async item => {
await setMessageRead({ id: item.id })
if (item.type === 1) {
navigator('/books/audit/dataset')
} else if (item.type === 2) {
navigator('/books/sale/dataset')
}
onRead(item)
}
useEffect(() => {
getMessage()
}, [])
return (
<>
{messageList.map(item => {
return <MessageListItem item={item} key={item.id} onRead={() => handleRead(item)} />
})}
</>
)
}
function MessageListItem({ item, onRead }) {
return (
<Row key={item.id} style={{ borderRadius: 4, border: '1px solid #e4e4e4', padding: 15, marginBottom: 20, cursor: 'pointer' }} onClick={onRead}>
<Col span={1} style={{ marginRight: 15 }}>
<Badge dot={item.status == 0}>
<BellOutlined />
</Badge>
</Col>
<Col span={22} className="process">
<div className="head" style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}>
<strong>审核通知</strong>
<span className="date" style={{ color: '#999999', fontSize: 12 }}>
{item.create_time}
</span>
</div>
<div className="content">{item.content}</div>
</Col>
</Row>
)
}
export default Message
import { useEffect } from 'react'
import { Button, Flex, Dropdown } from 'antd'
import { UserOutlined } from '@ant-design/icons'
import { useDispatch, useSelector } from 'react-redux'
import { setUserInfo } from '@/store/modules/user'
import { getUser } from '@/api/base'
function User() {
const dispatch = useDispatch()
const { userInfo } = useSelector(state => state.user)
const fetchUser = async () => {
const { data } = await getUser()
dispatch(setUserInfo(data))
}
useEffect(() => {
fetchUser()
}, [])
const menuItems = [
{
key: 'user',
label: (
<Button type="link" href="/userinfo">
个人中心
</Button>
)
}
]
return (
<Dropdown menu={{ items: menuItems }} arrow>
<Button style={{ border: 'none', background: '#f6f6f6' }}>
<Flex align="center" gap="small">
{userInfo.pic ? <img src={userInfo.pic} style={{ width: '28px', height: '28px', borderRadius: '50%', objectFit: 'cover' }} /> : <UserOutlined />}
{userInfo.real_name}
</Flex>
</Button>
</Dropdown>
)
}
export default User
import React, { useState, useEffect, useRef } from 'react'
import { UserOutlined, LogoutOutlined, BellOutlined } from '@ant-design/icons'
import { Button, Space, Dropdown, Drawer, Row, Col, Image } from 'antd'
import { useNavigate } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import msgWebsocket from '@/common/websocket/websocket'
import { setUserInfo } from '@/store/modules/user'
import { getUserInfo, getMessageList, getMessageCount, setRead, setAllRead } from '../request'
import bell from '../../assets/images/bell.png'
import point1 from '../../assets/images/point1.png'
import quit from '../../assets/images/quit.png'
import slide from '../../assets/images/slide.png'
const items = [
{
key: 'userinfo',
label: (
<Button type="link" href="/userinfo">
个人中心
</Button>
)
}
]
const UserInfo = ({ props, extraSlot }) => {
const navigator = useNavigate()
const dispatch = useDispatch()
const [showDrawer, setShowDrawer] = useState(false)
const drawerRef = useRef(null)
const { userInfo } = useSelector(state => state.user)
const [userInfos, setUserInfos] = useState({})
const [messageList, setMessageList] = useState([])
const [page, setPage] = useState(1)
const [page_size, setpage_size] = useState(10)
const [drawerScrolled, setDrawerScrolled] = useState(false)
const [unreadMessagesNum, setUnreadMessagesNum] = useState(0)
const heartbeatInterval = useRef(null)
const [isReadSet, setIsReadSet] = useState(false)
const { webSocketInit, wsMessage, setWsMessage, reconnect, sendMessage, closeWebSocket, wsReadyState } = msgWebsocket({})
const [messageListLoaded, setMessageListLoaded] = useState(false) // 新增状态来表示消息列表是否已加载
// 推出登录
const signOut = () => {
localStorage.clear()
navigator('/login')
}
const getInfo = async () => {
const data = await getUserInfo()
dispatch(setUserInfo(data))
setUserInfos(data)
}
// 所有消息中心
// 未读数
const noneRead = async () => {
const { num } = await getMessageCount()
setUnreadMessagesNum(num)
}
// 部分已读
const setMessageReaded = async id => {
setRead({ id })
}
// 全部已读
const setMessageALLReaded = async () => {
setAllRead()
setUnreadMessagesNum(0)
}
const getMessage = async () => {
const { list } = await getMessageList({ page: 1, page_size })
setMessageList(list)
setMessageListLoaded(true) // 设置消息列表已加载的状态为true
}
// 加载下一页消息
const loadNextPage = async () => {
const nextPage = page + 1
const { list } = await getMessageList({ page: nextPage, page_size })
setPage(nextPage)
setMessageList(prevList => [...prevList, ...list])
}
useEffect(() => {
getInfo()
getMessage()
noneRead()
}, [])
// 设置全部已读,且只调用一次
useEffect(() => {
if (!isReadSet && showDrawer && unreadMessagesNum > 0) {
setIsReadSet(true)
} else if (!showDrawer) {
// noneRead();
}
}, [showDrawer, unreadMessagesNum, isReadSet])
// 加载消息的websocket
useEffect(() => {
if (userInfos.id > 0) {
const wsUrl = `${import.meta.env.VITE_API_WEBSOCKET_URL}/ws`
// 建立websokect链接
webSocketInit(wsUrl)
// 获取websocket消息
if (Object.entries(wsMessage).length > 0) {
console.log('收到消息了')
console.log(wsMessage)
// 收到消息一个未读消息
setUnreadMessagesNum(unreadMessagesNum + 1)
}
}
}, [userInfos, wsMessage, wsReadyState])
// 重连websocket
// useEffect(() => {
// if (wsReadyState.key === 3) {
// reconnect();
// }
// if (wsReadyState.key === 1 && userInfos.id > 0) {
// // 链接成功后发送消息
// sendMessage('{"userId":' + userInfos.id + '}');
// // 心跳
// heartbeatInterval.current = setInterval(() => {
// sendMessage('{"heart":1}');
// }, 10000);
// // 在组件卸载或 WebSocket 关闭时清除间隔,以防止内存泄漏
// return () => clearInterval(heartbeatInterval.current);
// }
// }, [userInfos, wsReadyState]);
let reconnectTimeoutId
// 重连websocket
useEffect(() => {
// ...其他逻辑不变...
// 重连逻辑修改
if (wsReadyState.key === 3) {
clearTimeout(reconnectTimeoutId)
reconnectTimeoutId = setTimeout(reconnect, 3000) // 3秒后重试
}
// 清理函数中取消定时器
return () => {
clearTimeout(reconnectTimeoutId)
clearInterval(heartbeatInterval.current)
}
}, [userInfos, wsReadyState])
// 滚动消息抽屉翻页
useEffect(() => {
if (showDrawer) {
const handleScroll = () => {
const drawerElement = document.querySelector('.ant-drawer-body')
if (drawerElement && drawerElement.scrollTop + drawerElement.clientHeight >= drawerElement.scrollHeight) {
// 在抽屉滚动到底部时加载下一页数据
loadNextPage()
setDrawerScrolled(true)
// 更新滚动位置,使用户能够继续向上滚动
drawerElement.scrollTop = drawerElement.scrollHeight - drawerElement.clientHeight
} else {
setDrawerScrolled(false)
}
}
// 添加滚动事件监听
document.querySelector('.ant-drawer-body').addEventListener('scroll', handleScroll)
// 在组件卸载时移除滚动事件监听
return () => {
document.querySelector('.ant-drawer-body').removeEventListener('scroll', handleScroll)
}
}
}, [drawerScrolled, showDrawer])
//
// useEffect(() => {
// if (showDrawer) {
// const drawerElement = document.querySelector('.ant-drawer-body');
// // 获取上次关闭时保存的滚动位置
// const lastScrollPosition = localStorage.getItem('drawerScrollPosition');
// if (lastScrollPosition) {
// drawerElement.scrollTop = parseInt(lastScrollPosition, 10);
// }
// const handleScroll = () => {
// if (drawerElement.scrollTop + drawerElement.clientHeight >= drawerElement.scrollHeight) {
// // 在抽屉滚动到底部时加载下一页数据
// loadNextPage();
// setDrawerScrolled(true);
// } else {
// setDrawerScrolled(false);
// }
// };
// // 添加滚动事件监听
// drawerElement.addEventListener('scroll', handleScroll);
// // 在组件卸载时保存当前滚动位置
// return () => {
// localStorage.setItem('drawerScrollPosition', drawerElement.scrollTop.toString());
// drawerElement.removeEventListener('scroll', handleScroll);
// };
// }
// }, [drawerScrolled, showDrawer]);
// 在渲染时根据消息列表是否加载来决定显示哪个图标
const renderBellIcon = () => {
if (unreadMessagesNum > 0) {
return (
<span
style={{
display: 'inline-block',
position: 'relative',
width: '16px',
height: '19px',
pointerEvents: 'none'
}}>
<Image src={bell} style={{ width: 14 }} />
<Image
src={point1}
style={{
position: 'absolute',
top: -18,
right: -5,
width: '12px',
height: '12px'
}}
/>
</span>
)
} else {
return (
<span
style={{
display: 'inline-block',
width: '16px',
height: '19px',
pointerEvents: 'none'
}}>
<Image src={bell} style={{ width: 14 }} />
</span>
)
}
}
return (
<div className="header-user">
<Drawer
open={showDrawer}
// closeIcon={false}
closeIcon={
<span
style={{
display: 'inline-block',
width: '22px',
height: '18px',
cursor: 'pointer',
pointerEvents: 'none'
}}>
<Image
src={slide}
style={{
width: 14
}}
/>
</span>
}
extra={
<span
style={{
fontSize: '14px',
color: '#999',
cursor: 'pointer'
}}
draggable="false"
onClick={() => setMessageALLReaded()}>
全部已读
</span>
}
onClose={() => {
setShowDrawer(false)
setMessageListLoaded(false)
}}>
{/* <div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0px 10px 20px 10px',
}}
>
<span
style={{
display: 'inline-block',
width: '22px',
height: '18px',
cursor: 'pointer',
userSelect: 'none',
}}
draggable='false'
onClick={() => {
setShowDrawer(false);
setMessageListLoaded(false);
(e) => e.preventDefault();
}}
className='DrawerClose'
>
<Image
src={slide}
style={{
width: 14,
userSelect: 'none',
WebkitUserDrag: 'none',
userDrag: 'none',
outline: 'none',
}}
draggable='false'
/>
</span>
<span
style={{
fontSize: '14px',
color: '#999',
cursor: 'pointer',
}}
draggable='false'
onClick={() => setMessageALLReaded()}
>
全部已读
</span>
</div> */}
{messageList &&
messageList.length > 0 &&
messageList.map((item, index) => (
<Row
key={item.id}
className="processItem"
style={{
borderRadius: 4,
border: '1px solid #E4E4E4',
padding: 15,
marginBottom: 20
}}>
<Col span={1} style={{ marginRight: 15 }}>
<Button
style={{
border: 'none',
background: '#fff',
display: 'inline-block',
width: '19px',
height: '22px',
pointerEvents: 'none'
}}
icon={
index < unreadMessagesNum && item.status === 0 ? (
<span
style={{
display: 'inline-block',
position: 'relative',
width: '16px',
height: '19px',
pointerEvents: 'none'
}}>
<Image src={bell} style={{ width: 14 }} />
<Image
src={point1}
style={{
position: 'absolute',
top: -18,
right: -5,
width: '12px',
height: '12px'
}}
/>
</span>
) : (
<span
style={{
display: 'inline-block',
width: '16px',
height: '19px',
pointerEvents: 'none'
}}>
<Image src={bell} style={{ width: 14 }} />
</span>
)
}></Button>
</Col>
<Col span={22} className="process">
<div
className="head"
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 10
}}>
<strong>审核通知</strong>
<span
className="date"
style={{
color: '#999999',
fontSize: 12
}}>
{item.create_time}
</span>
</div>
<div
className="content"
onClick={() => {
item.isRead = true
if (item.type === 1) {
const handleType1 = async () => {
await navigator('/books/audit/dataset')
await setMessageReaded(item.id)
await getMessage()
await noneRead()
}
handleType1()
} else if (item.type === 2) {
const handleType2 = async () => {
await navigator('/books/sale/dataset')
await setMessageReaded(item.id)
await getMessage()
await noneRead()
}
handleType2()
// setUnreadMessagesNum(0);
} else {
const handleType3 = async () => {
await setMessageReaded(item.id)
await getMessage()
await noneRead()
}
handleType3()
}
setShowDrawer(false)
}}
style={{ cursor: 'pointer' }}>
{item.content}
</div>
</Col>
</Row>
))}
</Drawer>
<Space>
<div
className="header-user-bell"
style={{
width: 36,
height: 36,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: 'none',
marginTop: '10px'
}}>
<Button
style={{
border: 'none',
background: '#f6f6f6'
}}
onClick={() => {
setPage(1)
setShowDrawer(true)
getMessage()
// setMessageReaded();
}}
icon={renderBellIcon()}></Button>
</div>
<Dropdown menu={{ items }} arrow>
<Button
type="link"
className="logout"
onClick={e => e.preventDefault()}
style={{
padding: 10,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: 'none',
marginTop: '10px'
}}>
<Space>
{userInfos.pic ? <img src={userInfos.pic} style={{ width: '25px', height: '28px', borderRadius: '50%', marginTop: '6px' }} /> : <UserOutlined />}
{/* {<UserOutlined />} */}
{userInfos.real_name}
</Space>
</Button>
</Dropdown>
<div
style={{
width: 36,
height: 36,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<Button
type="link"
onClick={signOut}
className="logout"
style={{
marginTop: '10px'
}}
icon={
<span
style={{
display: 'inline-block',
width: '17px',
height: '18px',
pointerEvents: 'none',
alignItems: 'center',
justifyContent: 'center'
}}>
<Image src={quit} style={{ width: 18 }} />
</span>
}>
{/* <LogoutOutlined /> */}
</Button>
</div>
</Space>
</div>
)
}
export default UserInfo
import React, { useState, useEffect } from 'react'
import { Layout, Spin } from 'antd'
import { Outlet, useNavigate, Link } from 'react-router-dom'
import { useState, useEffect } from 'react'
import { Layout, Flex } from 'antd'
import { Outlet, Link } from 'react-router-dom'
import { useDispatch } from 'react-redux'
import { setMenuRouter, setOperationPermissionsList } from '@/store/modules/user'
import UserInfo from './components/userinfo'
import UserMenu from './components/menu'
import BreadCrumbMenu from './components/breadMenu'
import Menu from './components/menu'
import Message from './components/message'
import User from './components/user'
import Logout from './components/logout'
import Breadcrumb from './components/breadcrumb'
import './index.less'
import logo from '@/assets/images/logo.png'
import { getMenuList, checkSSOLogin } from './request'
import { generateToAntdMenus } from '@/utils/breadCrumb'
const { Header, Content, Footer } = Layout
const { Header, Content } = Layout
const LayoutComponent = () => {
function generateMenus(items = [], maxLevel = 2, level = 1) {
return items.map(item => {
const children = level < maxLevel && item.childs ? generateMenus(item.childs, maxLevel, level + 1) : null
return { key: item.id + '', path: item.front_url, label: <Link to={item.front_url}>{item.name}</Link>, children }
})
}
export default function AppLayout() {
const dispatch = useDispatch()
const navigate = useNavigate()
const [loading, setLoading] = useState(false)
const [menuList, setMenuList] = useState([])
const [menuRole, setMenuRole] = useState([])
const [isSSOToken, setIsSSOToken] = useState(false)
const getSSOToken = async () => {
const data = await checkSSOLogin()
if (data && data.token) {
localStorage.setItem('kiwi.token', data.token)
setIsSSOToken(data.token)
}
}
// 获取菜单
const getMenu = async () => {
setLoading(true)
// 获取菜单内容
const { data, operation_permissions_list } = await getMenuList()
if (data && data.length) {
setMenuRole(data)
const menus = generateToAntdMenus(data, 'name', 'front_url', 'childs', 1)
setMenuList(menus)
setMenuList(generateMenus(data))
dispatch(setMenuRouter(menus))
}
// 获取操作权限内容
if (operation_permissions_list && operation_permissions_list.length) {
dispatch(setOperationPermissionsList(operation_permissions_list))
}
setLoading(false)
}
useEffect(() => {
;(async () => {
(async () => {
await getSSOToken()
await getMenu()
})()
}, [])
useEffect(() => {
if (menuRole.length > 0 && location.pathname === '/') {
setLoading(true)
setTimeout(() => {
if (menuRole.length) {
navigate(menuRole[0].front_url)
}
setLoading(false)
}, 1000)
}
}, [location.pathname, menuRole])
return (
<Layout className="layout-container">
<Spin spinning={loading}>
<div className="layout-sticky">
<Header className="layout-header">
<div className="header-logo">
<Link to="/">
<img src={logo} />
</Link>
</div>
<UserMenu menuList={menuList} />
{menuList && menuList.length && <UserInfo />}
<Menu menuList={menuList}></Menu>
<Flex align="center" gap="small">
<Message></Message>
<User></User>
<Logout></Logout>
</Flex>
</Header>
<Content className="layout-content">
<div className="crumb">
<BreadCrumbMenu />
</div>
<div className="content-sandbox">
<div className="content-pro" style={{ padding: 20 }}>
<Outlet aa={2} />
</div>
</div>
</Content>
</Spin>
<Breadcrumb menuList={menuList}></Breadcrumb>
</div>
<Content className="layout-content">
<Outlet />
</Content>
</Layout>
)
}
export default LayoutComponent
.layout-container {
width: 100vw;
height: 100vh;
overflow: hidden;
.layout-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 50px;
line-height: 50px;
background-color: #fff;
padding: 0 20px;
> .ant-spin-nested-loading {
height: 100vh;
> .ant-spin-container {
height: 100vh;
display: flex;
flex-direction: column;
.header-logo {
img {
height: 32px;
vertical-align: middle;
}
}
.layout-header {
display: flex;
align-items: center;
justify-content: space-between;
.header-menu {
flex: 1;
padding: 0 10px;
height: 50px;
line-height: 50px;
background-color: #fff;
padding: 0 20px;
.ant-menu-overflow {
margin-left: 100px;
font-size: 16px !important;
a {
color: inherit;
}
.ant-menu-overflow-item.ant-menu-submenu.ant-menu-submenu-horizontal {
width: 110px; //hyy
width: 110px;
display: flex;
justify-content: center;
}
}
.header-logo {
img {
height: 32px;
vertical-align: middle;
.ant-menu-light.ant-menu-horizontal > .ant-menu-submenu::after {
content: '';
display: none;
}
.ant-menu-light.ant-menu-horizontal > .ant-menu-item:hover,
.ant-menu-light.ant-menu-horizontal > .ant-menu-submenu:hover {
color: #ab1941 !important;
background-color: #ede2e8 !important;
transition: none !important;
.ant-menu-title-content {
color: #ab1941 !important;
transition: none !important;
}
}
.ant-menu-light.ant-menu-horizontal > .ant-menu-item:hover::after {
border: none !important;
transition: none !important;
}
.ant-menu-light.ant-menu-horizontal > .ant-menu-item {
border: none !important;
transition: none !important;
.header-menu {
flex: 1;
padding: 0 10px;
height: 50px;
line-height: 50px;
.ant-menu-light.ant-menu-horizontal > .ant-menu-submenu::after {
&::after {
content: '';
display: none;
}
.ant-menu-light.ant-menu-horizontal > .ant-menu-item:hover,
.ant-menu-light.ant-menu-horizontal > .ant-menu-submenu:hover {
}
.ant-menu-light.ant-menu-horizontal > .ant-menu-item-selected {
// color: #AB1941 !important;
background: #ede2e8 !important;
.ant-menu-title-content {
color: #ab1941 !important;
background-color: #ede2e8 !important;
transition: none !important;
.ant-menu-title-content {
color: #ab1941 !important;
transition: none !important;
}
}
.ant-menu-light.ant-menu-horizontal > .ant-menu-item:hover::after {
border: none !important;
transition: none !important;
}
.ant-menu-light.ant-menu-horizontal > .ant-menu-item {
border: none !important;
transition: none !important;
&::after {
content: '';
display: none;
}
}
.ant-menu-light.ant-menu-horizontal > .ant-menu-item-selected {
// color: #AB1941 !important;
background: #ede2e8 !important;
.ant-menu-title-content {
color: #ab1941 !important;
}
&::after {
border: none;
}
}
.ant-menu-light.ant-menu-horizontal > .ant-menu-submenu-selected {
background: #ede2e8 !important;
&::before{
content: '';
display: block;
width: 0;
height: 0;
clear: both;
z-index: 2;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 8px solid #ede2e8;
bottom: -8px;
position: absolute;
}
ml10 &::after {
border: none !important;
}
.ant-menu-submenu-title {
color: #ab1941 !important;
font-weight: bold;
}
&::after {
border: none;
}
}
.header-user {
text-align: right;
.logout {
color: #666;
background: #f6f6f6;
.header-user-bell:hover {
background: #f6f6f6 !important;
border-color: #f6f6f6 !important;
color: #f6f6f6 !important;
}
.header-user-bell:focus {
background: #f6f6f6 !important;
border-color: #f6f6f6 !important;
color: #f6f6f6 !important;
outline: none;
}
.ant-menu-submenu-selected {
background: #ede2e8 !important;
&::before {
content: '';
display: block;
width: 0;
height: 0;
clear: both;
z-index: 2;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 8px solid #ede2e8;
bottom: -8px;
position: absolute;
}
}
}
.layout-content {
display: flex;
flex-direction: column;
flex: 1;
.ant-menu-horizontal {
border-bottom: none;
}
.crumb {
padding: 10px 0px; //hyy
padding-top: 10px;
background: #f7f8fc;
.crumb-bread-list {
// height: 36px;
background-color: #fff;
// box-shadow: 0px 0px 20px 1px rgba(123, 123, 123, 0.2);
box-sizing: border-box;
border-radius: 5px;
padding: 3px 10px;
.crumb-bread-menu {
line-height: 30px;
color: #666;
font-size: 12px;
.backTo {
margin-top: 7px;
}
.breadmenu-container-box {
width: 100%;
display: flex;
justify-content: flex-end;
.ant-menu {
width: 100% !important;
justify-content: flex-end;
flex: 1 !important;
.ant-menu-overflow-item {
width: 110px; //hyy
display: flex;
justify-content: center;
}
}
}
.breadcrumb-box {
line-height: 30px;
.breadcrumb-item,
.breadcrumb-item span {
font-size: 12px;
}
}
.ant-menu-light.ant-menu-horizontal > .ant-menu-item:hover::after {
border-color: #ab1941;
}
.ant-menu-light.ant-menu-horizontal > .ant-menu-item {
transition: none;
}
.ant-menu-light.ant-menu-horizontal > .ant-menu-item-selected {
color: #ab1941 !important ;
font-weight: bold;
background-color: #fbfbfb;
transition: none;
&::after {
// border-bottom-color: #ab1941 !important ;
// width: 40px;
content: '';
display: block;
position: absolute;
bottom: -1px; /* 将下边框定位到菜单项底部 */
left: 50%; /* 将左侧边距设为菜单项宽度的一半 */
transform: translateX(-50%); /* 将菜单项水平居中 */
border-bottom: 2px solid #ab1941;
width: 60px;
}
}
.header-menu {
width: 110px;
flex: 1;
display: flex;
justify-content: flex-end;
.ant-menu {
flex: 1;
width: 110px;
display: flex;
justify-content: flex-end;
}
}
}
&::after {
border: none !important;
}
}
.content-sandbox {
flex: 1;
display: flex;
overflow: hidden;
.content-pro {
flex: 1;
background-color: #fff;
border-radius: 5px;
box-shadow: 0px 0px 20px 1px rgba(123, 123, 123, 0.2);
height: calc(100vh - 120px);
overflow-x: hidden;
overflow-y: auto;
padding: 10px;
.ant-menu-submenu-title {
color: #ab1941 !important;
font-weight: bold;
}
}
}
......@@ -237,8 +98,34 @@
.ant-menu .ant-menu-item {
text-align: center;
}
:global {
.ant-menu-vertical {
min-width: 96px !important;
.layout-breadcrumb {
height: 50px;
margin: 10px 0;
padding: 2px 20px;
background-color: #fff;
display: flex;
align-items: center;
.ant-menu {
flex: 1;
justify-content: flex-end;
}
.ant-menu-item {
width: 110px;
}
}
.layout-content {
background-color: #fff;
border-radius: 5px;
box-shadow: 0px 0px 20px 1px rgba(123, 123, 123, 0.2);
height: calc(100vh - 120px);
padding: 20px;
}
.layout-sticky {
position: sticky;
top: 0;
z-index: 1000;
background-color: #f7f8fc;
}
import { fileURLToPath, URL } from 'node:url';
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react-swc';
import mkcert from 'vite-plugin-mkcert';
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react-swc'
import mkcert from 'vite-plugin-mkcert'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd());
const env = loadEnv(mode, process.cwd())
return {
plugins: [react(), mkcert()],
server: {
......@@ -16,26 +16,26 @@ export default defineConfig(({ mode }) => {
'/api': {
target: env.VITE_API_URL_WORD,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''), // 将请求路径中的“/api”前缀替换为空字符串
rewrite: path => path.replace(/^\/api/, '') // 将请求路径中的“/api”前缀替换为空字符串
},
'/openapi': {
target: env.VITE_API_OPENAI_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/openapi/, '/openapi'), // 将请求路径中的“/api”前缀替换为空字符串
},
},
rewrite: path => path.replace(/^\/openapi/, '/openapi') // 将请求路径中的“/api”前缀替换为空字符串
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
css: {
preprocessorOptions: {
less: {
javascriptEnabled: true,
},
},
},
};
});
javascriptEnabled: true
}
}
}
}
})
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论