index.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import { SetStateAction, useCallback, useEffect, useState } from 'react'
  2. import { Layout, Menu, Space, ConfigProvider, Watermark, App, message } from 'antd';
  3. import { DesktopOutlined, MessageOutlined, SendOutlined, CloudOutlined, TeamOutlined, HomeOutlined, PaperClipOutlined, ContainerOutlined, AlipayCircleOutlined, StopOutlined, QrcodeOutlined, DatabaseOutlined, ReadOutlined, MobileOutlined, FundViewOutlined, RadarChartOutlined, BarChartOutlined, WechatOutlined, BookOutlined, FileImageOutlined, EyeOutlined, UserOutlined, AlertOutlined } from '@ant-design/icons';
  4. import { ReactComponent as LaunchSvg } from '../public/svg/launch.svg'
  5. import { ReactComponent as AdLaunchSvg } from '../public/svg/adLaunch.svg'
  6. import { ReactComponent as MaterialSvg } from '../public/svg/material.svg'
  7. import { ReactComponent as GameSvg } from '../public/svg/game.svg'
  8. import { ReactComponent as OrdrtListSvg } from '../public/svg/ordrtList.svg'
  9. import { ReactComponent as PosSvg } from '../public/svg/pos.svg'
  10. import { ReactComponent as PayInstallSvg } from '../public/svg/payInstall.svg'
  11. import { ReactComponent as UseSvg } from '../public/svg/use.svg'
  12. import { ReactComponent as PayBoxSvg } from '../public/svg/payBox.svg'
  13. import { ReactComponent as ExtensionSvg } from '../public/svg/extension.svg'
  14. import { ReactComponent as ExtensionListSvg } from '../public/svg/extensionList.svg'
  15. import { ReactComponent as MediaSvg } from '../public/svg/media.svg'
  16. import { ReactComponent as PositionSvg } from '../public/svg/position.svg'
  17. import { ReactComponent as GameListSvg } from '../public/svg/gameList.svg'
  18. import { ReactComponent as RoleListSvg } from '../public/svg/roleList.svg'
  19. import { ReactComponent as RealNameSvg } from '../public/svg/realName.svg'
  20. import { ReactComponent as CorpWechatSvg } from '../public/svg/corpWechat.svg'
  21. import { ReactComponent as MsgSvg } from '../public/svg/msg.svg'
  22. import { ReactComponent as WeComSvg } from '../public/svg/weCom.svg'
  23. import { ReactComponent as BusinessPlanSvg } from '../public/svg/businessPlan.svg'
  24. import { ReactComponent as ImageSvg } from '../public/svg/image.svg'
  25. import { ReactComponent as BooKSvg } from '../public/svg/book.svg'
  26. import { ReactComponent as PaSvg } from '../public/svg/pa.svg'
  27. import { ReactComponent as GroupChatSendSvg } from '../public/svg/groupChatSend.svg'
  28. import { ReactComponent as MomentsSvg } from '../public/svg/moments.svg'
  29. import Avatar from './AvatarDropdown';
  30. import styles from './index.less'
  31. import { useLocation, useNavigate } from 'react-router-dom';
  32. import globaStore from '../store';
  33. import { getItem } from '@/utils/utils';
  34. import zhCN from 'antd/locale/zh_CN';
  35. import moment from 'dayjs';
  36. import 'dayjs/locale/zh-cn';
  37. import useNewToken from '@/Hook/useNewToken';
  38. import React from 'react';
  39. import { useInterval, useLocalStorageState, useUpdateEffect } from 'ahooks';
  40. import versions from '@/utils/versions';
  41. moment.locale('zh-cn');
  42. const { Header, Content, Sider } = Layout;
  43. type Props = {
  44. data: any,//路由数据
  45. name: string,//当前路由名称对应data的key
  46. children: any,//渲染的数据
  47. }
  48. // =====================枚举转译服务端菜单icon=====================
  49. const IconMap = {
  50. desktop: <DesktopOutlined />,
  51. message: <MessageOutlined />,
  52. send: <SendOutlined />,
  53. team: <TeamOutlined />,
  54. database: <DatabaseOutlined />,
  55. qrcode: <QrcodeOutlined />,
  56. read: <ReadOutlined />,
  57. mobile: <MobileOutlined />,
  58. fundView: <FundViewOutlined />,
  59. radarChart: <RadarChartOutlined />,
  60. barChart: <BarChartOutlined />,
  61. wechat: <WechatOutlined />,
  62. // book: <BookOutlined />,
  63. peoples: <UserOutlined />,
  64. 'file-image': <FileImageOutlined />,
  65. pay: <AlipayCircleOutlined />,
  66. cloud: <CloudOutlined />,
  67. link: <PaperClipOutlined />,
  68. dashboard: <HomeOutlined />,
  69. launch: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><LaunchSvg /></span>,
  70. adLaunch: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><AdLaunchSvg /></span>,
  71. material: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><MaterialSvg /></span>,
  72. game: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><GameSvg /></span>,
  73. order: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><OrdrtListSvg /></span>,
  74. pos: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><PosSvg /></span>,
  75. payInstall: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><PayInstallSvg /></span>,
  76. use: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><UseSvg /></span>,
  77. payBox: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><PayBoxSvg /></span>,
  78. extension: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><ExtensionSvg /></span>,
  79. extensionList: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><ExtensionListSvg /></span>,
  80. media: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><MediaSvg /></span>,
  81. position: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><PositionSvg /></span>,
  82. gameList: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><GameListSvg /></span>,
  83. roleList: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><RoleListSvg /></span>,
  84. realName: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><RealNameSvg /></span>,
  85. corpWechat: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><CorpWechatSvg /></span>,
  86. msg: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><MsgSvg /></span>,
  87. weCom: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><WeComSvg /></span>,
  88. businessPlan: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><BusinessPlanSvg /></span>,
  89. image: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><ImageSvg /></span>,
  90. book: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><BooKSvg /></span>,
  91. pa: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><PaSvg /></span>,
  92. groupChatSend: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><GroupChatSendSvg /></span>,
  93. moments: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><MomentsSvg /></span>,
  94. interdiction: <StopOutlined />,
  95. eye: <EyeOutlined />,
  96. alert: <AlertOutlined />,
  97. log: <ContainerOutlined />
  98. };
  99. export const DispatchContext = React.createContext<{ config: any } | null>(null);
  100. /**
  101. * 菜单路由
  102. * @param data 路由数据 clientele
  103. * @param name 当前路由名称对应data的key
  104. * */
  105. function MainLayout(props: Props) {
  106. let { data, name } = props
  107. const [topKey] = useState([name])
  108. const [letKey, setLetKey] = useState([])
  109. const [topMenu, setTopMenu] = useState<any>([])
  110. const [leftMenu, setLeftMenu] = useState<any>([])
  111. const [jump, setJump] = useLocalStorageState<string>('JUMP');
  112. const { themeConfig } = useNewToken();
  113. const [config] = useState(themeConfig);
  114. const navigate = useNavigate();//跳转
  115. // =====================顶部菜单点击事件=====================
  116. const getParentKeys = (path) => {
  117. const keys = [];
  118. path.split('/').reduce((prev, curr) => {
  119. const key = `${prev}/${curr}`.replace('//', '/');
  120. if (key !== '/') keys.push(key);
  121. return key;
  122. });
  123. keys.shift()
  124. return keys;
  125. };
  126. const location1 = useLocation();
  127. const [openKeys, setOpenKeys] = useState(getParentKeys(location1.pathname));
  128. const handleTopKey = useCallback((menu: { pData: any; key: string; }) => {
  129. let topOpen = localStorage.getItem('topOpen')
  130. data = menu?.pData || data
  131. sessionStorage.setItem('name', menu.key)
  132. if (topOpen == '1') {//打开新页面
  133. window.open(window.location.origin + data[menu.key][0].path)
  134. } else {//当前页面
  135. if (name !== menu.key) {
  136. window.location.href = window.location.origin + data[menu.key][0].path
  137. let oldPath = sessionStorage.getItem('oldPath')
  138. if (!oldPath || oldPath?.split('/')?.length <= 2 || oldPath?.split('/')[1] !== menu.key) {//不存在路径或者路径的长度小于2或者主路径对不上
  139. sessionStorage.setItem('oldPath', data[menu.key][0].path)
  140. }
  141. }
  142. }
  143. }, [data])
  144. //=====================左侧菜单点击事件=====================
  145. const handleLetKey = useCallback((menu: { domEvent: { target: { innerText: string; }; }; key: string; keyPath: SetStateAction<string[]>; }) => {
  146. handleTitle(menu?.domEvent?.target?.innerText)//设置title
  147. setLetKey([menu.key])
  148. setOpenKeys(menu.keyPath)//在这可以关闭上次展开的菜单
  149. let path = menu.key
  150. if (path) {
  151. navigate(path)
  152. sessionStorage.setItem('oldPath', path)
  153. }
  154. }, [data])
  155. // =====================首次默认跳转第一个菜单=====================
  156. useEffect(() => {
  157. let clickName = sessionStorage.getItem('name')
  158. if (clickName === 'imChat') {
  159. clickName = ''
  160. sessionStorage.removeItem('name')
  161. }
  162. let oldPath = sessionStorage.getItem('oldPath')
  163. let thatName = name
  164. // 当前菜单刷新
  165. if (jump) {
  166. setTimeout(() => {
  167. setLetKey([jump])
  168. sessionStorage.setItem('oldPath', jump)
  169. setJump()
  170. }, 200)
  171. } else {
  172. if (clickName && data[clickName]) {
  173. if (thatName !== clickName) {//更换一级菜单
  174. handleTopKey({
  175. key: clickName,
  176. pData: data
  177. })
  178. }
  179. }
  180. if (data[name]) {
  181. let item = data[thatName][0].children[0]
  182. if (!data[thatName][0].children[0]) {
  183. message.error('没有下级菜单,请联系管理员,确认是否开通了系统及菜单')
  184. return
  185. }
  186. let path = ''
  187. if (oldPath && oldPath?.split('/').length > 2) {//刷新页面时
  188. path = oldPath
  189. } else if (item?.children?.length > 0) {//假如有子菜单
  190. path = item?.children[0]?.path
  191. } else {//没有子菜单
  192. path = item?.path
  193. }
  194. navigate(path)//跳转到指定页面
  195. setLetKey([path])//指定页面名称选中样式
  196. }
  197. }
  198. if (data) {
  199. // 顶部菜单
  200. let TopMenu = Object.values(data).sort((a: any, b: any) => { return a[0]?.orderNum - b[0]?.orderNum }).map((r: any) => {
  201. return getItem(r[0]?.title, r[0]?.belongPlatform, IconMap[r[0]?.icon as keyof typeof IconMap])
  202. })
  203. if (!sessionStorage.getItem('name') && TopMenu?.length > 0) {
  204. sessionStorage.setItem('name', TopMenu[0]?.key as string)
  205. }
  206. setTopMenu(TopMenu)
  207. }
  208. }, [data])
  209. useUpdateEffect(() => {
  210. const newKey = location.hash.replace('#', '').split('?')?.[0]
  211. if (newKey !== letKey?.[0]) {
  212. // sessionStorage.setItem('oldPath', newKey);
  213. setLetKey([newKey])
  214. }
  215. }, [location.href])
  216. // ================后期items=================
  217. useEffect(() => {
  218. if (topKey && data[topKey[0]]) {
  219. const LeftMenu = data[topKey[0]][0].children.map((item: any, i: number) => {
  220. if (item.children?.length > 0) {
  221. return getItem(item?.title, item?.path, IconMap[item?.icon as keyof typeof IconMap], item.children)
  222. }
  223. return getItem(item?.title, item?.path, IconMap[item?.icon as keyof typeof IconMap])
  224. })
  225. setLeftMenu(() => LeftMenu)
  226. }
  227. }, [topKey, data])
  228. const handleTitle = useCallback((str: string) => {
  229. if (str) {
  230. let title = document.head.getElementsByTagName('title')[0]
  231. let text = '趣程企微管理后台' + '-' + str
  232. title.innerText = text
  233. }
  234. }, [])
  235. // 版本
  236. useEffect(() => {
  237. versions()
  238. }, [])
  239. useInterval(versions, 1000 * 60);
  240. return <ConfigProvider
  241. /**语言*/
  242. locale={zhCN}
  243. /**尺寸*/
  244. componentSize='middle'
  245. theme={{
  246. ...config.default, token: {
  247. ...config.default.token,
  248. },
  249. algorithm: [...config.default.algorithm, ...config.algorithm]
  250. }}
  251. >
  252. <App>
  253. <div style={{ background: `url(${config?.myBgUrl || (config.bgUrl as any)[config.acPrimary]}) no-repeat 0% 0% /cover` }}>
  254. <Layout>
  255. {/* 顶部 */}
  256. <Header className={styles.header} style={{ borderBottom: '1px solid rgba(5, 5, 5, 0.06)' }}>
  257. {/* logo */}
  258. <div className={styles.logo} style={{ width: (globaStore.data.isMobile || globaStore.data.collapsed) ? '48px' : '205px' }}>
  259. <Space size={16} align='center'>
  260. <div style={{ display: 'flex' }}> <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 200 200"><defs><linearGradient id="linearGradient-1" x1="62.102%" x2="108.197%" y1="0%" y2="37.864%"><stop offset="0%" stopColor="#4285EB"></stop><stop offset="100%" stopColor="#2EC7FF"></stop></linearGradient><linearGradient id="linearGradient-2" x1="69.644%" x2="54.043%" y1="0%" y2="108.457%"><stop offset="0%" stopColor="#29CDFF"></stop><stop offset="37.86%" stopColor="#148EFF"></stop><stop offset="100%" stopColor="#0A60FF"></stop></linearGradient><linearGradient id="linearGradient-3" x1="69.691%" x2="16.723%" y1="-12.974%" y2="117.391%"><stop offset="0%" stopColor="#FA816E"></stop><stop offset="41.473%" stopColor="#F74A5C"></stop><stop offset="100%" stopColor="#F51D2C"></stop></linearGradient><linearGradient id="linearGradient-4" x1="68.128%" x2="30.44%" y1="-35.691%" y2="114.943%"><stop offset="0%" stopColor="#FA8E7D"></stop><stop offset="51.264%" stopColor="#F74A5C"></stop><stop offset="100%" stopColor="#F51D2C"></stop></linearGradient></defs><g fill="none" fillRule="evenodd" stroke="none" strokeWidth="1"><g transform="translate(-20 -20)"><g transform="translate(20 20)"><g><g fillRule="nonzero"><g><path fill="url(#linearGradient-1)" d="M91.588 4.177L4.18 91.513a11.981 11.981 0 000 16.974l87.408 87.336a12.005 12.005 0 0016.989 0l36.648-36.618c4.209-4.205 4.209-11.023 0-15.228-4.208-4.205-11.031-4.205-15.24 0l-27.783 27.76c-1.17 1.169-2.945 1.169-4.114 0l-69.802-69.744c-1.17-1.169-1.17-2.942 0-4.11l69.802-69.745c1.17-1.169 2.944-1.169 4.114 0l27.783 27.76c4.209 4.205 11.032 4.205 15.24 0 4.209-4.205 4.209-11.022 0-15.227L108.581 4.056c-4.719-4.594-12.312-4.557-16.993.12z"></path><path fill="url(#linearGradient-2)" d="M91.588 4.177L4.18 91.513a11.981 11.981 0 000 16.974l87.408 87.336a12.005 12.005 0 0016.989 0l36.648-36.618c4.209-4.205 4.209-11.023 0-15.228-4.208-4.205-11.031-4.205-15.24 0l-27.783 27.76c-1.17 1.169-2.945 1.169-4.114 0l-69.802-69.744c-1.17-1.169-1.17-2.942 0-4.11l69.802-69.745c2.912-2.51 7.664-7.596 14.642-8.786 5.186-.883 10.855 1.062 17.009 5.837L108.58 4.056c-4.719-4.594-12.312-4.557-16.993.12z"></path></g><path fill="url(#linearGradient-3)" d="M153.686 135.855c4.208 4.205 11.031 4.205 15.24 0l27.034-27.012c4.7-4.696 4.7-12.28 0-16.974l-27.27-27.15c-4.218-4.2-11.043-4.195-15.254.013-4.209 4.205-4.209 11.022 0 15.227l18.418 18.403c1.17 1.169 1.17 2.943 0 4.111l-18.168 18.154c-4.209 4.205-4.209 11.023 0 15.228z"></path></g><ellipse cx="100.519" cy="100.437" fill="url(#linearGradient-4)" rx="23.6" ry="23.581"></ellipse></g></g></g></g></svg></div>
  261. {(!globaStore.data.isMobile && !globaStore.data.collapsed) && <h2 style={{ color: 'white', margin: 0 }}>企微运营系统</h2>}
  262. </Space>
  263. </div>
  264. <Menu items={topMenu} mode="horizontal" onClick={(menu: any) => { handleTopKey(menu) }} selectedKeys={topKey} style={{ width: '55%', borderBottom: 0, background: 'transparent' }} />
  265. {/* 右侧操作 */}
  266. <div className={styles.action}>
  267. <Space className={`${styles.right} ${styles.dark}`} >
  268. <Avatar config={config} />
  269. </Space>
  270. </div>
  271. </Header>
  272. <Layout>
  273. {/* 左侧菜单 */}
  274. <Sider
  275. width={230}
  276. className={`site-layout-background ${styles.sider}`}
  277. breakpoint="lg"
  278. collapsedWidth={globaStore.data.isMobile ? '0' : '80'}
  279. collapsible
  280. id={styles.sider}
  281. collapsed={globaStore.data.collapsed}
  282. onCollapse={(c) => {
  283. globaStore.menuCollapsed(c)
  284. }}
  285. trigger={!globaStore.data.isMobile && null}
  286. >
  287. <Menu
  288. mode="inline"
  289. style={{ height: '100%' }}
  290. openKeys={openKeys}
  291. onClick={(menu: any) => { handleLetKey(menu) }}
  292. onOpenChange={setOpenKeys}
  293. selectedKeys={letKey}
  294. items={leftMenu}
  295. theme="dark"
  296. />
  297. </Sider>
  298. {/* 内容 */}
  299. <Layout style={{ position: 'relative', background: 'transparent' }} >
  300. <Watermark content={sessionStorage.getItem('userName') || ''} gap={[100, 100]} font={{ color: 'rgba(255, 93, 93, 0.1)', fontSize: 20 }}>
  301. <Content
  302. className={`site-layout-background ${styles.section}`}
  303. style={{
  304. padding: '20px 0 0 16px',
  305. boxSizing: 'border-box',
  306. margin: 0,
  307. minHeight: 280,
  308. backgroundColor: '#f0f2f5'
  309. }}
  310. >
  311. <div style={{ marginRight: 16, paddingBottom: 10, position: 'relative' }}>{props.children}</div>
  312. </Content>
  313. </Watermark>
  314. </Layout>
  315. </Layout>
  316. </Layout>
  317. </div>
  318. </App>
  319. </ConfigProvider>
  320. }
  321. export default MainLayout