@@ -4,37 +4,33 @@ import { lazy, useEffect, useState } from 'react'; | |||||
import GloablLoading from '@/components/global-loading'; | import GloablLoading from '@/components/global-loading'; | ||||
import Slide from './slide'; | import Slide from './slide'; | ||||
import Header from './header'; | import Header from './header'; | ||||
// import userService from '@/service'; | |||||
// import { useRequest } from '@/hooks/use-request'; | |||||
import { useUserStore } from '@/store/global/user'; | import { useUserStore } from '@/store/global/user'; | ||||
import { useMenusStore } from "@/store/global/menu"; | |||||
import { Menu } from '@/models'; | import { Menu } from '@/models'; | ||||
// import { components } from '@/config/routes'; | |||||
import { components } from '@/router/config'; | |||||
import { replaceRoutes, router } from '@/router/router'; | import { replaceRoutes, router } from '@/router/router'; | ||||
import Result404 from './404'; | import Result404 from './404'; | ||||
import './index.css' | import './index.css' | ||||
// import { MenuType } from '@/pages/menu/interface'; | |||||
import TabsLayout from './tabs-layout'; | import TabsLayout from './tabs-layout'; | ||||
import Content from './content'; | import Content from './content'; | ||||
import homeService from '@/request/service/home'; | |||||
import { useRequest } from "@/hooks/use-request"; | |||||
import menuData from './menu.json' | |||||
const BasicLayout: React.FC = () => { | const BasicLayout: React.FC = () => { | ||||
const [loading, setLoading] = useState(true); | const [loading, setLoading] = useState(true); | ||||
const { refreshToken, lang, token } = useGlobalStore(); | |||||
const { setCurrentUser, currentUser } = useUserStore(); | |||||
const { lang } = useGlobalStore(); | |||||
const { currentUser } = useUserStore(); | |||||
const { setMenus } = useMenusStore(); | |||||
const navigate = useNavigate(); | const navigate = useNavigate(); | ||||
const location = useLocation(); | const location = useLocation(); | ||||
// const { setLatestMessage } = useMessageStore(); | // const { setLatestMessage } = useMessageStore(); | ||||
// const { | |||||
// data: currentUserDetail, | |||||
// run: getCurrentUserDetail, | |||||
// } = useRequest( | |||||
// userService.getCurrentUserDetail, | |||||
// { manual: true } | |||||
// ); | |||||
const { runAsync: getAnnouncement } = useRequest(homeService.getAnnouncement, { manual: true }); | |||||
const formatMenus = ( | const formatMenus = ( | ||||
menus: Menu[], | menus: Menu[], | ||||
@@ -62,71 +58,67 @@ const BasicLayout: React.FC = () => { | |||||
}); | }); | ||||
} | } | ||||
// useEffect(() => { | |||||
// if (!refreshToken) { | |||||
// navigate('/user/login'); | |||||
// return; | |||||
// } | |||||
// getCurrentUserDetail(); | |||||
// }, [refreshToken, getCurrentUserDetail, navigate]); | |||||
// useEffect(() => { | |||||
// if (!currentUserDetail) return; | |||||
// const { menus = [] } = currentUserDetail; | |||||
// const menuGroup = menus.reduce<Record<string, Menu[]>>((prev, menu) => { | |||||
// if (!menu.parentId) { | |||||
// return prev; | |||||
// } | |||||
// if (!prev[menu.parentId]) { | |||||
// prev[menu.parentId] = []; | |||||
// } | |||||
// prev[menu.parentId].push(menu); | |||||
// return prev; | |||||
// }, {}); | |||||
// const routes: Menu[] = []; | |||||
// currentUserDetail.menus = formatMenus(menus.filter(o => !o.parentId), menuGroup, routes); | |||||
// currentUserDetail.authList = menus | |||||
// .filter(menu => menu.type === MenuType.BUTTON && menu.authCode) | |||||
// .map(menu => menu.authCode!); | |||||
// console.log(components, 'components'); | |||||
// replaceRoutes('*', [ | |||||
// ...routes.map(menu => ({ | |||||
// path: `/*${menu.path}`, | |||||
// Component: menu.filePath ? lazy(components[menu.filePath]) : null, | |||||
// id: `/*${menu.path}`, | |||||
// handle: { | |||||
// parentPaths: menu.parentPaths, | |||||
// path: menu.path, | |||||
// name: menu.name, | |||||
// icon: menu.icon, | |||||
// }, | |||||
// })), { | |||||
// id: '*', | |||||
// path: '*', | |||||
// Component: Result404, | |||||
// handle: { | |||||
// path: '404', | |||||
// name: '404', | |||||
// }, | |||||
// } | |||||
// ]); | |||||
// setCurrentUser(currentUserDetail); | |||||
// setLoading(false); | |||||
// // replace一下当前路由,为了触发路由匹配 | |||||
// router.navigate(`${location.pathname}${location.search}`, { replace: true }); | |||||
// }, [currentUserDetail, setCurrentUser]); | |||||
useEffect(() => { | |||||
const menus:Array<Menu> = menuData as Array<Menu> ; | |||||
// { | |||||
// "id": 90121, | |||||
// "parentid": 901, | |||||
// "homeid": 9, | |||||
// "menuName": "国家管理", | |||||
// "parentMenuName": "基础配置", | |||||
// "pageUrl": "/modules/config/country_list.html", | |||||
// "sort": 13, | |||||
// "level": 3 | |||||
// } | |||||
console.log(menus); | |||||
const menuGroup = menus.reduce<Record<string, Menu[]>>((prev, menu) => { | |||||
if (!menu.parentId) { | |||||
return prev; | |||||
} | |||||
if (!prev[menu.parentId]) { | |||||
prev[menu.parentId] = []; | |||||
} | |||||
prev[menu.parentId].push(menu); | |||||
return prev; | |||||
}, {}); | |||||
const routes: Menu[] = []; | |||||
const formatedMenus = formatMenus(menus.filter(o => !o.parentId), menuGroup, routes); | |||||
setMenus(formatedMenus); | |||||
console.log(components, 'components'); | |||||
replaceRoutes('*', [ | |||||
...routes.map(menu => ({ | |||||
path: `/*${menu.path}`, | |||||
Component: menu.filePath ? lazy(components[menu.filePath]) : null, | |||||
id: `/*${menu.path}`, | |||||
handle: { | |||||
parentPaths: menu.parentPaths, | |||||
path: menu.path, | |||||
name: menu.name, | |||||
icon: menu.icon, | |||||
}, | |||||
})), { | |||||
id: '*', | |||||
path: '*', | |||||
Component: Result404, | |||||
handle: { | |||||
path: '404', | |||||
name: '404', | |||||
}, | |||||
} | |||||
]); | |||||
setLoading(false); | |||||
// replace一下当前路由,为了触发路由匹配 | |||||
router.navigate(`${location.pathname}${location.search}`, { replace: true }); | |||||
}, [ currentUser ]); | |||||
useEffect(() => { | useEffect(() => { | ||||
function storageChange(e: StorageEvent) { | function storageChange(e: StorageEvent) { | ||||
@@ -142,10 +134,6 @@ const BasicLayout: React.FC = () => { | |||||
} | } | ||||
}, []); | }, []); | ||||
setTimeout(()=>{ | |||||
setLoading(false); | |||||
}, 3000) | |||||
if (loading) { | if (loading) { | ||||
return ( | return ( | ||||
<GloablLoading /> | <GloablLoading /> | ||||
@@ -0,0 +1,14 @@ | |||||
[ | |||||
{ | |||||
"id": "22582728306196480", | |||||
"parentId": "", | |||||
"name": "首页", | |||||
"icon": "HomeOutlined", | |||||
"type":1, | |||||
"route": "/dashboard", | |||||
"filePath": "/dashboard/index.tsx", | |||||
"orderNumber": 0, | |||||
"url":"", | |||||
"show":true | |||||
} | |||||
] |
@@ -1,11 +1,11 @@ | |||||
import React, { useCallback, useEffect, useState } from 'react'; | |||||
import React, { useCallback, useEffect, useState, useMemo } from 'react'; | |||||
import { Menu } from 'antd'; | import { Menu } from 'antd'; | ||||
import type { ItemType } from 'antd/es/menu/hooks/useItems'; | import type { ItemType } from 'antd/es/menu/hooks/useItems'; | ||||
import { Link, useMatches } from 'react-router-dom'; | import { Link, useMatches } from 'react-router-dom'; | ||||
import { useGlobalStore } from '@/store/global'; | import { useGlobalStore } from '@/store/global'; | ||||
import { antdIcons } from '@/assets/antd-icons'; | import { antdIcons } from '@/assets/antd-icons'; | ||||
import { Menu as MenuType } from '@/models'; | import { Menu as MenuType } from '@/models'; | ||||
import { useMenusStore } from '@/store/global/menu'; | |||||
import { | import { | ||||
AccountBookOutlined, | AccountBookOutlined, | ||||
@@ -240,6 +240,7 @@ const items: MenuItem[] = [ | |||||
]), | ]), | ||||
]; | ]; | ||||
const SlideMenu = () => { | const SlideMenu = () => { | ||||
const matches = useMatches(); | const matches = useMatches(); | ||||
@@ -294,13 +295,22 @@ const SlideMenu = () => { | |||||
}) | }) | ||||
}, []); | }, []); | ||||
const { menus } = useMenusStore(); | |||||
console.log(menus) | |||||
debugger | |||||
const menuData = useMemo(() => { | |||||
return treeMenuData(menus?.filter(menu => menu.show) || []); | |||||
}, [menus]); | |||||
return ( | return ( | ||||
<Menu | <Menu | ||||
className='bg-primary color-transition' | className='bg-primary color-transition' | ||||
mode="inline" | mode="inline" | ||||
selectedKeys={selectKeys} | selectedKeys={selectKeys} | ||||
style={{ height: '100%', borderRight: 0 }} | style={{ height: '100%', borderRight: 0 }} | ||||
items={items} | |||||
items={menuData} | |||||
inlineCollapsed={collapsed} | inlineCollapsed={collapsed} | ||||
openKeys={openKeys} | openKeys={openKeys} | ||||
onOpenChange={setOpenKeys} | onOpenChange={setOpenKeys} | ||||
@@ -72,7 +72,6 @@ export interface LoginRespDTO { | |||||
pop: Array<PopMenu>; | pop: Array<PopMenu>; | ||||
} | } | ||||
export interface Menu { | export interface Menu { | ||||
id: string; | id: string; | ||||
parentId?: string; | parentId?: string; | ||||
@@ -88,7 +87,6 @@ export interface Menu { | |||||
path: string; | path: string; | ||||
Component?: any; | Component?: any; | ||||
parentPaths?: string[]; | parentPaths?: string[]; | ||||
authCode?: string; | |||||
} | } | ||||
export interface User { | export interface User { | ||||
@@ -0,0 +1,41 @@ | |||||
import { useState, useEffect } from 'react'; | |||||
import { Column } from '@ant-design/plots'; | |||||
import { useGlobalStore } from '@/store/global'; | |||||
import columnDarkTheme from './theme/dark-column-theme.json' | |||||
import columnLightTheme from './theme/light-column-theme.json' | |||||
const DemoColumn = () => { | |||||
const [data, setData] = useState([]); | |||||
const { darkMode } = useGlobalStore(); | |||||
useEffect(() => { | |||||
asyncFetch(); | |||||
}, []); | |||||
const asyncFetch = () => { | |||||
fetch('https://gw.alipayobjects.com/os/antfincdn/8elHX%26irfq/stack-column-data.json') | |||||
.then((response) => response.json()) | |||||
.then((json) => setData(json)) | |||||
.catch((error) => { | |||||
console.log('fetch data failed', error); | |||||
}); | |||||
}; | |||||
const config: any = { | |||||
data, | |||||
isStack: true, | |||||
xField: 'year', | |||||
yField: 'value', | |||||
seriesField: 'type', | |||||
height: 480, | |||||
legend: { | |||||
position: 'bottom' | |||||
}, | |||||
}; | |||||
return <Column theme={darkMode ? columnDarkTheme : columnLightTheme} {...config} />; | |||||
}; | |||||
export default DemoColumn; |
@@ -0,0 +1,55 @@ | |||||
.bg-card { | |||||
@apply before: (content-[''] absolute w-[210px] h-[210px] top-[-125px] right-[-15px] rounded-[50%] opacity-[.5]); | |||||
} | |||||
.dark .bg-card::before { | |||||
background: linear-gradient(140.9deg, rgb(101, 31, 255) -14.02%, rgba(144, 202, 249, 0) 85.5%); | |||||
} | |||||
.bg-card { | |||||
@apply after: (content-[''] absolute w-[210px] h-[210px] top-[-85px] right-[-95px] rounded-[50%] dark:opacity-[.5]); | |||||
} | |||||
.dark .bg-card::after { | |||||
background: linear-gradient(140.9deg, rgb(101, 31, 255) -14.02%, rgba(144, 202, 249, 0) 85.5%); | |||||
} | |||||
.dark .bg-card.theme1::before { | |||||
background: linear-gradient(140.9deg, rgb(30, 136, 229) -14.02%, rgba(144, 202, 249, 0) 82.5%); | |||||
} | |||||
.dark .bg-card.theme1::after { | |||||
background: linear-gradient(140.9deg, rgb(30, 136, 229) -14.02%, rgba(144, 202, 249, 0) 82.5%); | |||||
} | |||||
.dark .bg-card.theme2::before { | |||||
background: linear-gradient(140.9deg, rgb(255, 193, 7) -14.02%, rgba(144, 202, 249, 0) 70.5%); | |||||
} | |||||
.dark .bg-card.theme2::after { | |||||
background: linear-gradient(140.9deg, rgb(255, 193, 7) -14.02%, rgba(144, 202, 249, 0) 70.5%); | |||||
} | |||||
.bg-card::before { | |||||
background: rgb(69, 39, 160); | |||||
} | |||||
.bg-card::after { | |||||
background: rgb(69, 39, 160); | |||||
} | |||||
.bg-card.theme1::before { | |||||
background: rgb(21, 101, 192); | |||||
} | |||||
.bg-card.theme1::after { | |||||
background: rgb(21, 101, 192); | |||||
} | |||||
.dark .bg-card.theme2::before { | |||||
background: linear-gradient(140.9deg, rgb(255, 193, 7) -14.02%, rgba(144, 202, 249, 0) 70.5%); | |||||
} | |||||
.dark .bg-card.theme2::after { | |||||
background: linear-gradient(140.9deg, rgb(255, 193, 7) -14.02%, rgba(144, 202, 249, 0) 70.5%); | |||||
} |
@@ -0,0 +1,249 @@ | |||||
import { t } from '@/utils/i18n'; | |||||
import { InfoCircleOutlined, CaretUpOutlined, DashOutlined, CaretDownOutlined } from '@ant-design/icons'; | |||||
import { Col, Divider, Dropdown, Row, Select, Tooltip } from 'antd'; | |||||
import DemoTinyArea from './tiny-area'; | |||||
import DemoTinyColumn from './tiny-column'; | |||||
import DemoColumn from './column'; | |||||
import DemoTinyLine from './tiny-line'; | |||||
import './index.css' | |||||
const Dashboard = () => { | |||||
return ( | |||||
<> | |||||
<Row gutter={[16, 16]}> | |||||
<Col lg={24} xl={8} className='w-[100%]'> | |||||
<div className='color-transition dark:bg-[rgb(33,41,70)] w-[100%] bg-[rgb(94,53,177)] overflow-hidden h-[241px] relative rounded-md bg-card p-[32px] box-border'> | |||||
<div className='absolute top-[24px] right-[24px] z-10'> | |||||
<Tooltip title={t("yAdJryjx" /* 指标说明 */)}> | |||||
<InfoCircleOutlined className='text-[rgb(179,157,219)] text-[20px]' /> | |||||
</Tooltip> | |||||
</div> | |||||
<div className="text-[rgba(229,224,216,0.45)] text-[16px]"> | |||||
{t("nKMAkrqJ" /* 总销售额 */)} | |||||
</div> | |||||
<div className="text-white text-2xl mt-[28px] text-[30px]"> | |||||
¥ 126,560 | |||||
</div> | |||||
<div className='mt-[50px] text-[rgba(229,224,216,0.85)] text-[16px] flex gap-[24px]'> | |||||
<div className='flex items-center'> | |||||
<span>{t("NpRFMJyD" /* 周同比 */)}</span> | |||||
<span className='ml-[12px]'>12%</span> | |||||
<CaretDownOutlined className='ml-[6px] text-red-500' /> | |||||
</div> | |||||
<div className='flex items-center'> | |||||
<span>{t("WOQnwYUS" /* 日同比 */)}</span> | |||||
<span className='ml-[12px]'>12%</span> | |||||
<CaretUpOutlined className='ml-[6px] text-green-500' /> | |||||
</div> | |||||
</div> | |||||
<Divider className='dark:bg-[rgb(189,200,240)] bg-[rgb(227,232,239)] opacity-[0.2] my-[16px]' /> | |||||
<div className='text-[rgba(229,224,216,0.85)] text-[16px]'> | |||||
<span> | |||||
{t("ZPCQOWAn" /* 日销售额 */)} | |||||
</span> | |||||
<span className='ml-[8px]'> | |||||
¥12,423 | |||||
</span> | |||||
</div> | |||||
</div> | |||||
</Col> | |||||
<Col lg={24} xl={8} className='w-[100%]'> | |||||
<div className='color-transition dark:bg-[rgb(33,41,70)] bg-[rgb(30,136,229)] theme1 overflow-hidden h-[241px] relative rounded-md bg-card p-[32px] box-border'> | |||||
<div className='absolute top-[24px] right-[24px] z-10'> | |||||
<Tooltip title={t("iLyPEqwQ" /* 指标说明 */)}> | |||||
<InfoCircleOutlined className='text-[rgb(179,157,219)] text-[20px]' /> | |||||
</Tooltip> | |||||
</div> | |||||
<div className="text-[rgba(229,224,216,0.45)] text-[16px]"> | |||||
{t("ftuxZMpL" /* 访问量 */)} | |||||
</div> | |||||
<div className="text-white text-2xl mt-[20px] text-[30px]"> | |||||
8,930 | |||||
</div> | |||||
<div className='mt-[20px] text-[rgba(229,224,216,0.85)] text-[16px] flex gap-[24px]'> | |||||
<DemoTinyLine /> | |||||
</div> | |||||
<Divider className='dark:bg-[rgb(189,200,240)] bg-[rgb(227,232,239)] opacity-[0.2] my-[16px]' /> | |||||
<div className='text-[rgba(229,224,216,0.85)] text-[16px]'> | |||||
<span> | |||||
{t("sehypRaO" /* 日访问量 */)} | |||||
</span> | |||||
<span className='ml-[8px]'> | |||||
9,431 | |||||
</span> | |||||
</div> | |||||
</div> | |||||
</Col> | |||||
<Col lg={24} xl={8} className='w-[100%]'> | |||||
<div className='color-transition dark:bg-[rgb(33,41,70)] bg-[rgb(94,53,177)] theme2 overflow-hidden h-[241px] relative rounded-md bg-card p-[32px] box-border'> | |||||
<div className='absolute top-[24px] right-[24px] z-10'> | |||||
<Tooltip title={t("sdOusITo" /* 指标说明 */)}> | |||||
<InfoCircleOutlined className='text-[rgb(179,157,219)] text-[20px]' /> | |||||
</Tooltip> | |||||
</div> | |||||
<div className="text-[rgba(229,224,216,0.45)] text-[16px]"> | |||||
{t("PIYkoguj" /* 支付笔数 */)} | |||||
</div> | |||||
<div className="text-white text-2xl mt-[20px] text-[30px]"> | |||||
8,943 | |||||
</div> | |||||
<div className='mt-[12px] text-[rgba(229,224,216,0.85)] text-[16px] flex gap-[24px]'> | |||||
<DemoTinyColumn /> | |||||
</div> | |||||
<Divider className='dark:bg-[rgb(189,200,240)] bg-[rgb(227,232,239)] opacity-[0.2] my-[16px]' /> | |||||
<div className='text-[rgba(229,224,216,0.85)] text-[16px]'> | |||||
<span> | |||||
{t("BUjwpMzX" /* 转化率 */)} | |||||
</span> | |||||
<span className='ml-[8px]'> | |||||
2,421 | |||||
</span> | |||||
</div> | |||||
</div> | |||||
</Col> | |||||
<Col className='w-[100%]' lg={24} xl={16} > | |||||
<div className='color-transition dark:bg-[rgb(33,41,70)] bg-white h-[600px] rounded-md p-[24px] relative'> | |||||
<div className='flex justify-between items-center'> | |||||
<div> | |||||
<div className='text-[rgb(132,146,196)]'>{t("fHpiDHYH" /* 总增长 */)}</div> | |||||
<div className='dark:text-white text-[rgb(18,25,38)] mt-[8px] text-[18px]'>¥12,423</div> | |||||
</div> | |||||
<Select | |||||
options={[ | |||||
{ | |||||
label: t("yLkZTWbn" /* 今日 */), | |||||
value: 'today' | |||||
}, | |||||
{ | |||||
label: t("QFqMuZiD" /* 本月 */), | |||||
value: 'mouth' | |||||
}, | |||||
{ | |||||
label: t("lGOcGyrv" /* 本年 */), | |||||
value: 'year' | |||||
}, | |||||
]} | |||||
defaultValue="today" | |||||
size="large" | |||||
dropdownMatchSelectWidth={false} | |||||
placement="bottomRight" | |||||
/> | |||||
</div> | |||||
<div className='mt-[50px] absolute bottom-[12px] w-[90%] box-border'> | |||||
<DemoColumn /> | |||||
</div> | |||||
</div> | |||||
</Col> | |||||
<Col className='w-[100%]' lg={24} xl={8}> | |||||
<div className='color-transition dark:bg-[rgb(33,41,70)] bg-white h-[600px] rounded-md p-[24px] relative'> | |||||
<div className='flex justify-between'> | |||||
<span className='dark:text-[rgb(215,220,236)] text-[18px] text-[rgb(18,25,38)]'>{t("yzUIyMhr" /* 门店销售额 */)}</span> | |||||
<Dropdown menu={{ | |||||
items: [ | |||||
{ | |||||
label: t("aSPCUBcK" /* 今日 */), | |||||
key: 'today' | |||||
}, | |||||
{ | |||||
label: t("EhTpnarX" /* 本月 */), | |||||
key: 'mouth' | |||||
}, | |||||
{ | |||||
label: t("AGGPEAdX" /* 本年 */), | |||||
key: 'year' | |||||
}, | |||||
] | |||||
}}> | |||||
<span className='text-[rgb(144,202,249)] cursor-pointer'> | |||||
<DashOutlined /> | |||||
</span> | |||||
</Dropdown> | |||||
</div> | |||||
<div className='dark:bg-[rgb(209,196,233)] bg-[rgb(237,231,246)] px-[16px] pt-[16px] mt-[24px] rounded-t-lg'> | |||||
<div className='flex justify-between items-center'> | |||||
<span className='text-[rgb(101,31,255)] text-[16px]'>{t("jTSvVuJx" /* 上海分店 */)}</span> | |||||
<span className='text-[rgb(66,66,66)]'>¥12,423</span> | |||||
</div> | |||||
<div className='mt-[10px] text-[rgb(66,66,66)]'> | |||||
{t("SwsawJhB" /* 20% 利润 */)} | |||||
</div> | |||||
</div> | |||||
<div className='dark:bg-[rgb(209,196,233)] bg-[rgb(237,231,246)] rounded-b-lg'> | |||||
<DemoTinyArea /> | |||||
</div> | |||||
<div className='py-[24px]'> | |||||
<div> | |||||
<div className='flex justify-between items-center'> | |||||
<span className='dark:text-[rgb(189,200,240)] text-[rgb(54,65,82)] font-medium'>{t("JYSgIJHD" /* 上海分店 */)}</span> | |||||
<div> | |||||
<span className='mr-[8px] dark:text-[rgb(189,200,240)] text-[rgb(54,65,82)] font-medium'>¥12,423</span> | |||||
<CaretUpOutlined className='text-[rgb(0,200,83)]' /> | |||||
</div> | |||||
</div> | |||||
<div className='text-[rgb(0,200,83)] text-[12px] mt-[6px]'> | |||||
{t("yELACPnu" /* 20% 利润 */)} | |||||
</div> | |||||
</div> | |||||
<Divider className='dark:bg-[rgb(189,200,240)] bg-[rgb(227,232,239)] dark:opacity-[0.2] my-[16px]' /> | |||||
<div> | |||||
<div className='flex justify-between items-center'> | |||||
<span className='dark:text-[rgb(189,200,240)] text-[rgb(54,65,82)]font-medium'>{t("WAiyAuwV" /* 合肥分店 */)}</span> | |||||
<div> | |||||
<span className='mr-[8px] dark:text-[rgb(189,200,240)] text-[rgb(54,65,82)] font-medium'>¥10,000</span> | |||||
<CaretUpOutlined className='text-[rgb(0,200,83)]' /> | |||||
</div> | |||||
</div> | |||||
<div className='text-[rgb(0,200,83)] text-[12px] mt-[6px]'> | |||||
{t("HpNzGyBz" /* 6% 利润 */)} | |||||
</div> | |||||
</div> | |||||
<Divider className='dark:bg-[rgb(189,200,240)] bg-[rgb(227,232,239)] dark:opacity-[0.2] my-[16px]' /> | |||||
<div> | |||||
<div className='flex justify-between items-center'> | |||||
<span className='dark:text-[rgb(189,200,240)] text-[rgb(54,65,82)] font-medium'>{t("nGvTAQld" /* 北京分店 */)}</span> | |||||
<div> | |||||
<span className='mr-[8px] dark:text-[rgb(189,200,240)] text-[rgb(54,65,82)] font-medium'>¥8,000</span> | |||||
<CaretDownOutlined className='text-[rgb(216,67,21)] ' /> | |||||
</div> | |||||
</div> | |||||
<div className='text-[rgb(216,67,21)] text-[12px] mt-[6px]'> | |||||
{t("EeunYupT" /* 8% 亏损 */)} | |||||
</div> | |||||
</div> | |||||
<Divider className='dark:bg-[rgb(189,200,240)] bg-[rgb(227,232,239)] dark:opacity-[0.2] my-[16px]' /> | |||||
<div> | |||||
<div className='flex justify-between items-center'> | |||||
<span className='dark:text-[rgb(189,200,240)] text-[rgb(54,65,82)] font-medium'>{t("usCBUdwp" /* 苏州分店 */)}</span> | |||||
<div> | |||||
<span className='mr-[8px] dark:text-[rgb(189,200,240)] text-[rgb(54,65,82)] font-medium'>¥9,423</span> | |||||
<CaretUpOutlined className='text-[rgb(0,200,83)]' /> | |||||
</div> | |||||
</div> | |||||
<div className='text-[rgb(0,200,83)] text-[12px] mt-[6px]'> | |||||
{t("TacOGPiP" /* 14% 利润 */)} | |||||
</div> | |||||
</div> | |||||
<Divider className='dark:bg-[rgb(189,200,240)] bg-[rgb(227,232,239)] dark:opacity-[0.2] my-[16px]' /> | |||||
<div> | |||||
<div className='flex justify-between items-center'> | |||||
<span className='dark:text-[rgb(189,200,240)] text-[rgb(54,65,82)] font-medium'>{t("Imkllizi" /* 南京分店 */)}</span> | |||||
<div> | |||||
<span className='mr-[8px] dark:text-[rgb(189,200,240)] text-[rgb(54,65,82)] font-medium'>¥7,423</span> | |||||
<CaretDownOutlined className='text-[rgb(216,67,21)]' /> | |||||
</div> | |||||
</div> | |||||
<div className='text-[rgb(0,200,83)] text-[12px] mt-[6px] text-[rgb(216,67,21)]'> | |||||
{t("MzCxBxLH" /* 6% 亏损 */)} | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</Col> | |||||
</Row> | |||||
</> | |||||
) | |||||
} | |||||
export default Dashboard; |
@@ -0,0 +1,84 @@ | |||||
{ | |||||
"background": "rgba(20, 20, 20, 0)", | |||||
"components": { | |||||
"axis": { | |||||
"common": { | |||||
"grid": { | |||||
"line": { | |||||
"type": "line", | |||||
"style": { | |||||
"stroke": "rgba(189, 200, 240, 0.125)", | |||||
"lineWidth": 1, | |||||
"lineDash": null | |||||
} | |||||
}, | |||||
"alignTick": true, | |||||
"animate": true | |||||
} | |||||
} | |||||
}, | |||||
"tooltip": { | |||||
"domStyles": { | |||||
"g2-tooltip": { | |||||
"position": "absolute", | |||||
"visibility": "hidden", | |||||
"zIndex": 8, | |||||
"transition": "left 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s, top 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s", | |||||
"backgroundColor": "#1f1f1f", | |||||
"opacity": 0.95, | |||||
"boxShadow": "0px 2px 4px rgba(0,0,0,.5)", | |||||
"borderRadius": "3px", | |||||
"color": "#A6A6A6", | |||||
"fontSize": "12px", | |||||
"fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"", | |||||
"lineHeight": "12px", | |||||
"padding": "0 12px 0 12px" | |||||
}, | |||||
"g2-tooltip-title": { | |||||
"marginBottom": "12px", | |||||
"marginTop": "12px" | |||||
}, | |||||
"g2-tooltip-list": { | |||||
"margin": 0, | |||||
"listStyleType": "none", | |||||
"padding": 0 | |||||
}, | |||||
"g2-tooltip-list-item": { | |||||
"listStyleType": "none", | |||||
"padding": 0, | |||||
"marginBottom": "12px", | |||||
"marginTop": "12px", | |||||
"marginLeft": 0, | |||||
"marginRight": 0 | |||||
}, | |||||
"g2-tooltip-marker": { | |||||
"width": "8px", | |||||
"height": "8px", | |||||
"borderRadius": "50%", | |||||
"display": "inline-block", | |||||
"marginRight": "8px" | |||||
}, | |||||
"g2-tooltip-value": { | |||||
"display": "inline-block", | |||||
"float": "right", | |||||
"marginLeft": "30px" | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
"styleSheet": { | |||||
"brandColor": "rgba(124, 77, 255, 0.85)", | |||||
"paletteQualitative10": [ | |||||
"rgba(124, 77, 255, 0.85)", | |||||
"rgba(144, 202, 249, 0.85)", | |||||
"#65789B", | |||||
"#F6BD16", | |||||
"#7262fd", | |||||
"#78D3F8", | |||||
"#9661BC", | |||||
"#F6903D", | |||||
"#008685", | |||||
"#F08BB4" | |||||
] | |||||
} | |||||
} |
@@ -0,0 +1,84 @@ | |||||
{ | |||||
"background": "rgba(20, 20, 20, 0)", | |||||
"components": { | |||||
"axis": { | |||||
"common": { | |||||
"grid": { | |||||
"line": { | |||||
"type": "line", | |||||
"style": { | |||||
"stroke": "rgb(227,232,239)", | |||||
"lineWidth": 1, | |||||
"lineDash": null | |||||
} | |||||
}, | |||||
"alignTick": true, | |||||
"animate": true | |||||
} | |||||
} | |||||
}, | |||||
"tooltip": { | |||||
"domStyles": { | |||||
"g2-tooltip": { | |||||
"position": "absolute", | |||||
"visibility": "hidden", | |||||
"zIndex": 8, | |||||
"transition": "left 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s, top 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s", | |||||
"backgroundColor": "#ffffff", | |||||
"opacity": 0.95, | |||||
"boxShadow": "rgb(174, 174, 174) 0px 0px 10px", | |||||
"borderRadius": "3px", | |||||
"color": "#A6A6A6", | |||||
"fontSize": "12px", | |||||
"fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"", | |||||
"lineHeight": "12px", | |||||
"padding": "0 12px 0 12px" | |||||
}, | |||||
"g2-tooltip-title": { | |||||
"marginBottom": "12px", | |||||
"marginTop": "12px" | |||||
}, | |||||
"g2-tooltip-list": { | |||||
"margin": 0, | |||||
"listStyleType": "none", | |||||
"padding": 0 | |||||
}, | |||||
"g2-tooltip-list-item": { | |||||
"listStyleType": "none", | |||||
"padding": 0, | |||||
"marginBottom": "12px", | |||||
"marginTop": "12px", | |||||
"marginLeft": 0, | |||||
"marginRight": 0 | |||||
}, | |||||
"g2-tooltip-marker": { | |||||
"width": "8px", | |||||
"height": "8px", | |||||
"borderRadius": "50%", | |||||
"display": "inline-block", | |||||
"marginRight": "8px" | |||||
}, | |||||
"g2-tooltip-value": { | |||||
"display": "inline-block", | |||||
"float": "right", | |||||
"marginLeft": "30px" | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
"styleSheet": { | |||||
"brandColor": "rgba(124, 77, 255, 0.85)", | |||||
"paletteQualitative10": [ | |||||
"rgba(124, 77, 255, 0.85)", | |||||
"rgba(144, 202, 249, 0.85)", | |||||
"#65789B", | |||||
"#F6BD16", | |||||
"#7262fd", | |||||
"#78D3F8", | |||||
"#9661BC", | |||||
"#F6903D", | |||||
"#008685", | |||||
"#F08BB4" | |||||
] | |||||
} | |||||
} |
@@ -0,0 +1,16 @@ | |||||
import { TinyArea } from '@ant-design/plots'; | |||||
const DemoTinyArea = () => { | |||||
const data = [ | |||||
0, 300, 438, 287, 309, 600, 900, 575, 563, 300, 200 | |||||
]; | |||||
const config = { | |||||
height: 95, | |||||
data, | |||||
smooth: true, | |||||
areaStyle: { fill: 'l(360) 1:rgba(98,0,234,0.65) 0.5:rgba(177,128,245,0.5) 0.5:rgba(177,128,245,0.5)' }, | |||||
}; | |||||
return <TinyArea {...config} renderer="svg" line={{ color: '#6200ea' }} appendPadding={[0, -24, 0, -16]} />; | |||||
}; | |||||
export default DemoTinyArea; |
@@ -0,0 +1,35 @@ | |||||
import { TinyColumn } from '@antv/g2plot'; | |||||
import { useLayoutEffect, useRef } from 'react'; | |||||
const DemoTinyColumn = () => { | |||||
const container = useRef(null); | |||||
useLayoutEffect(() => { | |||||
const data = [50, 40, 81, 400, 300, 219, 269]; | |||||
const tinyColumn = new TinyColumn(container.current!, { | |||||
height: 50, | |||||
autoFit: true, | |||||
data, | |||||
tooltip: { | |||||
customContent: function (x, data) { | |||||
return `NO.${x}: ${data[0]?.data?.y.toFixed(2)}`; | |||||
}, | |||||
}, | |||||
}); | |||||
tinyColumn.render(); | |||||
return () => { | |||||
tinyColumn.destroy(); | |||||
} | |||||
}, []); | |||||
return ( | |||||
<div ref={container} className='w-[100%]' /> | |||||
); | |||||
}; | |||||
export default DemoTinyColumn; |
@@ -0,0 +1,33 @@ | |||||
import { TinyLine } from '@antv/g2plot'; | |||||
import { useLayoutEffect, useRef } from 'react'; | |||||
const DemoTinyLine = () => { | |||||
const container = useRef(null); | |||||
useLayoutEffect(() => { | |||||
const data = [ | |||||
264, 417, 438, 887, 309, 397, 550, 575, 563, 430, 525, 592, 492, 467, 513, 546, 983, 340, 539, 243, 226, 192, | |||||
]; | |||||
const tinyLine = new TinyLine(container.current!, { | |||||
height: 50, | |||||
autoFit: true, | |||||
data, | |||||
smooth: true, | |||||
color: '#ffffff', | |||||
}); | |||||
tinyLine.render(); | |||||
return () => { | |||||
tinyLine.destroy(); | |||||
} | |||||
}, []); | |||||
return ( | |||||
<div ref={container} className='w-[100%]' /> | |||||
); | |||||
}; | |||||
export default DemoTinyLine; |
@@ -5,22 +5,33 @@ import { Button, Form, Input, Carousel } from 'antd'; | |||||
import { useNavigate } from 'react-router-dom'; | import { useNavigate } from 'react-router-dom'; | ||||
import { useRequest } from '@/hooks/use-request'; | import { useRequest } from '@/hooks/use-request'; | ||||
import { useUserStore } from '@/store/global/user'; | import { useUserStore } from '@/store/global/user'; | ||||
import { useGlobalStore } from '@/store/global'; | |||||
import loginService from '@/request/service/login'; | import loginService from '@/request/service/login'; | ||||
import userService from '@/request/service/user'; | |||||
import homeService from '@/request/service/home'; | |||||
import { LoginDTO } from '@/models'; | import { LoginDTO } from '@/models'; | ||||
import './index.css' | import './index.css' | ||||
const Login = () => { | const Login = () => { | ||||
const navigate = useNavigate(); | const navigate = useNavigate(); | ||||
const { runAsync: getUserInfo } = useRequest(userService.getUserInfo, { manual: true }); | |||||
const { runAsync: getAnnouncement } = useRequest(homeService.getAnnouncement, { manual: true }); | |||||
const { runAsync: login, loading } = useRequest(loginService.login, { manual: true }); | const { runAsync: login, loading } = useRequest(loginService.login, { manual: true }); | ||||
const { setCurrentUser } = useUserStore(); | const { setCurrentUser } = useUserStore(); | ||||
const { setCookie } = useGlobalStore(); | |||||
const onFinish = async (values: LoginDTO) => { | const onFinish = async (values: LoginDTO) => { | ||||
const [loginError, data] = await login(values); | const [loginError, data] = await login(values); | ||||
if (loginError) { | if (loginError) { | ||||
return; | return; | ||||
} | } | ||||
data.data.avatarUrl = 'https://test.vogocm.com:9010/eshop/eshop_img/2023/5/24/43853633d16749bfb291f81bebb73451_20230524150631A001.jpg' | |||||
data.data.avatarUrl = 'https://test.vogocm.com:9010/eshop/eshop_img/2023/5/24/43853633d16749bfb291f81bebb73451_20230524150631A001.jpg'; | |||||
setCookie(data.msg); | |||||
setCurrentUser(data.data); | setCurrentUser(data.data); | ||||
const [e, userInfo] = await getUserInfo(); | |||||
const [ _, announcementData ] = await getAnnouncement(); | |||||
console.log(announcementData) | |||||
console.log(userInfo) | |||||
navigate('/'); | navigate('/'); | ||||
}; | }; | ||||
@@ -5,10 +5,9 @@ import axios, { | |||||
CreateAxiosDefaults, | CreateAxiosDefaults, | ||||
InternalAxiosRequestConfig, | InternalAxiosRequestConfig, | ||||
} from 'axios'; | } from 'axios'; | ||||
import {useGlobalStore} from '@/store/global'; | |||||
import {antdUtils} from '@/utils/antd'; | import {antdUtils} from '@/utils/antd'; | ||||
const refreshTokenUrl = '/api/auth/refresh/token'; | |||||
// axios.defaults.withCredentials = true; | |||||
export type Response<T> = Promise<[boolean, T, AxiosResponse<T>]>; | export type Response<T> = Promise<[boolean, T, AxiosResponse<T>]>; | ||||
@@ -29,7 +28,6 @@ class Request { | |||||
private axiosInstance: AxiosInstance; | private axiosInstance: AxiosInstance; | ||||
private refreshTokenFlag = false; | |||||
private requestQueue: { | private requestQueue: { | ||||
resolve: any; | resolve: any; | ||||
config: any; | config: any; | ||||
@@ -46,11 +44,7 @@ class Request { | |||||
private async requestInterceptor( | private async requestInterceptor( | ||||
axiosConfig: InternalAxiosRequestConfig | axiosConfig: InternalAxiosRequestConfig | ||||
): Promise<any> { | ): Promise<any> { | ||||
if ([refreshTokenUrl].includes(axiosConfig.url || '')) { | |||||
return Promise.resolve(axiosConfig); | |||||
} | |||||
if (this.refreshTokenFlag || this.requestingCount >= this.limit) { | |||||
if (this.requestingCount >= this.limit) { | |||||
return new Promise((resolve) => { | return new Promise((resolve) => { | ||||
this.requestQueue.push({ | this.requestQueue.push({ | ||||
resolve, | resolve, | ||||
@@ -61,12 +55,6 @@ class Request { | |||||
} | } | ||||
this.requestingCount += 1; | this.requestingCount += 1; | ||||
const {token} = useGlobalStore.getState(); | |||||
if (token) { | |||||
axiosConfig.headers.Authorization = `Bearer ${token}`; | |||||
} | |||||
return Promise.resolve(axiosConfig); | return Promise.resolve(axiosConfig); | ||||
} | } | ||||
@@ -91,8 +79,7 @@ class Request { | |||||
resolve(await this.request(config)); | resolve(await this.request(config)); | ||||
} else if (type === 'reuqest') { | } else if (type === 'reuqest') { | ||||
this.requestingCount += 1; | this.requestingCount += 1; | ||||
const {token} = useGlobalStore.getState(); | |||||
config.headers.Authorization = `Bearer ${token}`; | |||||
//TODO: cookie | |||||
resolve(config); | resolve(config); | ||||
} | } | ||||
} | } | ||||
@@ -102,13 +89,10 @@ class Request { | |||||
private async responseSuccessInterceptor( | private async responseSuccessInterceptor( | ||||
response: AxiosResponse<any, any> | response: AxiosResponse<any, any> | ||||
): Promise<any> { | ): Promise<any> { | ||||
if (response.config.url !== refreshTokenUrl) { | |||||
this.requestingCount -= 1; | |||||
if (this.requestQueue.length) { | |||||
this.requestByQueue(); | |||||
} | |||||
this.requestingCount -= 1; | |||||
if (this.requestQueue.length) { | |||||
this.requestByQueue(); | |||||
} | } | ||||
return Promise.resolve([false, response.data, response]); | return Promise.resolve([false, response.data, response]); | ||||
} | } | ||||
@@ -119,9 +103,6 @@ class Request { | |||||
if (status === 401) { | if (status === 401) { | ||||
return new Promise((resolve) => { | return new Promise((resolve) => { | ||||
this.requestQueue.unshift({resolve, config, type: 'response'}); | this.requestQueue.unshift({resolve, config, type: 'response'}); | ||||
if (this.refreshTokenFlag) return; | |||||
this.refreshTokenFlag = true; | |||||
}); | }); | ||||
} else { | } else { | ||||
antdUtils.notification?.error({ | antdUtils.notification?.error({ | ||||
@@ -134,7 +115,6 @@ class Request { | |||||
private reset() { | private reset() { | ||||
this.requestQueue = []; | this.requestQueue = []; | ||||
this.refreshTokenFlag = false; | |||||
this.requestingCount = 0; | this.requestingCount = 0; | ||||
} | } | ||||
@@ -4,7 +4,7 @@ import { LoginDTO, LoginRespDTO } from '@/models' | |||||
const loginService = { | const loginService = { | ||||
// 登录 | // 登录 | ||||
login: (loginDTO: LoginDTO) => { | login: (loginDTO: LoginDTO) => { | ||||
return request.post<LoginRespDTO>('/api/login', loginDTO); | |||||
return request.post<LoginRespDTO>('/api/login', loginDTO, { withCredentials: false }); | |||||
}, | }, | ||||
logout: () => { | logout: () => { | ||||
@@ -1,5 +1,8 @@ | |||||
const userService = { | |||||
import request from '@/request'; | |||||
import { LoginRespDTO } from '@/models'; | |||||
export default { | |||||
getUserInfo: () => { | |||||
return request.get<LoginRespDTO>('/api/userinfo/getUserInfo'); | |||||
} | |||||
}; | }; | ||||
export default userService; |
@@ -0,0 +1,33 @@ | |||||
export const modules = import.meta.glob('../pages/**/index.tsx'); | |||||
export const componentPaths = Object.keys(modules).map((path: string) => path.replace('../pages', '')); | |||||
let manifest: any; | |||||
export const components = Object.keys(modules).reduce<Record<string, () => Promise<any>>>((prev, path: string) => { | |||||
const formatPath = path.replace('../pages', ''); | |||||
prev[formatPath] = async () => { | |||||
try { | |||||
// 这里其实就是动态加载js,如果报错了说明js资源不存在 | |||||
return await modules[path]() as any; | |||||
} catch { | |||||
// 如果manifest已经存在了,就不用再请求了 | |||||
if (manifest) { | |||||
try { | |||||
// 有可能manifest是过期的,所以可能还会加载失败 | |||||
return await import('/' + manifest[`src/pages${formatPath}`]?.file); | |||||
} catch { | |||||
// 如果失败,重新获取一下manifest.json,拿到最新的路径 | |||||
manifest = await (await fetch('/manifest.json')).json() as any; | |||||
return await import('/' + manifest[`src/pages${formatPath}`]?.file); | |||||
} | |||||
} else { | |||||
// 如果manifest.json为空,请求manifest.json,并根据最新的路径加载对应js | |||||
manifest = await (await fetch('/manifest.json')).json() as any; | |||||
return await import('/' + manifest[`src/pages${formatPath}`]?.file); | |||||
} | |||||
} | |||||
} | |||||
return prev; | |||||
}, {}); | |||||
@@ -2,6 +2,7 @@ import { RouteObject, RouterProvider, createBrowserRouter, Navigate } from 'reac | |||||
import Login from '@/pages/login'; | import Login from '@/pages/login'; | ||||
import BasicLayout from '@/layout'; | import BasicLayout from '@/layout'; | ||||
import Dashboard from '@/pages/dashboard'; | |||||
import { App } from 'antd'; | import { App } from 'antd'; | ||||
import { useEffect } from 'react'; | import { useEffect } from 'react'; | ||||
import { antdUtils } from '@/utils/antd'; | import { antdUtils } from '@/utils/antd'; | ||||
@@ -18,6 +19,7 @@ export const router = createBrowserRouter( | |||||
element: ( | element: ( | ||||
<Navigate to="/dashboard" /> | <Navigate to="/dashboard" /> | ||||
), | ), | ||||
children: [], | |||||
}, | }, | ||||
{ | { | ||||
path: '*', | path: '*', | ||||
@@ -62,7 +64,6 @@ export const replaceRoutes = (parentPath: string, routes: RouteObject[]) => { | |||||
router.routes.push(...routes as any); | router.routes.push(...routes as any); | ||||
return; | return; | ||||
} | } | ||||
const curNode = findNodeByPath(router.routes, parentPath); | const curNode = findNodeByPath(router.routes, parentPath); | ||||
if (curNode) { | if (curNode) { | ||||
@@ -5,12 +5,14 @@ interface State { | |||||
darkMode: boolean; | darkMode: boolean; | ||||
collapsed: boolean; | collapsed: boolean; | ||||
lang: string; | lang: string; | ||||
cookie: string; | |||||
} | } | ||||
interface Action { | interface Action { | ||||
setDarkMode: (darkMode: State['darkMode']) => void; | setDarkMode: (darkMode: State['darkMode']) => void; | ||||
setCollapsed: (collapsed: State['collapsed']) => void; | setCollapsed: (collapsed: State['collapsed']) => void; | ||||
setLang: (lang: State['lang']) => void; | setLang: (lang: State['lang']) => void; | ||||
setCookie: (cookie: State['cookie']) => void; | |||||
} | } | ||||
export const useGlobalStore = create<State & Action>()( | export const useGlobalStore = create<State & Action>()( | ||||
@@ -20,8 +22,7 @@ export const useGlobalStore = create<State & Action>()( | |||||
darkMode: false, | darkMode: false, | ||||
collapsed: false, | collapsed: false, | ||||
lang: 'zh', | lang: 'zh', | ||||
token: '', | |||||
refreshToken: '', | |||||
cookie: '', | |||||
setDarkMode: (darkMode: State['darkMode']) => set({ | setDarkMode: (darkMode: State['darkMode']) => set({ | ||||
darkMode, | darkMode, | ||||
}), | }), | ||||
@@ -31,6 +32,9 @@ export const useGlobalStore = create<State & Action>()( | |||||
setLang: (lang: State['lang']) => set({ | setLang: (lang: State['lang']) => set({ | ||||
lang, | lang, | ||||
}), | }), | ||||
setCookie: (cookie: State['cookie']) => set({ | |||||
cookie, | |||||
}), | |||||
}; | }; | ||||
}, | }, | ||||
{ | { | ||||
@@ -0,0 +1,24 @@ | |||||
import { Menu } from '@/models'; | |||||
import { create } from 'zustand'; | |||||
import { devtools } from 'zustand/middleware'; | |||||
interface State { | |||||
menus: Menu[] | null; | |||||
} | |||||
interface Action { | |||||
setMenus: (menus: State['menus']) => void; | |||||
} | |||||
export const useMenusStore = create<State & Action>()( | |||||
devtools( | |||||
(set) => { | |||||
return { | |||||
menus: null, | |||||
setMenus: (menus: State['menus']) => | |||||
set({menus}), | |||||
}; | |||||
}, | |||||
{name: 'globalMenuStore'} | |||||
) | |||||
); |