@@ -4,37 +4,33 @@ import { lazy, useEffect, useState } from 'react'; | |||
import GloablLoading from '@/components/global-loading'; | |||
import Slide from './slide'; | |||
import Header from './header'; | |||
// import userService from '@/service'; | |||
// import { useRequest } from '@/hooks/use-request'; | |||
import { useUserStore } from '@/store/global/user'; | |||
import { useMenusStore } from "@/store/global/menu"; | |||
import { Menu } from '@/models'; | |||
// import { components } from '@/config/routes'; | |||
import { components } from '@/router/config'; | |||
import { replaceRoutes, router } from '@/router/router'; | |||
import Result404 from './404'; | |||
import './index.css' | |||
// import { MenuType } from '@/pages/menu/interface'; | |||
import TabsLayout from './tabs-layout'; | |||
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 [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 location = useLocation(); | |||
// const { setLatestMessage } = useMessageStore(); | |||
// const { | |||
// data: currentUserDetail, | |||
// run: getCurrentUserDetail, | |||
// } = useRequest( | |||
// userService.getCurrentUserDetail, | |||
// { manual: true } | |||
// ); | |||
const { runAsync: getAnnouncement } = useRequest(homeService.getAnnouncement, { manual: true }); | |||
const formatMenus = ( | |||
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(() => { | |||
function storageChange(e: StorageEvent) { | |||
@@ -142,10 +134,6 @@ const BasicLayout: React.FC = () => { | |||
} | |||
}, []); | |||
setTimeout(()=>{ | |||
setLoading(false); | |||
}, 3000) | |||
if (loading) { | |||
return ( | |||
<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 type { ItemType } from 'antd/es/menu/hooks/useItems'; | |||
import { Link, useMatches } from 'react-router-dom'; | |||
import { useGlobalStore } from '@/store/global'; | |||
import { antdIcons } from '@/assets/antd-icons'; | |||
import { Menu as MenuType } from '@/models'; | |||
import { useMenusStore } from '@/store/global/menu'; | |||
import { | |||
AccountBookOutlined, | |||
@@ -240,6 +240,7 @@ const items: MenuItem[] = [ | |||
]), | |||
]; | |||
const SlideMenu = () => { | |||
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 ( | |||
<Menu | |||
className='bg-primary color-transition' | |||
mode="inline" | |||
selectedKeys={selectKeys} | |||
style={{ height: '100%', borderRight: 0 }} | |||
items={items} | |||
items={menuData} | |||
inlineCollapsed={collapsed} | |||
openKeys={openKeys} | |||
onOpenChange={setOpenKeys} | |||
@@ -72,7 +72,6 @@ export interface LoginRespDTO { | |||
pop: Array<PopMenu>; | |||
} | |||
export interface Menu { | |||
id: string; | |||
parentId?: string; | |||
@@ -88,7 +87,6 @@ export interface Menu { | |||
path: string; | |||
Component?: any; | |||
parentPaths?: string[]; | |||
authCode?: string; | |||
} | |||
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 { useRequest } from '@/hooks/use-request'; | |||
import { useUserStore } from '@/store/global/user'; | |||
import { useGlobalStore } from '@/store/global'; | |||
import loginService from '@/request/service/login'; | |||
import userService from '@/request/service/user'; | |||
import homeService from '@/request/service/home'; | |||
import { LoginDTO } from '@/models'; | |||
import './index.css' | |||
const Login = () => { | |||
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 { setCurrentUser } = useUserStore(); | |||
const { setCookie } = useGlobalStore(); | |||
const onFinish = async (values: LoginDTO) => { | |||
const [loginError, data] = await login(values); | |||
if (loginError) { | |||
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); | |||
const [e, userInfo] = await getUserInfo(); | |||
const [ _, announcementData ] = await getAnnouncement(); | |||
console.log(announcementData) | |||
console.log(userInfo) | |||
navigate('/'); | |||
}; | |||
@@ -5,10 +5,9 @@ import axios, { | |||
CreateAxiosDefaults, | |||
InternalAxiosRequestConfig, | |||
} from 'axios'; | |||
import {useGlobalStore} from '@/store/global'; | |||
import {antdUtils} from '@/utils/antd'; | |||
const refreshTokenUrl = '/api/auth/refresh/token'; | |||
// axios.defaults.withCredentials = true; | |||
export type Response<T> = Promise<[boolean, T, AxiosResponse<T>]>; | |||
@@ -29,7 +28,6 @@ class Request { | |||
private axiosInstance: AxiosInstance; | |||
private refreshTokenFlag = false; | |||
private requestQueue: { | |||
resolve: any; | |||
config: any; | |||
@@ -46,11 +44,7 @@ class Request { | |||
private async requestInterceptor( | |||
axiosConfig: InternalAxiosRequestConfig | |||
): 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) => { | |||
this.requestQueue.push({ | |||
resolve, | |||
@@ -61,12 +55,6 @@ class Request { | |||
} | |||
this.requestingCount += 1; | |||
const {token} = useGlobalStore.getState(); | |||
if (token) { | |||
axiosConfig.headers.Authorization = `Bearer ${token}`; | |||
} | |||
return Promise.resolve(axiosConfig); | |||
} | |||
@@ -91,8 +79,7 @@ class Request { | |||
resolve(await this.request(config)); | |||
} else if (type === 'reuqest') { | |||
this.requestingCount += 1; | |||
const {token} = useGlobalStore.getState(); | |||
config.headers.Authorization = `Bearer ${token}`; | |||
//TODO: cookie | |||
resolve(config); | |||
} | |||
} | |||
@@ -102,13 +89,10 @@ class Request { | |||
private async responseSuccessInterceptor( | |||
response: AxiosResponse<any, 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]); | |||
} | |||
@@ -119,9 +103,6 @@ class Request { | |||
if (status === 401) { | |||
return new Promise((resolve) => { | |||
this.requestQueue.unshift({resolve, config, type: 'response'}); | |||
if (this.refreshTokenFlag) return; | |||
this.refreshTokenFlag = true; | |||
}); | |||
} else { | |||
antdUtils.notification?.error({ | |||
@@ -134,7 +115,6 @@ class Request { | |||
private reset() { | |||
this.requestQueue = []; | |||
this.refreshTokenFlag = false; | |||
this.requestingCount = 0; | |||
} | |||
@@ -4,7 +4,7 @@ import { LoginDTO, LoginRespDTO } from '@/models' | |||
const loginService = { | |||
// 登录 | |||
login: (loginDTO: LoginDTO) => { | |||
return request.post<LoginRespDTO>('/api/login', loginDTO); | |||
return request.post<LoginRespDTO>('/api/login', loginDTO, { withCredentials: false }); | |||
}, | |||
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 BasicLayout from '@/layout'; | |||
import Dashboard from '@/pages/dashboard'; | |||
import { App } from 'antd'; | |||
import { useEffect } from 'react'; | |||
import { antdUtils } from '@/utils/antd'; | |||
@@ -18,6 +19,7 @@ export const router = createBrowserRouter( | |||
element: ( | |||
<Navigate to="/dashboard" /> | |||
), | |||
children: [], | |||
}, | |||
{ | |||
path: '*', | |||
@@ -62,7 +64,6 @@ export const replaceRoutes = (parentPath: string, routes: RouteObject[]) => { | |||
router.routes.push(...routes as any); | |||
return; | |||
} | |||
const curNode = findNodeByPath(router.routes, parentPath); | |||
if (curNode) { | |||
@@ -5,12 +5,14 @@ interface State { | |||
darkMode: boolean; | |||
collapsed: boolean; | |||
lang: string; | |||
cookie: string; | |||
} | |||
interface Action { | |||
setDarkMode: (darkMode: State['darkMode']) => void; | |||
setCollapsed: (collapsed: State['collapsed']) => void; | |||
setLang: (lang: State['lang']) => void; | |||
setCookie: (cookie: State['cookie']) => void; | |||
} | |||
export const useGlobalStore = create<State & Action>()( | |||
@@ -20,8 +22,7 @@ export const useGlobalStore = create<State & Action>()( | |||
darkMode: false, | |||
collapsed: false, | |||
lang: 'zh', | |||
token: '', | |||
refreshToken: '', | |||
cookie: '', | |||
setDarkMode: (darkMode: State['darkMode']) => set({ | |||
darkMode, | |||
}), | |||
@@ -31,6 +32,9 @@ export const useGlobalStore = create<State & Action>()( | |||
setLang: (lang: State['lang']) => set({ | |||
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'} | |||
) | |||
); |