@@ -1,27 +1,54 @@ | |||
# React + TypeScript + Vite | |||
# VOGOCM-ERP 后台管理系统Web端 | |||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. | |||
## 技术框架 | |||
Currently, two official plugins are available: | |||
技术框架: React + TypeScript + antd + Vite | |||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh | |||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh | |||
## Expanding the ESLint configuration | |||
## 项目目录结构 | |||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: | |||
``` | |||
├── public | |||
│ └── vite.svg | |||
├── src | |||
│ └── assets 公共资源 | |||
│ ├── components 页面组件 | |||
│ ├── router 路由模块 | |||
│ ├── hooks react hooks | |||
│ ├── layout 页面布局 | |||
│ ├── models 数据模型 | |||
│ ├── pages 业务代码 | |||
│ ├── request 处理axios的封装和调用 | |||
│ ├── store 数据持久化和状态管理 | |||
│ ├── utils 公共方法或常量 | |||
│ ├── App.css | |||
│ ├── App.tsx | |||
│ ├── index.css | |||
│ └── main.tsx | |||
├── index.html | |||
├── package.json | |||
└── vite.config.js | |||
``` | |||
- Configure the top-level `parserOptions` property like this: | |||
## 测试 & 构建 | |||
```js | |||
parserOptions: { | |||
ecmaVersion: 'latest', | |||
sourceType: 'module', | |||
project: ['./tsconfig.json', './tsconfig.node.json'], | |||
tsconfigRootDir: __dirname, | |||
}, | |||
### 安装依赖 | |||
``` | |||
pnpm install(推荐使用pnpm) | |||
``` | |||
### 启动 | |||
``` | |||
pnpm start 或 pnpm run dev | |||
``` | |||
### 构建 | |||
``` | |||
pnpm build | |||
``` | |||
### 预览build产物 | |||
``` | |||
pnpm preview | |||
``` | |||
## 关键技术点说明 | |||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` | |||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` | |||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list |
@@ -4,7 +4,7 @@ | |||
<meta charset="UTF-8" /> | |||
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
<title>Vite + React + TS</title> | |||
<title>vogocm erp admin</title> | |||
</head> | |||
<body> | |||
<div id="root"></div> | |||
@@ -4,6 +4,7 @@ | |||
"version": "0.0.0", | |||
"type": "module", | |||
"scripts": { | |||
"start": "vite", | |||
"dev": "vite", | |||
"build": "tsc && vite build", | |||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", | |||
@@ -0,0 +1,39 @@ | |||
<svg width="670" height="903" viewBox="0 0 670 903" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="670" height="903"> | |||
<g opacity="0.2"> | |||
<path d="M0 0H670V903H0V0Z" fill="white"/> | |||
</g> | |||
</mask> | |||
<g mask="url(#mask0)"> | |||
<path d="M2030.91 374.849L426.331 1300.78" stroke="#8492C4"/> | |||
<path d="M426.409 -527.071L2030.72 399.311" stroke="#8492C4"/> | |||
<path d="M1919.22 310.39L314.731 1236.47" stroke="#8492C4"/> | |||
<path d="M314.731 -462.612L1919.22 463.467" stroke="#8492C4"/> | |||
<path d="M1807.54 245.932L203.055 1172.01" stroke="#8492C4"/> | |||
<path d="M203.052 -398.154L1807.54 527.925" stroke="#8492C4"/> | |||
<path d="M1695.87 181.473L91.3788 1107.55" stroke="#8492C4"/> | |||
<path d="M91.3744 -333.695L1695.86 592.384" stroke="#8492C4"/> | |||
<path d="M1584.19 117.014L-20.3012 1043.09" stroke="#8492C4"/> | |||
<path d="M-20.3044 -269.237L1584.19 656.843" stroke="#8492C4"/> | |||
<path d="M1472.51 52.5562L-131.98 978.636" stroke="#8492C4"/> | |||
<path d="M-131.983 -204.778L1472.51 721.301" stroke="#8492C4"/> | |||
<path d="M1360.83 -11.9023L-243.658 914.177" stroke="#8492C4"/> | |||
<path d="M-243.662 -140.319L1360.83 785.76" stroke="#8492C4"/> | |||
<path d="M1249.15 -76.3613L-355.336 849.718" stroke="#8492C4"/> | |||
<path d="M-355.341 -75.8608L1249.15 850.219" stroke="#8492C4"/> | |||
<path d="M1137.48 -140.819L-467.014 785.26" stroke="#8492C4"/> | |||
<path d="M-467.017 -11.4023L1137.47 914.677" stroke="#8492C4"/> | |||
<path d="M1025.8 -205.278L-578.692 720.801" stroke="#8492C4"/> | |||
<path d="M-578.693 53.0562L1025.8 979.136" stroke="#8492C4"/> | |||
<path d="M914.119 -269.736L-690.371 656.343" stroke="#8492C4"/> | |||
<path d="M-690.379 117.515L914.111 1043.59" stroke="#8492C4"/> | |||
<path d="M802.441 -334.195L-802.052 591.887" stroke="#8492C4"/> | |||
<path d="M-802.055 181.974L802.435 1108.05" stroke="#8492C4"/> | |||
<path d="M690.762 -398.654L-913.728 527.426" stroke="#8492C4"/> | |||
<path d="M-913.731 246.432L690.759 1172.51" stroke="#8492C4"/> | |||
<path d="M579.084 -463.112L-1025.41 462.967" stroke="#8492C4"/> | |||
<path d="M-1025.41 310.891L579.083 1236.97" stroke="#8492C4"/> | |||
<path d="M467.406 -527.571L-1136.91 398.811" stroke="#8492C4"/> | |||
<path d="M-1137.09 375.35L467.397 1301.43" stroke="#8492C4"/> | |||
</g> | |||
</svg> |
@@ -1,34 +1,66 @@ | |||
import { useState } from 'react' | |||
import reactLogo from './assets/react.svg' | |||
import viteLogo from '/vite.svg' | |||
import './App.css' | |||
import { useEffect, useMemo } from 'react'; | |||
import { ConfigProvider, ThemeConfig, theme, App as AntdApp } from 'antd' | |||
import zhCN from 'antd/locale/zh_CN'; | |||
import enUS from 'antd/locale/en_US'; | |||
import { useGlobalStore } from './store/global' | |||
import { i18n } from './utils/i18n'; | |||
import Router from './router/router'; | |||
function App() { | |||
const [count, setCount] = useState(0) | |||
const { darkMode, lang } = useGlobalStore(); | |||
useEffect(() => { | |||
if (darkMode) { | |||
document.body.classList.remove('light'); | |||
document.body.classList.add('dark'); | |||
} else { | |||
document.body.classList.remove('dark'); | |||
document.body.classList.add('light'); | |||
} | |||
}, [darkMode]); | |||
useEffect(() => { | |||
i18n.changeLanguage(lang); | |||
}, [lang]); | |||
const curTheme: ThemeConfig = useMemo(() => { | |||
if (darkMode) { | |||
return { | |||
token: { | |||
colorPrimary: 'rgb(124, 77, 255)', | |||
colorBgBase: 'rgb(17, 25, 54)', | |||
colorBgContainer: 'rgb(26, 34, 63)', | |||
colorBorder: 'rgba(189, 200, 240, 0.157)', | |||
colorBgTextHover: 'rgba(124, 77, 255, 0.082)', | |||
colorTextHover: 'rgba(124, 77, 255, 0.082)', | |||
controlItemBgActive: 'rgba(33, 150, 243, 0.16)', | |||
colorBgElevated: 'rgb(33, 41, 70)' | |||
}, | |||
algorithm: theme.darkAlgorithm, | |||
} | |||
} else { | |||
return { | |||
token: { | |||
colorPrimary: 'rgb(124, 77, 255)', | |||
}, | |||
} | |||
} | |||
}, [darkMode]); | |||
return ( | |||
<> | |||
<div> | |||
<a href="https://vitejs.dev" target="_blank"> | |||
<img src={viteLogo} className="logo" alt="Vite logo" /> | |||
</a> | |||
<a href="https://react.dev" target="_blank"> | |||
<img src={reactLogo} className="logo react" alt="React logo" /> | |||
</a> | |||
</div> | |||
<h1>Vite + React</h1> | |||
<div className="card"> | |||
<button onClick={() => setCount((count) => count + 1)}> | |||
count is {count} | |||
</button> | |||
<p> | |||
Edit <code>src/App.tsx</code> and save to test HMR | |||
</p> | |||
</div> | |||
<p className="read-the-docs"> | |||
Click on the Vite and React logos to learn more | |||
</p> | |||
</> | |||
<ConfigProvider | |||
theme={curTheme} | |||
locale={lang === 'zh' ? zhCN : enUS} | |||
componentSize='large' | |||
> | |||
<AntdApp> | |||
<Router /> | |||
</AntdApp> | |||
</ConfigProvider> | |||
) | |||
} | |||
@@ -0,0 +1,846 @@ | |||
import { | |||
StepBackwardOutlined, | |||
StepForwardOutlined, | |||
FastBackwardOutlined, | |||
FastForwardOutlined, | |||
ShrinkOutlined, | |||
ArrowsAltOutlined, | |||
DownOutlined, | |||
UpOutlined, | |||
LeftOutlined, | |||
RightOutlined, | |||
CaretUpOutlined, | |||
CaretDownOutlined, | |||
CaretLeftOutlined, | |||
CaretRightOutlined, | |||
UpCircleOutlined, | |||
DownCircleOutlined, | |||
LeftCircleOutlined, | |||
RightCircleOutlined, | |||
DoubleRightOutlined, | |||
DoubleLeftOutlined, | |||
VerticalLeftOutlined, | |||
VerticalRightOutlined, | |||
VerticalAlignTopOutlined, | |||
VerticalAlignMiddleOutlined, | |||
VerticalAlignBottomOutlined, | |||
ForwardOutlined, | |||
BackwardOutlined, | |||
RollbackOutlined, | |||
EnterOutlined, | |||
RetweetOutlined, | |||
SwapOutlined, | |||
SwapLeftOutlined, | |||
SwapRightOutlined, | |||
ArrowUpOutlined, | |||
ArrowDownOutlined, | |||
ArrowLeftOutlined, | |||
ArrowRightOutlined, | |||
PlayCircleOutlined, | |||
UpSquareOutlined, | |||
DownSquareOutlined, | |||
LeftSquareOutlined, | |||
RightSquareOutlined, | |||
LoginOutlined, | |||
LogoutOutlined, | |||
MenuFoldOutlined, | |||
MenuUnfoldOutlined, | |||
BorderBottomOutlined, | |||
BorderHorizontalOutlined, | |||
BorderInnerOutlined, | |||
BorderOuterOutlined, | |||
BorderLeftOutlined, | |||
BorderRightOutlined, | |||
BorderTopOutlined, | |||
BorderVerticleOutlined, | |||
PicCenterOutlined, | |||
PicLeftOutlined, | |||
PicRightOutlined, | |||
RadiusBottomleftOutlined, | |||
RadiusBottomrightOutlined, | |||
RadiusUpleftOutlined, | |||
RadiusUprightOutlined, | |||
FullscreenOutlined, | |||
FullscreenExitOutlined, | |||
QuestionOutlined, | |||
QuestionCircleOutlined, | |||
PlusOutlined, | |||
PlusCircleOutlined, | |||
PauseOutlined, | |||
PauseCircleOutlined, | |||
MinusOutlined, | |||
MinusCircleOutlined, | |||
PlusSquareOutlined, | |||
MinusSquareOutlined, | |||
InfoOutlined, | |||
InfoCircleOutlined, | |||
ExclamationOutlined, | |||
ExclamationCircleOutlined, | |||
CloseOutlined, | |||
CloseCircleOutlined, | |||
CloseSquareOutlined, | |||
CheckOutlined, | |||
CheckCircleOutlined, | |||
CheckSquareOutlined, | |||
ClockCircleOutlined, | |||
WarningOutlined, | |||
IssuesCloseOutlined, | |||
StopOutlined, | |||
EditOutlined, | |||
FormOutlined, | |||
CopyOutlined, | |||
ScissorOutlined, | |||
DeleteOutlined, | |||
SnippetsOutlined, | |||
DiffOutlined, | |||
HighlightOutlined, | |||
AlignCenterOutlined, | |||
AlignLeftOutlined, | |||
AlignRightOutlined, | |||
BgColorsOutlined, | |||
BoldOutlined, | |||
ItalicOutlined, | |||
UnderlineOutlined, | |||
StrikethroughOutlined, | |||
RedoOutlined, | |||
UndoOutlined, | |||
ZoomInOutlined, | |||
ZoomOutOutlined, | |||
FontColorsOutlined, | |||
FontSizeOutlined, | |||
LineHeightOutlined, | |||
DashOutlined, | |||
SmallDashOutlined, | |||
SortAscendingOutlined, | |||
SortDescendingOutlined, | |||
DragOutlined, | |||
OrderedListOutlined, | |||
UnorderedListOutlined, | |||
RadiusSettingOutlined, | |||
ColumnWidthOutlined, | |||
ColumnHeightOutlined, | |||
AreaChartOutlined, | |||
PieChartOutlined, | |||
BarChartOutlined, | |||
DotChartOutlined, | |||
LineChartOutlined, | |||
RadarChartOutlined, | |||
HeatMapOutlined, | |||
FallOutlined, | |||
RiseOutlined, | |||
StockOutlined, | |||
BoxPlotOutlined, | |||
FundOutlined, | |||
SlidersOutlined, | |||
AndroidOutlined, | |||
AppleOutlined, | |||
WindowsOutlined, | |||
IeOutlined, | |||
ChromeOutlined, | |||
GithubOutlined, | |||
AliwangwangOutlined, | |||
DingdingOutlined, | |||
WeiboSquareOutlined, | |||
WeiboCircleOutlined, | |||
TaobaoCircleOutlined, | |||
Html5Outlined, | |||
WeiboOutlined, | |||
TwitterOutlined, | |||
WechatOutlined, | |||
YoutubeOutlined, | |||
AlipayCircleOutlined, | |||
TaobaoOutlined, | |||
SkypeOutlined, | |||
QqOutlined, | |||
MediumWorkmarkOutlined, | |||
GitlabOutlined, | |||
MediumOutlined, | |||
LinkedinOutlined, | |||
GooglePlusOutlined, | |||
DropboxOutlined, | |||
FacebookOutlined, | |||
CodepenOutlined, | |||
CodeSandboxOutlined, | |||
AmazonOutlined, | |||
GoogleOutlined, | |||
CodepenCircleOutlined, | |||
AlipayOutlined, | |||
AntDesignOutlined, | |||
AntCloudOutlined, | |||
AliyunOutlined, | |||
ZhihuOutlined, | |||
SlackOutlined, | |||
SlackSquareOutlined, | |||
BehanceOutlined, | |||
BehanceSquareOutlined, | |||
DribbbleOutlined, | |||
DribbbleSquareOutlined, | |||
InstagramOutlined, | |||
YuqueOutlined, | |||
AlibabaOutlined, | |||
YahooOutlined, | |||
RedditOutlined, | |||
SketchOutlined, | |||
AccountBookOutlined, | |||
AimOutlined, | |||
AlertOutlined, | |||
ApartmentOutlined, | |||
ApiOutlined, | |||
AppstoreAddOutlined, | |||
AppstoreOutlined, | |||
AudioOutlined, | |||
AudioMutedOutlined, | |||
AuditOutlined, | |||
BankOutlined, | |||
BarcodeOutlined, | |||
BarsOutlined, | |||
BellOutlined, | |||
BlockOutlined, | |||
BookOutlined, | |||
BorderOutlined, | |||
BorderlessTableOutlined, | |||
BranchesOutlined, | |||
BugOutlined, | |||
BuildOutlined, | |||
BulbOutlined, | |||
CalculatorOutlined, | |||
CalendarOutlined, | |||
CameraOutlined, | |||
CarOutlined, | |||
CarryOutOutlined, | |||
CiCircleOutlined, | |||
CiOutlined, | |||
ClearOutlined, | |||
CloudDownloadOutlined, | |||
CloudOutlined, | |||
CloudServerOutlined, | |||
CloudSyncOutlined, | |||
CloudUploadOutlined, | |||
ClusterOutlined, | |||
CodeOutlined, | |||
CoffeeOutlined, | |||
CommentOutlined, | |||
CompassOutlined, | |||
CompressOutlined, | |||
ConsoleSqlOutlined, | |||
ContactsOutlined, | |||
ContainerOutlined, | |||
ControlOutlined, | |||
CopyrightOutlined, | |||
CreditCardOutlined, | |||
CrownOutlined, | |||
CustomerServiceOutlined, | |||
DashboardOutlined, | |||
DatabaseOutlined, | |||
DeleteColumnOutlined, | |||
DeleteRowOutlined, | |||
DeliveredProcedureOutlined, | |||
DeploymentUnitOutlined, | |||
DesktopOutlined, | |||
DingtalkOutlined, | |||
DisconnectOutlined, | |||
DislikeOutlined, | |||
DollarCircleOutlined, | |||
DollarOutlined, | |||
DownloadOutlined, | |||
EllipsisOutlined, | |||
EnvironmentOutlined, | |||
EuroCircleOutlined, | |||
EuroOutlined, | |||
ExceptionOutlined, | |||
ExpandAltOutlined, | |||
ExpandOutlined, | |||
ExperimentOutlined, | |||
ExportOutlined, | |||
EyeOutlined, | |||
EyeInvisibleOutlined, | |||
FieldBinaryOutlined, | |||
FieldNumberOutlined, | |||
FieldStringOutlined, | |||
FieldTimeOutlined, | |||
FileAddOutlined, | |||
FileDoneOutlined, | |||
FileExcelOutlined, | |||
FileExclamationOutlined, | |||
FileOutlined, | |||
FileGifOutlined, | |||
FileImageOutlined, | |||
FileJpgOutlined, | |||
FileMarkdownOutlined, | |||
FilePdfOutlined, | |||
FilePptOutlined, | |||
FileProtectOutlined, | |||
FileSearchOutlined, | |||
FileSyncOutlined, | |||
FileTextOutlined, | |||
FileUnknownOutlined, | |||
FileWordOutlined, | |||
FileZipOutlined, | |||
FilterOutlined, | |||
FireOutlined, | |||
FlagOutlined, | |||
FolderAddOutlined, | |||
FolderOutlined, | |||
FolderOpenOutlined, | |||
FolderViewOutlined, | |||
ForkOutlined, | |||
FormatPainterOutlined, | |||
FrownOutlined, | |||
FunctionOutlined, | |||
FundProjectionScreenOutlined, | |||
FundViewOutlined, | |||
FunnelPlotOutlined, | |||
GatewayOutlined, | |||
GifOutlined, | |||
GiftOutlined, | |||
GlobalOutlined, | |||
GoldOutlined, | |||
GroupOutlined, | |||
HddOutlined, | |||
HeartOutlined, | |||
HistoryOutlined, | |||
HolderOutlined, | |||
HomeOutlined, | |||
HourglassOutlined, | |||
IdcardOutlined, | |||
ImportOutlined, | |||
InboxOutlined, | |||
InsertRowAboveOutlined, | |||
InsertRowBelowOutlined, | |||
InsertRowLeftOutlined, | |||
InsertRowRightOutlined, | |||
InsuranceOutlined, | |||
InteractionOutlined, | |||
KeyOutlined, | |||
LaptopOutlined, | |||
LayoutOutlined, | |||
LikeOutlined, | |||
LineOutlined, | |||
LinkOutlined, | |||
Loading3QuartersOutlined, | |||
LoadingOutlined, | |||
LockOutlined, | |||
MacCommandOutlined, | |||
MailOutlined, | |||
ManOutlined, | |||
MedicineBoxOutlined, | |||
MehOutlined, | |||
MenuOutlined, | |||
MergeCellsOutlined, | |||
MessageOutlined, | |||
MobileOutlined, | |||
MoneyCollectOutlined, | |||
MonitorOutlined, | |||
MoreOutlined, | |||
NodeCollapseOutlined, | |||
NodeExpandOutlined, | |||
NodeIndexOutlined, | |||
NotificationOutlined, | |||
NumberOutlined, | |||
OneToOneOutlined, | |||
PaperClipOutlined, | |||
PartitionOutlined, | |||
PayCircleOutlined, | |||
PercentageOutlined, | |||
PhoneOutlined, | |||
PictureOutlined, | |||
PlaySquareOutlined, | |||
PoundCircleOutlined, | |||
PoundOutlined, | |||
PoweroffOutlined, | |||
PrinterOutlined, | |||
ProfileOutlined, | |||
ProjectOutlined, | |||
PropertySafetyOutlined, | |||
PullRequestOutlined, | |||
PushpinOutlined, | |||
QrcodeOutlined, | |||
ReadOutlined, | |||
ReconciliationOutlined, | |||
RedEnvelopeOutlined, | |||
ReloadOutlined, | |||
RestOutlined, | |||
RobotOutlined, | |||
RocketOutlined, | |||
RotateLeftOutlined, | |||
RotateRightOutlined, | |||
SafetyCertificateOutlined, | |||
SafetyOutlined, | |||
SaveOutlined, | |||
ScanOutlined, | |||
ScheduleOutlined, | |||
SearchOutlined, | |||
SecurityScanOutlined, | |||
SelectOutlined, | |||
SendOutlined, | |||
SettingOutlined, | |||
ShakeOutlined, | |||
ShareAltOutlined, | |||
ShopOutlined, | |||
ShoppingCartOutlined, | |||
ShoppingOutlined, | |||
SisternodeOutlined, | |||
SkinOutlined, | |||
SmileOutlined, | |||
SolutionOutlined, | |||
SoundOutlined, | |||
SplitCellsOutlined, | |||
StarOutlined, | |||
SubnodeOutlined, | |||
SwitcherOutlined, | |||
SyncOutlined, | |||
TableOutlined, | |||
TabletOutlined, | |||
TagOutlined, | |||
TagsOutlined, | |||
TeamOutlined, | |||
ThunderboltOutlined, | |||
ToTopOutlined, | |||
ToolOutlined, | |||
TrademarkCircleOutlined, | |||
TrademarkOutlined, | |||
TransactionOutlined, | |||
TranslationOutlined, | |||
TrophyOutlined, | |||
UngroupOutlined, | |||
UnlockOutlined, | |||
UploadOutlined, | |||
UsbOutlined, | |||
UserAddOutlined, | |||
UserDeleteOutlined, | |||
UserOutlined, | |||
UserSwitchOutlined, | |||
UsergroupAddOutlined, | |||
UsergroupDeleteOutlined, | |||
VerifiedOutlined, | |||
VideoCameraAddOutlined, | |||
VideoCameraOutlined, | |||
WalletOutlined, | |||
WhatsAppOutlined, | |||
WifiOutlined, | |||
WomanOutlined, | |||
} from '@ant-design/icons' | |||
export const antdIcons: any = { | |||
StepBackwardOutlined, | |||
StepForwardOutlined, | |||
FastBackwardOutlined, | |||
FastForwardOutlined, | |||
ShrinkOutlined, | |||
ArrowsAltOutlined, | |||
DownOutlined, | |||
UpOutlined, | |||
LeftOutlined, | |||
RightOutlined, | |||
CaretUpOutlined, | |||
CaretDownOutlined, | |||
CaretLeftOutlined, | |||
CaretRightOutlined, | |||
UpCircleOutlined, | |||
DownCircleOutlined, | |||
LeftCircleOutlined, | |||
RightCircleOutlined, | |||
DoubleRightOutlined, | |||
DoubleLeftOutlined, | |||
VerticalLeftOutlined, | |||
VerticalRightOutlined, | |||
VerticalAlignTopOutlined, | |||
VerticalAlignMiddleOutlined, | |||
VerticalAlignBottomOutlined, | |||
ForwardOutlined, | |||
BackwardOutlined, | |||
RollbackOutlined, | |||
EnterOutlined, | |||
RetweetOutlined, | |||
SwapOutlined, | |||
SwapLeftOutlined, | |||
SwapRightOutlined, | |||
ArrowUpOutlined, | |||
ArrowDownOutlined, | |||
ArrowLeftOutlined, | |||
ArrowRightOutlined, | |||
PlayCircleOutlined, | |||
UpSquareOutlined, | |||
DownSquareOutlined, | |||
LeftSquareOutlined, | |||
RightSquareOutlined, | |||
LoginOutlined, | |||
LogoutOutlined, | |||
MenuFoldOutlined, | |||
MenuUnfoldOutlined, | |||
BorderBottomOutlined, | |||
BorderHorizontalOutlined, | |||
BorderInnerOutlined, | |||
BorderOuterOutlined, | |||
BorderLeftOutlined, | |||
BorderRightOutlined, | |||
BorderTopOutlined, | |||
BorderVerticleOutlined, | |||
PicCenterOutlined, | |||
PicLeftOutlined, | |||
PicRightOutlined, | |||
RadiusBottomleftOutlined, | |||
RadiusBottomrightOutlined, | |||
RadiusUpleftOutlined, | |||
RadiusUprightOutlined, | |||
FullscreenOutlined, | |||
FullscreenExitOutlined, | |||
QuestionOutlined, | |||
QuestionCircleOutlined, | |||
PlusOutlined, | |||
PlusCircleOutlined, | |||
PauseOutlined, | |||
PauseCircleOutlined, | |||
MinusOutlined, | |||
MinusCircleOutlined, | |||
PlusSquareOutlined, | |||
MinusSquareOutlined, | |||
InfoOutlined, | |||
InfoCircleOutlined, | |||
ExclamationOutlined, | |||
ExclamationCircleOutlined, | |||
CloseOutlined, | |||
CloseCircleOutlined, | |||
CloseSquareOutlined, | |||
CheckOutlined, | |||
CheckCircleOutlined, | |||
CheckSquareOutlined, | |||
ClockCircleOutlined, | |||
WarningOutlined, | |||
IssuesCloseOutlined, | |||
StopOutlined, | |||
EditOutlined, | |||
FormOutlined, | |||
CopyOutlined, | |||
ScissorOutlined, | |||
DeleteOutlined, | |||
SnippetsOutlined, | |||
DiffOutlined, | |||
HighlightOutlined, | |||
AlignCenterOutlined, | |||
AlignLeftOutlined, | |||
AlignRightOutlined, | |||
BgColorsOutlined, | |||
BoldOutlined, | |||
ItalicOutlined, | |||
UnderlineOutlined, | |||
StrikethroughOutlined, | |||
RedoOutlined, | |||
UndoOutlined, | |||
ZoomInOutlined, | |||
ZoomOutOutlined, | |||
FontColorsOutlined, | |||
FontSizeOutlined, | |||
LineHeightOutlined, | |||
DashOutlined, | |||
SmallDashOutlined, | |||
SortAscendingOutlined, | |||
SortDescendingOutlined, | |||
DragOutlined, | |||
OrderedListOutlined, | |||
UnorderedListOutlined, | |||
RadiusSettingOutlined, | |||
ColumnWidthOutlined, | |||
ColumnHeightOutlined, | |||
AreaChartOutlined, | |||
PieChartOutlined, | |||
BarChartOutlined, | |||
DotChartOutlined, | |||
LineChartOutlined, | |||
RadarChartOutlined, | |||
HeatMapOutlined, | |||
FallOutlined, | |||
RiseOutlined, | |||
StockOutlined, | |||
BoxPlotOutlined, | |||
FundOutlined, | |||
SlidersOutlined, | |||
AndroidOutlined, | |||
AppleOutlined, | |||
WindowsOutlined, | |||
IeOutlined, | |||
ChromeOutlined, | |||
GithubOutlined, | |||
AliwangwangOutlined, | |||
DingdingOutlined, | |||
WeiboSquareOutlined, | |||
WeiboCircleOutlined, | |||
TaobaoCircleOutlined, | |||
Html5Outlined, | |||
WeiboOutlined, | |||
TwitterOutlined, | |||
WechatOutlined, | |||
YoutubeOutlined, | |||
AlipayCircleOutlined, | |||
TaobaoOutlined, | |||
SkypeOutlined, | |||
QqOutlined, | |||
MediumWorkmarkOutlined, | |||
GitlabOutlined, | |||
MediumOutlined, | |||
LinkedinOutlined, | |||
GooglePlusOutlined, | |||
DropboxOutlined, | |||
FacebookOutlined, | |||
CodepenOutlined, | |||
CodeSandboxOutlined, | |||
AmazonOutlined, | |||
GoogleOutlined, | |||
CodepenCircleOutlined, | |||
AlipayOutlined, | |||
AntDesignOutlined, | |||
AntCloudOutlined, | |||
AliyunOutlined, | |||
ZhihuOutlined, | |||
SlackOutlined, | |||
SlackSquareOutlined, | |||
BehanceOutlined, | |||
BehanceSquareOutlined, | |||
DribbbleOutlined, | |||
DribbbleSquareOutlined, | |||
InstagramOutlined, | |||
YuqueOutlined, | |||
AlibabaOutlined, | |||
YahooOutlined, | |||
RedditOutlined, | |||
SketchOutlined, | |||
AccountBookOutlined, | |||
AimOutlined, | |||
AlertOutlined, | |||
ApartmentOutlined, | |||
ApiOutlined, | |||
AppstoreAddOutlined, | |||
AppstoreOutlined, | |||
AudioOutlined, | |||
AudioMutedOutlined, | |||
AuditOutlined, | |||
BankOutlined, | |||
BarcodeOutlined, | |||
BarsOutlined, | |||
BellOutlined, | |||
BlockOutlined, | |||
BookOutlined, | |||
BorderOutlined, | |||
BorderlessTableOutlined, | |||
BranchesOutlined, | |||
BugOutlined, | |||
BuildOutlined, | |||
BulbOutlined, | |||
CalculatorOutlined, | |||
CalendarOutlined, | |||
CameraOutlined, | |||
CarOutlined, | |||
CarryOutOutlined, | |||
CiCircleOutlined, | |||
CiOutlined, | |||
ClearOutlined, | |||
CloudDownloadOutlined, | |||
CloudOutlined, | |||
CloudServerOutlined, | |||
CloudSyncOutlined, | |||
CloudUploadOutlined, | |||
ClusterOutlined, | |||
CodeOutlined, | |||
CoffeeOutlined, | |||
CommentOutlined, | |||
CompassOutlined, | |||
CompressOutlined, | |||
ConsoleSqlOutlined, | |||
ContactsOutlined, | |||
ContainerOutlined, | |||
ControlOutlined, | |||
CopyrightOutlined, | |||
CreditCardOutlined, | |||
CrownOutlined, | |||
CustomerServiceOutlined, | |||
DashboardOutlined, | |||
DatabaseOutlined, | |||
DeleteColumnOutlined, | |||
DeleteRowOutlined, | |||
DeliveredProcedureOutlined, | |||
DeploymentUnitOutlined, | |||
DesktopOutlined, | |||
DingtalkOutlined, | |||
DisconnectOutlined, | |||
DislikeOutlined, | |||
DollarCircleOutlined, | |||
DollarOutlined, | |||
DownloadOutlined, | |||
EllipsisOutlined, | |||
EnvironmentOutlined, | |||
EuroCircleOutlined, | |||
EuroOutlined, | |||
ExceptionOutlined, | |||
ExpandAltOutlined, | |||
ExpandOutlined, | |||
ExperimentOutlined, | |||
ExportOutlined, | |||
EyeOutlined, | |||
EyeInvisibleOutlined, | |||
FieldBinaryOutlined, | |||
FieldNumberOutlined, | |||
FieldStringOutlined, | |||
FieldTimeOutlined, | |||
FileAddOutlined, | |||
FileDoneOutlined, | |||
FileExcelOutlined, | |||
FileExclamationOutlined, | |||
FileOutlined, | |||
FileGifOutlined, | |||
FileImageOutlined, | |||
FileJpgOutlined, | |||
FileMarkdownOutlined, | |||
FilePdfOutlined, | |||
FilePptOutlined, | |||
FileProtectOutlined, | |||
FileSearchOutlined, | |||
FileSyncOutlined, | |||
FileTextOutlined, | |||
FileUnknownOutlined, | |||
FileWordOutlined, | |||
FileZipOutlined, | |||
FilterOutlined, | |||
FireOutlined, | |||
FlagOutlined, | |||
FolderAddOutlined, | |||
FolderOutlined, | |||
FolderOpenOutlined, | |||
FolderViewOutlined, | |||
ForkOutlined, | |||
FormatPainterOutlined, | |||
FrownOutlined, | |||
FunctionOutlined, | |||
FundProjectionScreenOutlined, | |||
FundViewOutlined, | |||
FunnelPlotOutlined, | |||
GatewayOutlined, | |||
GifOutlined, | |||
GiftOutlined, | |||
GlobalOutlined, | |||
GoldOutlined, | |||
GroupOutlined, | |||
HddOutlined, | |||
HeartOutlined, | |||
HistoryOutlined, | |||
HolderOutlined, | |||
HomeOutlined, | |||
HourglassOutlined, | |||
IdcardOutlined, | |||
ImportOutlined, | |||
InboxOutlined, | |||
InsertRowAboveOutlined, | |||
InsertRowBelowOutlined, | |||
InsertRowLeftOutlined, | |||
InsertRowRightOutlined, | |||
InsuranceOutlined, | |||
InteractionOutlined, | |||
KeyOutlined, | |||
LaptopOutlined, | |||
LayoutOutlined, | |||
LikeOutlined, | |||
LineOutlined, | |||
LinkOutlined, | |||
Loading3QuartersOutlined, | |||
LoadingOutlined, | |||
LockOutlined, | |||
MacCommandOutlined, | |||
MailOutlined, | |||
ManOutlined, | |||
MedicineBoxOutlined, | |||
MehOutlined, | |||
MenuOutlined, | |||
MergeCellsOutlined, | |||
MessageOutlined, | |||
MobileOutlined, | |||
MoneyCollectOutlined, | |||
MonitorOutlined, | |||
MoreOutlined, | |||
NodeCollapseOutlined, | |||
NodeExpandOutlined, | |||
NodeIndexOutlined, | |||
NotificationOutlined, | |||
NumberOutlined, | |||
OneToOneOutlined, | |||
PaperClipOutlined, | |||
PartitionOutlined, | |||
PayCircleOutlined, | |||
PercentageOutlined, | |||
PhoneOutlined, | |||
PictureOutlined, | |||
PlaySquareOutlined, | |||
PoundCircleOutlined, | |||
PoundOutlined, | |||
PoweroffOutlined, | |||
PrinterOutlined, | |||
ProfileOutlined, | |||
ProjectOutlined, | |||
PropertySafetyOutlined, | |||
PullRequestOutlined, | |||
PushpinOutlined, | |||
QrcodeOutlined, | |||
ReadOutlined, | |||
ReconciliationOutlined, | |||
RedEnvelopeOutlined, | |||
ReloadOutlined, | |||
RestOutlined, | |||
RobotOutlined, | |||
RocketOutlined, | |||
RotateLeftOutlined, | |||
RotateRightOutlined, | |||
SafetyCertificateOutlined, | |||
SafetyOutlined, | |||
SaveOutlined, | |||
ScanOutlined, | |||
ScheduleOutlined, | |||
SearchOutlined, | |||
SecurityScanOutlined, | |||
SelectOutlined, | |||
SendOutlined, | |||
SettingOutlined, | |||
ShakeOutlined, | |||
ShareAltOutlined, | |||
ShopOutlined, | |||
ShoppingCartOutlined, | |||
ShoppingOutlined, | |||
SisternodeOutlined, | |||
SkinOutlined, | |||
SmileOutlined, | |||
SolutionOutlined, | |||
SoundOutlined, | |||
SplitCellsOutlined, | |||
StarOutlined, | |||
SubnodeOutlined, | |||
SwitcherOutlined, | |||
SyncOutlined, | |||
TableOutlined, | |||
TabletOutlined, | |||
TagOutlined, | |||
TagsOutlined, | |||
TeamOutlined, | |||
ThunderboltOutlined, | |||
ToTopOutlined, | |||
ToolOutlined, | |||
TrademarkCircleOutlined, | |||
TrademarkOutlined, | |||
TransactionOutlined, | |||
TranslationOutlined, | |||
TrophyOutlined, | |||
UngroupOutlined, | |||
UnlockOutlined, | |||
UploadOutlined, | |||
UsbOutlined, | |||
UserAddOutlined, | |||
UserDeleteOutlined, | |||
UserOutlined, | |||
UserSwitchOutlined, | |||
UsergroupAddOutlined, | |||
UsergroupDeleteOutlined, | |||
VerifiedOutlined, | |||
VideoCameraAddOutlined, | |||
VideoCameraOutlined, | |||
WalletOutlined, | |||
WhatsAppOutlined, | |||
WifiOutlined, | |||
WomanOutlined, | |||
}; |
@@ -0,0 +1,148 @@ | |||
* { | |||
margin: 0; | |||
box-sizing: border-box; | |||
} | |||
.ant-menu-item { | |||
height: 50px !important; | |||
line-height: 50px !important; | |||
} | |||
.ant-menu-submenu-title { | |||
height: 50px !important; | |||
line-height: 50px !important; | |||
} | |||
.ant-menu-item span { | |||
transition: none !important; | |||
} | |||
.ant-menu-item:hover { | |||
color: rgb(124, 77, 255) !important; | |||
background-color: #f0e9f7 !important; | |||
} | |||
.light .ant-menu-item:hover { | |||
color: rgb(124, 77, 255) !important; | |||
background-color: #f0e9f7 !important; | |||
} | |||
.dark .ant-menu-item:hover { | |||
color: rgb(124, 77, 255) !important; | |||
background-color: rgba(124, 77, 255, 0.082) !important; | |||
} | |||
.dark .ant-menu-item-selected { | |||
color: rgb(124, 77, 255) !important; | |||
background-color: rgba(124, 77, 255, 0.082) !important; | |||
} | |||
.ant-menu-submenu-title:hover { | |||
color: rgb(124, 77, 255) !important; | |||
background-color: #f0e9f7 !important; | |||
} | |||
.light .ant-menu-submenu-title:hover { | |||
color: rgb(124, 77, 255) !important; | |||
background-color: #f0e9f7 !important; | |||
} | |||
.dark .ant-menu-submenu-title:hover { | |||
color: rgb(124, 77, 255) !important; | |||
background-color: rgba(124, 77, 255, 0.082) !important; | |||
} | |||
.dark .ant-menu-item-selected { | |||
color: rgb(124, 77, 255) !important; | |||
background-color: rgba(124, 77, 255, 0.082) !important; | |||
} | |||
.ant-menu-inline-collapsed .ant-menu-submenu-selected .ant-menu-submenu-title { | |||
color: rgb(124, 77, 255) !important; | |||
background-color: rgba(124, 77, 255, 0.082) !important; | |||
} | |||
.ant-menu-submenu-selected .ant-menu-submenu-title { | |||
color: rgb(124, 77, 255) !important; | |||
} | |||
.ant-menu-submenu-selected .ant-menu-title-content a { | |||
color: rgb(124, 77, 255) !important; | |||
} | |||
.ant-menu-submenu-open>.ant-menu-submenu-title .ant-menu-title-content a { | |||
color: rgb(124, 77, 255) !important; | |||
} | |||
.ant-menu-submenu-open>.ant-menu-submenu-title { | |||
color: rgb(124, 77, 255) !important; | |||
} | |||
.ant-menu-submenu-open>.ant-menu-submenu-title .ant-menu-item-icon { | |||
color: rgb(124, 77, 255) !important; | |||
} | |||
/* .ant-menu-submenu-selected .ant-menu-title-content a { | |||
color: rgb(124, 77, 255) !important; | |||
} */ | |||
/* .dark .ant-menu-submenu-selected .ant-menu-title-content a { | |||
color: rgb(124, 77, 255) !important; | |||
} */ | |||
/* | |||
.light .ant-menu-submenu-selected .ant-menu-title-content a { | |||
color: rgb(124, 77, 255) !important; | |||
} */ | |||
.dark .ant-menu-title-content a { | |||
color: #ffffff !important; | |||
} | |||
.dark .ant-menu-item-selected .ant-menu-title-content a { | |||
color: rgb(124, 77, 255) !important; | |||
} | |||
.ant-menu-title-content a { | |||
color: rgba(0, 0, 0, 0.88) !important; | |||
} | |||
.light .ant-menu-item-selected .ant-menu-title-content a { | |||
color: rgb(124, 77, 255) !important; | |||
} | |||
.light .ant-menu-title-content a { | |||
color: rgba(0, 0, 0, 0.88) !important; | |||
} | |||
.ant-menu-title-content:hover a { | |||
color: rgb(124, 77, 255) !important; | |||
} | |||
.ant-menu-sub { | |||
background-color: transparent !important; | |||
} | |||
body.dark { | |||
background-color: rgb(17, 25, 54); | |||
} | |||
.ant-btn-primary { | |||
box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 1px -2px, rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px 0px !important; | |||
} | |||
.ant-menu-item .ant-menu-item-icon { | |||
font-size: 18px !important; | |||
vertical-align: middle !important; | |||
} | |||
a { | |||
color: rgb(124, 77, 255); | |||
} | |||
a:hover { | |||
color: rgba(124, 77, 255, 0.7); | |||
} |
@@ -0,0 +1,22 @@ | |||
import Icon from "@ant-design/icons"; | |||
import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; | |||
const SVG3 = () => ( | |||
<svg | |||
style={{ | |||
width: "1em", | |||
height: "1em", | |||
fill: "currentcolor", | |||
overflow: "hidden", | |||
}} | |||
viewBox="0 0 1024 1024" | |||
version="1.1" | |||
xmlns="http://www.w3.org/2000/svg" | |||
> | |||
<path d="M477.04728007 930.47791389c-68.87620267 0-137.16909283-17.11280925-197.49704135-49.49069028-58.36611129-31.32303019-109.17128989-76.74334435-146.92122851-131.34878151-5.38952135-7.79611933-5.51643022-18.08150187-0.32018091-26.00802304 5.19624931-7.92884907 14.68059648-11.9142309 23.97865643-10.07930255 21.53130894 4.24501931 43.46895587 6.39663673 65.20401578 6.39663674 88.70996878 0 172.10725945-34.54463431 234.83016192-97.27102976 62.72290361-62.72756053 97.26637283-146.13067207 97.26637284-234.84762682 0-97.14994403-42.43739079-189.1630171-116.43066937-252.4459463-7.21164402-6.16727097-9.95937963-16.09405099-6.94501945-25.09288676s11.18654691-15.26740082 20.6592512-15.84605526c9.61940594-0.58796942 18.181632-0.87438563 26.17801045-0.87438563 56.47762432 0 111.27866368 11.066624 162.88138468 32.89249906 49.83182791 21.07723321 94.58034461 51.24644637 133.00324579 89.66818361 38.42406514 38.42290119 68.59444338 83.17258183 89.67400448 133.00557368 21.82820409 51.60621397 32.89599203 106.41191026 32.89599204 162.89652054 0 56.47413134-11.06778909 111.27284281-32.89715712 162.87672775-21.07839715 49.82950002-51.24877539 94.57685163-89.67284054 132.99975282s-83.17258183 68.59211435-133.00324579 89.67167659C588.32594375 919.4101248 533.52374045 930.47791389 477.04728007 930.47791389zM205.69434681 766.18773163c27.73001557 29.56377998 60.21966848 54.62639502 95.87853312 73.76274887 53.57969408 28.7545947 114.25809522 43.95446727 175.47440014 43.95446727 99.32601117 0 192.71178809-38.6848677 262.95487374-108.92795222 70.2442496-70.24075662 108.92911616-163.62304057 108.92911616-262.9443948 0-99.33765405-38.6848677-192.72925298-108.92911616-262.97117355-62.65537422-62.65537422-143.73338795-100.2003968-231.01360242-107.57737017 58.48254009 68.18926819 91.17128818 155.28552448 91.17128818 246.3438757 0 101.15511865-39.38693689 196.25357426-110.90608924 267.77738354-71.51915235 71.52380928-166.61295104 110.9130752-267.76224768 110.9130752C216.23354595 766.51955541 210.96511033 766.40894749 205.69434681 766.18773163z"></path> | |||
</svg> | |||
); | |||
export const Icon3 = (props: Partial<CustomIconComponentProps>) => ( | |||
<Icon component={SVG3} {...props} /> | |||
); |
@@ -0,0 +1,22 @@ | |||
import Icon from "@ant-design/icons"; | |||
import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; | |||
const SVGBuguang = () => ( | |||
<svg | |||
style={{ | |||
width: "1em", | |||
height: "1em", | |||
fill: "currentcolor", | |||
overflow: "hidden", | |||
}} | |||
viewBox="0 0 1024 1024" | |||
version="1.1" | |||
xmlns="http://www.w3.org/2000/svg" | |||
> | |||
<path d="M547.84 0 128 468.48l194.56 112.64L140.8 1024l673.28-547.84-204.8-117.76L896 102.4 547.84 0z"></path> | |||
</svg> | |||
); | |||
export const IconBuguang = (props: Partial<CustomIconComponentProps>) => ( | |||
<Icon component={SVGBuguang} {...props} /> | |||
); |
@@ -0,0 +1,22 @@ | |||
import Icon from "@ant-design/icons"; | |||
import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; | |||
const SVGFangdajing = () => ( | |||
<svg | |||
style={{ | |||
width: "1em", | |||
height: "1em", | |||
fill: "currentcolor", | |||
overflow: "hidden", | |||
}} | |||
viewBox="0 0 1024 1024" | |||
version="1.1" | |||
xmlns="http://www.w3.org/2000/svg" | |||
> | |||
<path d="M781.7 728l-13.6 13.6-102-102c54.7-61.1 88.3-141.6 88.3-230.1C754.4 218.7 599.7 64 408.9 64S63.4 218.7 63.4 409.5 218.1 755 408.9 755c88.5 0 168.9-33.6 230.1-88.3l102 102-13.6 13.6 177.1 177.1 54.3-54.3L781.7 728z m-680-318.6c0-169.3 137.8-307.1 307.1-307.1s307.1 137.8 307.1 307.1-137.8 307.1-307.1 307.1c-169.3 0.1-307.1-137.7-307.1-307.1z"></path> | |||
</svg> | |||
); | |||
export const IconFangdajing = (props: Partial<CustomIconComponentProps>) => ( | |||
<Icon component={SVGFangdajing} {...props} /> | |||
); |
@@ -0,0 +1,22 @@ | |||
import Icon from "@ant-design/icons"; | |||
import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; | |||
const SVGJiaretaiyang = () => ( | |||
<svg | |||
style={{ | |||
width: "1em", | |||
height: "1em", | |||
fill: "currentcolor", | |||
overflow: "hidden", | |||
}} | |||
viewBox="0 0 1024 1024" | |||
version="1.1" | |||
xmlns="http://www.w3.org/2000/svg" | |||
> | |||
<path d="M265.7 512c0-19.1-16.5-31.9-31.9-31.9H95.7c-19.1 0-31.9 16.5-31.9 31.9 0 15.4 12.8 31.9 31.9 31.9h138.1c16.4 0 31.9-15.5 31.9-31.9zM744.2 311.7c9.4 0 17.1-3.7 22.1-10.8l91.3-91.3c6.1-5 9.6-12 10.1-19.6 0.5-8.5-2.9-16.8-9.5-23.6-5.4-6.5-12.9-10.1-21.1-10.1-8.1 0-16 3.5-22.4 9.9L722.8 260c-6 5-9.6 11.9-10 19.6-0.5 8.6 3 17.1 9.8 23.9 5.3 5.2 13.1 8.2 21.6 8.2zM512 265.7c16.4 0 31.9-15.5 31.9-31.9V95.7c0-19.1-16.5-31.9-31.9-31.9-15.4 0-31.9 12.8-31.9 31.9v138.1c0 19.1 16.5 31.9 31.9 31.9zM166.4 209.6l91.8 93.8c5.3 5.3 13.2 8.3 21.6 8.3 9.3 0 16.8-3.6 21.9-10.5 5.7-5 9.1-11.7 9.5-19.1 0.5-8.6-3-17.1-9.8-23.9l-91.8-91.8c-5.4-6.5-12.9-10.1-21.1-10.1-7.9 0-15.8 3.4-22 9.5-6.4 5.3-10.1 13.3-10.1 21.9 0 8.6 3.6 16.6 10 21.9zM280.6 714.8c-8.1 0-16 3.5-22.3 9.8l-91.8 91.8c-6.1 5-9.6 12-10.1 19.6-0.5 8.6 3 17.1 9.8 23.9 5.3 5.3 13.2 8.3 21.6 8.3 9.4 0 17-3.7 22.1-10.8l91.3-89.3c6.1-5 9.7-12 10.1-19.6 0.5-8.5-2.9-16.8-9.5-23.6-5.5-6.5-13-10.1-21.2-10.1zM928.3 480.1H790.2c-19.1 0-31.9 16.5-31.9 31.9 0 19.1 16.5 31.9 31.9 31.9h138.1c19.1 0 31.9-16.5 31.9-31.9 0-19.1-16.5-31.9-31.9-31.9zM766 724.9c-5.4-6.5-12.9-10.1-21.1-10.1-7.9 0-15.8 3.4-22 9.5-6.1 5-9.7 12-10.1 19.6-0.5 8.6 3 17.1 9.8 23.9l92.1 92.1c5.3 5.3 13.1 8.3 21.6 8.3 8.2 0 16-2.9 21.3-8 6.1-5 9.7-12 10.1-19.6 0.5-8.6-3-17.1-9.8-23.9L766 724.9zM664.4 359.6c-41-41-95.1-63.6-152.4-63.6-57.3 0-111.4 22.6-152.4 63.6S296 454.8 296 512c0 57.3 22.6 111.4 63.6 152.4S454.7 728 512 728c57.3 0 111.4-22.6 152.4-63.6S728 569.3 728 512c0-57.3-22.6-111.4-63.6-152.4zM512 357.8c85 0 154.2 69.2 154.2 154.2 0 85-69.2 154.2-154.2 154.2S357.8 597 357.8 512c0-85 69.2-154.2 154.2-154.2zM512 758.3c-19.1 0-31.9 16.5-31.9 31.9v138.1c0 19.1 16.5 31.9 31.9 31.9 15.4 0 31.9-12.8 31.9-31.9V790.2c0-19.1-16.5-31.9-31.9-31.9z"></path> | |||
</svg> | |||
); | |||
export const IconJiaretaiyang = (props: Partial<CustomIconComponentProps>) => ( | |||
<Icon component={SVGJiaretaiyang} {...props} /> | |||
); |
@@ -0,0 +1,22 @@ | |||
import Icon from "@ant-design/icons"; | |||
import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; | |||
const SVGMoon = () => ( | |||
<svg | |||
style={{ | |||
width: "1em", | |||
height: "1em", | |||
fill: "currentcolor", | |||
overflow: "hidden", | |||
}} | |||
viewBox="0 0 1024 1024" | |||
version="1.1" | |||
xmlns="http://www.w3.org/2000/svg" | |||
> | |||
<path d="M572.4 229.9c8.8-8.8 21.9-11.7 33.6-7.5 115.1 41.7 197.5 152 197.5 281.6 0 165.4-134.1 299.5-299.5 299.5-129.6 0-239.9-82.3-281.6-197.5-4.2-11.7-1.3-24.8 7.5-33.6s21.9-11.7 33.6-7.5c25 9 51.9 14 80.1 14 129.9 0 235.3-105.3 235.3-235.3 0-28.2-4.9-55.2-14-80.1-4.2-11.7-1.3-24.8 7.5-33.6z m69.1 83.2c1 10 1.5 20.2 1.5 30.5C643 509 508.9 643 343.6 643c-10.3 0-20.5-0.5-30.5-1.5 42.7 59.3 112.4 97.8 191 97.8C634 739.3 739.3 634 739.3 504c0-78.5-38.5-148.2-97.8-190.9z"></path> | |||
</svg> | |||
); | |||
export const IconMoon = (props: Partial<CustomIconComponentProps>) => ( | |||
<Icon component={SVGMoon} {...props} /> | |||
); |
@@ -0,0 +1,22 @@ | |||
import Icon from "@ant-design/icons"; | |||
import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; | |||
const SVGShuyi_fanyi36 = () => ( | |||
<svg | |||
style={{ | |||
width: "1em", | |||
height: "1em", | |||
fill: "currentcolor", | |||
overflow: "hidden", | |||
}} | |||
viewBox="0 0 1024 1024" | |||
version="1.1" | |||
xmlns="http://www.w3.org/2000/svg" | |||
> | |||
<path d="M938.666667 981.333333c-17.066667 0-29.866667-8.533333-38.4-25.6l-59.733334-119.466666h-277.333333l-59.733333 119.466666c-8.533333 21.333333-34.133333 29.866667-55.466667 17.066667-25.6-8.533333-34.133333-34.133333-21.333333-51.2l72.533333-140.8 145.066667-290.133333c12.8-21.333333 34.133333-38.4 59.733333-38.4s46.933333 12.8 59.733333 38.4l145.066667 290.133333 72.533333 140.8c8.533333 21.333333 0 46.933333-17.066666 55.466667-12.8 4.266667-17.066667 4.266667-25.6 4.266666z m-332.8-226.133333h192l-98.133334-192-93.866666 192zM85.333333 844.8c-17.066667 0-29.866667-8.533333-38.4-25.6-8.533333-21.333333 0-46.933333 21.333334-55.466667 93.866667-46.933333 179.2-110.933333 247.466666-187.733333-46.933333-64-85.333333-128-110.933333-192-8.533333-21.333333 4.266667-46.933333 25.6-55.466667 21.333333-8.533333 46.933333 4.266667 55.466667 25.6 21.333333 51.2 46.933333 102.4 81.066666 149.333334 59.733333-85.333333 102.4-179.2 128-281.6H85.333333c-25.6 0-42.666667-17.066667-42.666666-42.666667s17.066667-42.666667 42.666666-42.666667h243.2V85.333333c0-25.6 17.066667-42.666667 42.666667-42.666666s42.666667 17.066667 42.666667 42.666666v51.2h238.933333c25.6 0 42.666667 17.066667 42.666667 42.666667s-17.066667 42.666667-42.666667 42.666667h-68.266667c-25.6 128-85.333333 247.466667-162.133333 349.866666l25.6 25.6c17.066667 17.066667 17.066667 42.666667 0 59.733334-17.066667 17.066667-42.666667 17.066667-59.733333 0l-17.066667-17.066667c-72.533333 81.066667-162.133333 149.333333-264.533333 200.533333-8.533333 0-17.066667 4.266667-21.333334 4.266667z"></path> | |||
</svg> | |||
); | |||
export const IconShuyi_fanyi36 = (props: Partial<CustomIconComponentProps>) => ( | |||
<Icon component={SVGShuyi_fanyi36} {...props} /> | |||
); |
@@ -0,0 +1,78 @@ | |||
export default { | |||
wbTMzvDM: "Vogocm Background Management System", | |||
wVzXBuYs: "Please enter an account", | |||
RNISycbR: "account number", | |||
DjMcEMAe: "Please input a password", | |||
HplkKxdY: "password", | |||
dDdqAAve: "Sign in", | |||
wrQwwbSV: "Chinese", | |||
hGtEfNnp: "English", | |||
jhqxJPbn: "Search Menu", | |||
wPqFuoLF: "Log out of login", | |||
yAdJryjx: "Indicator Description", | |||
nKMAkrqJ: "Total sales revenue", | |||
NpRFMJyD: "Weekly YoY", | |||
WOQnwYUS: "Daily YoY", | |||
ZPCQOWAn: "Daily sales", | |||
iLyPEqwQ: "Indicator Description", | |||
ftuxZMpL: "Visits", | |||
sehypRaO: "Daily Visits", | |||
sdOusITo: "Indicator Description", | |||
PIYkoguj: "Number of payments", | |||
BUjwpMzX: "Conversion rate", | |||
fHpiDHYH: "Total growth", | |||
yLkZTWbn: "today", | |||
QFqMuZiD: "This month", | |||
lGOcGyrv: "This year", | |||
yzUIyMhr: "Store sales", | |||
aSPCUBcK: "today", | |||
EhTpnarX: "This month", | |||
AGGPEAdX: "This year", | |||
jTSvVuJx: "Shanghai Branch", | |||
SwsawJhB: "20% profit", | |||
JYSgIJHD: "Shanghai Branch", | |||
yELACPnu: "20% profit", | |||
WAiyAuwV: "Hefei Branch", | |||
HpNzGyBz: "6% profit", | |||
nGvTAQld: "Beijing Branch", | |||
EeunYupT: "8% loss", | |||
usCBUdwp: "Suzhou Branch", | |||
TacOGPiP: "14% profit", | |||
Imkllizi: "Nanjing Branch", | |||
MzCxBxLH: "6% loss", | |||
LhjNVSoc: "name", | |||
MOlwAEMx: "Age", | |||
npxxdPKd: "address", | |||
YoERuunu: "occupation", | |||
QkOmYwne: "operation", | |||
EOSDTAVT: "name", | |||
hQeqcUTv: "Age", | |||
YHapJMTT: "search", | |||
uCkoPyVp: "eliminate", | |||
qYznwlfj: "user name", | |||
gohANZwy: "nickname", | |||
yBxFprdB: "Mobile phone number", | |||
XWVvMWig: "mailbox", | |||
ykrQSYRh: "Gender", | |||
AkkyZTUy: "male", | |||
yduIcxbx: "female", | |||
TMuQjpWo: "Creation time", | |||
qEIlwmxC: "edit", | |||
JjwFfqHG: "warning", | |||
nlZBTfzL: "Are you sure to delete this data?", | |||
bvwOSeoJ: "Successfully deleted!", | |||
HJYhipnp: "delete", | |||
rnyigssw: "nickname", | |||
SPsRnpyN: "Mobile phone number", | |||
morEPEyc: "Add", | |||
wXpnewYo: "edit", | |||
VjwnJLPY: "New", | |||
NfOSPWDa: "Updated successfully!", | |||
JANFdKFM: "Created successfully!", | |||
jwGPaPNq: "Cannot be empty", | |||
iricpuxB: "Cannot be empty", | |||
UdKeETRS: "Cannot be empty", | |||
AnDwfuuT: "Incorrect phone number format", | |||
QFkffbad: "Cannot be empty", | |||
EfwYKLsR: "Incorrect email format", | |||
}; |
@@ -0,0 +1,78 @@ | |||
export default { | |||
wbTMzvDM: "旺嘉-ERP后台管理系统", | |||
wVzXBuYs: "请输入账号", | |||
RNISycbR: "账号", | |||
DjMcEMAe: "请输入密码", | |||
HplkKxdY: "密码", | |||
dDdqAAve: "登录", | |||
wrQwwbSV: "中文", | |||
hGtEfNnp: "英语", | |||
jhqxJPbn: "搜索菜单", | |||
wPqFuoLF: "退出登录", | |||
yAdJryjx: "指标说明", | |||
nKMAkrqJ: "总销售额", | |||
NpRFMJyD: "周同比", | |||
WOQnwYUS: "日同比", | |||
ZPCQOWAn: "日销售额", | |||
iLyPEqwQ: "指标说明", | |||
ftuxZMpL: "访问量", | |||
sehypRaO: "日访问量", | |||
sdOusITo: "指标说明", | |||
PIYkoguj: "支付笔数", | |||
BUjwpMzX: "转化率", | |||
fHpiDHYH: "总增长", | |||
yLkZTWbn: "今日", | |||
QFqMuZiD: "本月", | |||
lGOcGyrv: "本年", | |||
yzUIyMhr: "门店销售额", | |||
aSPCUBcK: "今日", | |||
EhTpnarX: "本月", | |||
AGGPEAdX: "本年", | |||
jTSvVuJx: "上海分店", | |||
SwsawJhB: "20% 利润", | |||
JYSgIJHD: "上海分店", | |||
yELACPnu: "20% 利润", | |||
WAiyAuwV: "合肥分店", | |||
HpNzGyBz: "6% 利润", | |||
nGvTAQld: "北京分店", | |||
EeunYupT: "8% 亏损", | |||
usCBUdwp: "苏州分店", | |||
TacOGPiP: "14% 利润", | |||
Imkllizi: "南京分店", | |||
MzCxBxLH: "6% 亏损", | |||
LhjNVSoc: "名称", | |||
MOlwAEMx: "年龄", | |||
npxxdPKd: "地址", | |||
YoERuunu: "职业", | |||
QkOmYwne: "操作", | |||
EOSDTAVT: "名称", | |||
hQeqcUTv: "年龄", | |||
YHapJMTT: "搜索", | |||
uCkoPyVp: "清除", | |||
qYznwlfj: "用户名", | |||
gohANZwy: "昵称", | |||
yBxFprdB: "手机号", | |||
XWVvMWig: "邮箱", | |||
ykrQSYRh: "性别", | |||
AkkyZTUy: "男", | |||
yduIcxbx: "女", | |||
TMuQjpWo: "创建时间", | |||
qEIlwmxC: "编辑", | |||
JjwFfqHG: "警告", | |||
nlZBTfzL: "确认删除这条数据?", | |||
bvwOSeoJ: "删除成功!", | |||
HJYhipnp: "删除", | |||
rnyigssw: "昵称", | |||
SPsRnpyN: "手机号", | |||
morEPEyc: "新增", | |||
wXpnewYo: "编辑", | |||
VjwnJLPY: "新建", | |||
NfOSPWDa: "更新成功!", | |||
JANFdKFM: "创建成功!", | |||
jwGPaPNq: "不能为空", | |||
iricpuxB: "不能为空", | |||
UdKeETRS: "不能为空", | |||
AnDwfuuT: "手机号格式不正确", | |||
QFkffbad: "不能为空", | |||
EfwYKLsR: "邮箱格式不正确", | |||
}; |
@@ -1 +0,0 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> |
@@ -0,0 +1,15 @@ | |||
.tab-layout.ant-tabs-card>.ant-tabs-nav .ant-tabs-tab { | |||
transition: none; | |||
} | |||
.tab-layout.ant-tabs-card>.ant-tabs-nav .ant-tabs-tab-remove:active { | |||
color: unset; | |||
} | |||
.tab-layout.ant-tabs-card>.ant-tabs-nav .ant-tabs-tab-btn:active { | |||
color: unset; | |||
} | |||
.tab-layout.ant-tabs-card>.ant-tabs-nav .ant-tabs-tab-btn:focus:not(:focus-visible) { | |||
color: unset; | |||
} |
@@ -0,0 +1,88 @@ | |||
import type { DragEndEvent } from '@dnd-kit/core'; | |||
import { DndContext, PointerSensor, useSensor } from '@dnd-kit/core'; | |||
import { | |||
arrayMove, | |||
horizontalListSortingStrategy, | |||
SortableContext, | |||
useSortable, | |||
} from '@dnd-kit/sortable'; | |||
import { CSS } from '@dnd-kit/utilities'; | |||
import React, { useEffect, useState } from 'react'; | |||
import { Tabs, TabsProps } from 'antd'; | |||
import { | |||
restrictToHorizontalAxis, | |||
} from '@dnd-kit/modifiers'; | |||
import './index.css' | |||
interface DraggableTabPaneProps extends React.HTMLAttributes<HTMLDivElement> { | |||
'data-node-key': string; | |||
} | |||
const DraggableTabNode = (props: DraggableTabPaneProps) => { | |||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ | |||
id: props['data-node-key'], | |||
}); | |||
const style: React.CSSProperties = { | |||
...props.style, | |||
transform: CSS.Transform.toString(transform && { ...transform, scaleX: 1 }), | |||
transition, | |||
}; | |||
return React.cloneElement(props.children as React.ReactElement, { | |||
ref: setNodeRef, | |||
style, | |||
...attributes, | |||
...listeners, | |||
}); | |||
}; | |||
const DraggableTab: React.FC<TabsProps & { onItemsChange?: (items: any[]) => void }> = ({ onItemsChange, ...props }) => { | |||
const [items, setItems] = useState(props.items || []); | |||
const sensor = useSensor(PointerSensor, { activationConstraint: { distance: 10 } }); | |||
const onDragEnd = ({ active, over }: DragEndEvent) => { | |||
if (active.id !== over?.id) { | |||
setItems((prev) => { | |||
const activeIndex = prev.findIndex((i) => i.key === active.id); | |||
const overIndex = prev.findIndex((i) => i.key === over?.id); | |||
return arrayMove(prev, activeIndex, overIndex); | |||
}); | |||
} | |||
}; | |||
useEffect(() => { | |||
setItems(props.items || []); | |||
}, [props.items]); | |||
useEffect(() => { | |||
if (onItemsChange) { | |||
onItemsChange(items); | |||
} | |||
}, [items]); | |||
return ( | |||
<Tabs | |||
renderTabBar={(tabBarProps, DefaultTabBar) => ( | |||
<DndContext sensors={[sensor]} onDragEnd={onDragEnd} modifiers={[restrictToHorizontalAxis]}> | |||
<SortableContext items={items.map((i) => i.key)} strategy={horizontalListSortingStrategy}> | |||
<DefaultTabBar {...tabBarProps}> | |||
{(node) => ( | |||
<DraggableTabNode {...node.props} key={node.key}> | |||
{node} | |||
</DraggableTabNode> | |||
)} | |||
</DefaultTabBar> | |||
</SortableContext> | |||
</DndContext> | |||
)} | |||
{...props} | |||
items={items} | |||
className='tab-layout' | |||
/> | |||
); | |||
}; | |||
export default DraggableTab; |
@@ -0,0 +1,47 @@ | |||
.loading { | |||
display: block; | |||
position: relative; | |||
width: 6px; | |||
height: 10px; | |||
border-radius: 2px; | |||
animation: rectangle infinite 1s ease-in-out -0.2s; | |||
background-color: #673AB7; | |||
} | |||
.loading:before, | |||
.loading:after { | |||
position: absolute; | |||
width: 6px; | |||
height: 10px; | |||
content: ""; | |||
background-color: #673AB7; | |||
border-radius: 2px; | |||
} | |||
.loading:before { | |||
left: -14px; | |||
animation: rectangle infinite 1s ease-in-out -0.4s; | |||
} | |||
.loading:after { | |||
right: -14px; | |||
animation: rectangle infinite 1s ease-in-out; | |||
} | |||
@keyframes rectangle { | |||
0%, | |||
80%, | |||
100% { | |||
height: 20px; | |||
box-shadow: 0 0 #673AB7; | |||
} | |||
40% { | |||
height: 30px; | |||
box-shadow: 0 -20px #673AB7; | |||
} | |||
} |
@@ -0,0 +1,13 @@ | |||
import React from "react"; | |||
import './index.css' | |||
const GloablLoading: React.FC<any> = () => ( | |||
<> | |||
<div className='w-[100vw] h-[100vh] flex justify-center items-center'> | |||
<div className="loading transform translate-y-[-12vh]"></div> | |||
</div> | |||
</> | |||
) | |||
export default GloablLoading; |
@@ -0,0 +1,20 @@ | |||
import { Spin } from 'antd'; | |||
import NProgress from 'nprogress'; | |||
import { useEffect } from 'react'; | |||
export const Loading = () => { | |||
useEffect(() => { | |||
NProgress.start(); | |||
return () => { | |||
NProgress.done(); | |||
} | |||
}, []) | |||
return ( | |||
<div className='flex justify-center'> | |||
<Spin /> | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,14 @@ | |||
export const defaultSetting = { | |||
slideWidth: 280, | |||
languages: [ | |||
{ | |||
key: 'zh', | |||
name: 'wrQwwbSV', | |||
}, | |||
{ | |||
key: 'en', | |||
name: 'hGtEfNnp', | |||
}, | |||
], | |||
defaultLang: 'zh', | |||
} |
@@ -0,0 +1,47 @@ | |||
import { useEffect, useState } from 'react'; | |||
import { useLocation, useMatches, useOutlet } from 'react-router-dom'; | |||
interface MatchRouteType { | |||
// 菜单名称 | |||
title: string; | |||
// tab对应的url | |||
pathname: string; | |||
// 要渲染的组件 | |||
children: any; | |||
// 路由,和pathname区别是,详情页 pathname是 /:id,routePath是 /1 | |||
routePath: string; | |||
// 图标 | |||
icon?: string; | |||
} | |||
export function useMatchRoute(): MatchRouteType | undefined { | |||
// 获取路由组件实例 | |||
const children = useOutlet(); | |||
// 获取所有路由 | |||
const matches = useMatches(); | |||
// 获取当前url | |||
const { pathname } = useLocation(); | |||
const [matchRoute, setMatchRoute] = useState<MatchRouteType | undefined>(); | |||
// 监听pathname变了,说明路由有变化,重新匹配,返回新路由信息 | |||
useEffect(() => { | |||
// 获取当前匹配的路由 | |||
const lastRoute = matches.at(-1); | |||
if (!lastRoute?.handle) return; | |||
setMatchRoute({ | |||
title: (lastRoute?.handle as any)?.name, | |||
pathname, | |||
children, | |||
routePath: lastRoute?.pathname || '', | |||
icon: (lastRoute?.handle as any)?.icon, | |||
}); | |||
}, [pathname]) | |||
return matchRoute; | |||
} |
@@ -0,0 +1,6 @@ | |||
import { useMedia } from 'react-use'; | |||
export const usePCScreen = () => { | |||
const isPC = useMedia('(min-width: 1024px)'); | |||
return isPC; | |||
} |
@@ -0,0 +1,77 @@ | |||
import { useCallback, useEffect, useRef, useState } from 'react'; | |||
import { Response } from '@/request'; | |||
interface RequestOptions { | |||
manual?: boolean; | |||
defaultParams?: any[]; | |||
} | |||
interface RequestResponse<T> { | |||
error: boolean | undefined; | |||
data: T | undefined; | |||
loading: boolean; | |||
run(...params: any): void; | |||
runAsync(...params: any): Response<T>; | |||
refresh(): void; | |||
} | |||
export function useRequest<T>( | |||
serviceMethod: (...args: any) => Response<T>, | |||
options?: RequestOptions | |||
): RequestResponse<T> { | |||
const [loading, setLoading] = useState<boolean>(false); | |||
const [data, setData] = useState<T>(); | |||
const [error, setError] = useState<boolean>(); | |||
const paramsRef = useRef<any[]>([]); | |||
const resolveData = useCallback(async () => { | |||
setLoading(true); | |||
const [error, requestData] = await serviceMethod( | |||
...(options?.defaultParams || []) | |||
); | |||
setLoading(false); | |||
setData(requestData); | |||
setError(error); | |||
}, [serviceMethod, options]); | |||
const runAsync = useCallback( | |||
async (...params: any) => { | |||
paramsRef.current = params; | |||
setLoading(true); | |||
const res = await serviceMethod(...params); | |||
const [err, curData] = res; | |||
setError(err); | |||
setLoading(false); | |||
setData(curData); | |||
return res; | |||
}, | |||
[serviceMethod] | |||
); | |||
const run = useCallback( | |||
async (...params: any) => { | |||
await runAsync(...params); | |||
}, | |||
[runAsync] | |||
); | |||
const refresh = useCallback(() => { | |||
runAsync(...paramsRef.current); | |||
}, [runAsync]); | |||
useEffect(() => { | |||
if (!options?.manual) { | |||
resolveData(); | |||
} | |||
}, [options, resolveData]); | |||
return { | |||
loading, | |||
error, | |||
data, | |||
run, | |||
runAsync, | |||
refresh, | |||
}; | |||
} |
@@ -0,0 +1,106 @@ | |||
// /src/layouts/useTabs.tsx | |||
import { useMatchRoute } from '@/hooks/use-match-router'; | |||
import { router } from '@/router/router'; | |||
import { useCallback, useEffect, useState } from 'react'; | |||
export interface KeepAliveTab { | |||
title: string; | |||
routePath: string; | |||
key: string; | |||
pathname: string; | |||
icon?: any; | |||
children: any; | |||
} | |||
function getKey() { | |||
return new Date().getTime().toString(); | |||
} | |||
export function useTabs() { | |||
// 存放页面记录 | |||
const [keepAliveTabs, setKeepAliveTabs] = useState<KeepAliveTab[]>([]); | |||
// 当前激活的tab | |||
const [activeTabRoutePath, setActiveTabRoutePath] = useState<string>(''); | |||
const matchRoute = useMatchRoute(); | |||
// 关闭tab | |||
const closeTab = useCallback( | |||
(routePath: string = activeTabRoutePath) => { | |||
const index = keepAliveTabs.findIndex(o => o.routePath === routePath); | |||
if (keepAliveTabs[index].routePath === activeTabRoutePath) { | |||
if (index > 0) { | |||
router.navigate(keepAliveTabs[index - 1].routePath); | |||
} else { | |||
router.navigate(keepAliveTabs[index + 1].routePath); | |||
} | |||
} | |||
keepAliveTabs.splice(index, 1); | |||
setKeepAliveTabs([...keepAliveTabs]); | |||
}, | |||
[activeTabRoutePath], | |||
); | |||
// 关闭除了自己其它tab | |||
const closeOtherTab = useCallback((routePath: string = activeTabRoutePath) => { | |||
setKeepAliveTabs(prev => prev.filter(o => o.routePath === routePath)); | |||
}, [activeTabRoutePath]); | |||
// 刷新tab | |||
const refreshTab = useCallback((routePath: string = activeTabRoutePath) => { | |||
setKeepAliveTabs(prev => { | |||
const index = prev.findIndex(tab => tab.routePath === routePath); | |||
if (index >= 0) { | |||
// 这个是react的特性,key变了,组件会卸载重新渲染 | |||
prev[index].key = getKey(); | |||
} | |||
return [...prev]; | |||
}); | |||
}, [activeTabRoutePath]); | |||
useEffect(() => { | |||
if (!matchRoute) return; | |||
const existKeepAliveTab = keepAliveTabs.find(o => o.routePath === matchRoute?.routePath); | |||
// 如果不存在则需要插入 | |||
if (!existKeepAliveTab) { | |||
setKeepAliveTabs(prev => [...prev, { | |||
title: matchRoute.title, | |||
key: getKey(), | |||
routePath: matchRoute.routePath, | |||
pathname: matchRoute.pathname, | |||
children: matchRoute.children, | |||
icon: matchRoute.icon, | |||
}]); | |||
} else if (existKeepAliveTab.pathname !== matchRoute.pathname) { | |||
// 如果是同一个路由,但是参数不同,我们只需要刷新当前页签并且把pathname设置为新的pathname, children设置为新的children | |||
setKeepAliveTabs(prev => { | |||
const index = prev.findIndex(tab => tab.routePath === matchRoute.routePath); | |||
if (index >= 0) { | |||
prev[index].key = getKey(); | |||
prev[index].pathname = matchRoute.pathname; | |||
prev[index].children = matchRoute.children; | |||
} | |||
return [...prev]; | |||
}); | |||
} | |||
setActiveTabRoutePath(matchRoute.routePath); | |||
}, [matchRoute]) | |||
return { | |||
tabs: keepAliveTabs, | |||
activeTabRoutePath, | |||
closeTab, | |||
closeOtherTab, | |||
refreshTab, | |||
setTabs: setKeepAliveTabs, | |||
} | |||
} |
@@ -0,0 +1,17 @@ | |||
import { Button, Result } from 'antd'; | |||
import { Link } from 'react-router-dom'; | |||
const Result404 = () => ( | |||
<Result | |||
status="404" | |||
title="404" | |||
subTitle="对不起,你访问的页面不存在。" | |||
extra={( | |||
<Button type="primary"> | |||
<Link to="/">首页</Link> | |||
</Button> | |||
)} | |||
/> | |||
); | |||
export default Result404; |
@@ -0,0 +1,41 @@ | |||
import { Loading } from '@/components/loading'; | |||
import { defaultSetting } from '@/default-setting'; | |||
import { usePCScreen } from '@/hooks/use-pc-screen'; | |||
import { useGlobalStore } from '@/store/global'; | |||
import { FC, Suspense } from 'react'; | |||
const Content: FC<any> = ({ children }) => { | |||
const isPC = usePCScreen(); | |||
const { | |||
collapsed, | |||
} = useGlobalStore(); | |||
return ( | |||
<div | |||
className='color-transition mt-[80px] w-[100%] bg-container !<lg:ml-[16px]' | |||
style={{ | |||
borderRadius: '8px', | |||
marginLeft: collapsed ? 112 : defaultSetting.slideWidth, | |||
minHeight: 'calc(100vh - 80px)', | |||
transition: "all 200ms cubic-bezier(0.4, 0, 0.6, 1) 0ms", | |||
width: `calc(100vw - ${isPC ? collapsed ? 112 : defaultSetting.slideWidth : 32}px)` | |||
}} | |||
> | |||
<div | |||
className='m-0 rounded-md z-1 p-[0px]' | |||
> | |||
<Suspense | |||
fallback={( | |||
<Loading /> | |||
)} | |||
> | |||
{children} | |||
</Suspense> | |||
</div> | |||
</div> | |||
) | |||
} | |||
export default Content; |
@@ -0,0 +1,175 @@ | |||
import { memo } from 'react'; | |||
import { Avatar, Button, Dropdown, Input } from 'antd'; | |||
import { Icon3 } from '@/assets/icons/3'; | |||
import { IconBuguang } from '@/assets/icons/buguang'; | |||
import { IconFangdajing } from '@/assets/icons/fangdajing'; | |||
import { IconJiaretaiyang } from '@/assets/icons/jiaretaiyang'; | |||
import { IconShuyi_fanyi36 } from '@/assets/icons/shuyi_fanyi-36'; | |||
import { defaultSetting } from '@/default-setting'; | |||
import { useGlobalStore } from '@/store/global'; | |||
import { i18n, t } from '@/utils/i18n'; | |||
import { BellOutlined, MenuOutlined, SettingOutlined } from '@ant-design/icons'; | |||
import { useUserStore } from '@/store/global/user'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import loginService from '@/request/service/login'; | |||
const Header = () => { | |||
const { | |||
darkMode, | |||
collapsed, | |||
setCollapsed, | |||
setDarkMode, | |||
setLang, | |||
lang, | |||
} = useGlobalStore(); | |||
const { currentUser } = useUserStore(); | |||
const { runAsync } = useRequest(loginService.logout, { manual: true }); | |||
const logout = async () => { | |||
const [error] = await runAsync(); | |||
if (error) return; | |||
useGlobalStore.setState({ | |||
token: '', | |||
refreshToken: '' | |||
}); | |||
} | |||
return ( | |||
<div | |||
style={{ zIndex: 998 }} | |||
className="color-transition h-[80px] flex basis-[48px] items-center px-0 gap-[16px] fixed top-0 right-0 left-0 bg-primary" | |||
> | |||
<div style={{ width: defaultSetting.slideWidth }} className="<lg:hidden flex justify-between items-center"> | |||
<div className='flex items-center gap-[4px] text-[20px] px-[24px] pr-0'> | |||
<IconBuguang className="text-blue-500" /> | |||
<h1 className='text-primary font-bold text-[22px]'>VOGOCM-ERP</h1> | |||
</div> | |||
<div | |||
className='btn-icon' | |||
onClick={() => { | |||
setCollapsed(!collapsed); | |||
}} | |||
> | |||
<MenuOutlined /> | |||
</div> | |||
</div> | |||
<div className='flex items-center justify-between flex-1 pr-[24px]'> | |||
<Input | |||
style={{ | |||
borderRadius: 8, | |||
outline: 'none', | |||
boxShadow: 'none' | |||
}} | |||
className='w-[400px] h-[50px] focus:(border-[rgb(135,94,196)]) <lg:hidden' | |||
size="large" | |||
prefix={ | |||
<IconFangdajing | |||
style={{ | |||
color: '#697586', | |||
paddingRight: 8, | |||
}} | |||
/> | |||
} | |||
placeholder={t("jhqxJPbn" /* 搜索菜单 */)} | |||
allowClear | |||
/> | |||
<div className='pl-[20px] lg:hidden'> | |||
<div | |||
className='btn-icon' | |||
onClick={() => { | |||
setCollapsed(!collapsed); | |||
}} | |||
> | |||
<MenuOutlined /> | |||
</div> | |||
</div> | |||
<div className='flex gap-[16px] items-center'> | |||
<div onClick={() => { setDarkMode(!darkMode) }} className='btn-icon text-[20px]'> | |||
{darkMode ? ( | |||
<IconJiaretaiyang /> | |||
) : ( | |||
<Icon3 /> | |||
)} | |||
</div> | |||
<Dropdown | |||
menu={{ | |||
items: defaultSetting.languages.map(language => ({ | |||
label: `${t(language.name)} (${language.key.toUpperCase()})`, | |||
key: language.key, | |||
})), | |||
onClick: async ({ key }) => { | |||
await i18n.changeLanguage(key); | |||
setLang(key); | |||
} | |||
}} | |||
trigger={['click']} | |||
placement="bottom" | |||
overlayClassName='w-[160px]' | |||
> | |||
<div className='btn-icon text-[20px] bg-[rgb(227,242,253)] dark:text-[rgb(30,136,229)] text-[rgb(30,136,229)] hover:(bg-[rgb(33,150,243)] dark:text-[rgb(227,242,253)] text-[rgb(227,242,253)])'> | |||
{lang === 'zh' ? ( | |||
<IconShuyi_fanyi36 /> | |||
) : ( | |||
<span className='text-[14px]'> | |||
{lang.toUpperCase()} | |||
</span> | |||
)} | |||
</div> | |||
</Dropdown> | |||
<div className='btn-icon'> | |||
<BellOutlined /> | |||
</div> | |||
<Dropdown | |||
trigger={['click']} | |||
placement="bottomLeft" | |||
getPopupContainer={node => node.parentElement!} | |||
dropdownRender={() => { | |||
return ( | |||
<div | |||
style={{ | |||
boxShadow: darkMode ? | |||
'rgba(0, 0, 0, 0.2) 0px 8px 10px -5px, rgba(0, 0, 0, 0.14) 0px 16px 24px 2px, rgba(0, 0, 0, 0.12) 0px 6px 30px 5px' | |||
: 'rgba(0, 0, 0, 0.08) 0px 6px 30px', | |||
}} | |||
className='dark:bg-[rgb(33,41,70)] bg-white rounded-lg w-[200px]' | |||
> | |||
<div className='p-[16px]'> | |||
<p className='text-[16px] dark:text-[rgb(237,242,247)] text-[rgb(17,25,39)] '> | |||
{currentUser?.nickName} | |||
</p> | |||
<p className='text-[rgb(108,115,127)] dark:text-[rgb(160,174,192)] mt-[10px]'> | |||
{currentUser?.phoneNumber} | |||
</p> | |||
<p className='text-[rgb(108,115,127)] dark:text-[rgb(160,174,192)] mt-[0px]'> | |||
{currentUser?.email} | |||
</p> | |||
</div> | |||
<hr style={{ borderWidth: '0 0 thin' }} className='m-[0] border-solid dark:border-[rgb(45,55,72)] border-[rgb(242,244,247)]' /> | |||
<div className='p-[16px] text-center'> | |||
<Button onClick={logout} type='text' size='small'>退出登录</Button> | |||
</div> | |||
</div> | |||
) | |||
}} | |||
> | |||
<div className='btn-icon rounded-[27px] pl-[10px] pr-[14px] justify-between h-[48px] w-[92px] text-[20px] bg-[rgb(227,242,253)] text-[rgb(30,136,229)] hover:(bg-[rgb(33,150,243)] text-[rgb(227,242,253)])'> | |||
{currentUser?.avatarPath ? ( | |||
<Avatar style={{ verticalAlign: 'middle' }} src={currentUser.avatarPath} /> | |||
) : ( | |||
<Avatar style={{ backgroundColor: 'gold', verticalAlign: 'middle' }} icon={<IconBuguang />} /> | |||
)} | |||
<SettingOutlined /> | |||
</div> | |||
</Dropdown> | |||
</div> | |||
</div> | |||
</div> | |||
) | |||
} | |||
export default memo(Header); |
@@ -0,0 +1,24 @@ | |||
::-webkit-scrollbar-thumb { | |||
background: hsla(0, 0%, 52.9%, .4); | |||
border-radius: 4px; | |||
border: none; | |||
} | |||
.menu-slide::-webkit-scrollbar-thumb { | |||
background: transparent | |||
} | |||
.menu-slide:hover::-webkit-scrollbar-thumb { | |||
background: hsla(0, 0%, 52.9%, .4); | |||
} | |||
::-webkit-scrollbar { | |||
width: 6px; | |||
height: 6px; | |||
background-color: transparent; | |||
} | |||
::-webkit-scrollbar-track { | |||
background-color: transparent; | |||
} |
@@ -0,0 +1,168 @@ | |||
import { useLocation, useNavigate } from "react-router-dom" | |||
import { useGlobalStore } from '@/store/global'; | |||
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 { Menu } from '@/models'; | |||
// import { components } from '@/config/routes'; | |||
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'; | |||
const BasicLayout: React.FC = () => { | |||
const [loading, setLoading] = useState(true); | |||
const { refreshToken, lang, token } = useGlobalStore(); | |||
const { setCurrentUser, currentUser } = useUserStore(); | |||
const navigate = useNavigate(); | |||
const location = useLocation(); | |||
// const { setLatestMessage } = useMessageStore(); | |||
// const { | |||
// data: currentUserDetail, | |||
// run: getCurrentUserDetail, | |||
// } = useRequest( | |||
// userService.getCurrentUserDetail, | |||
// { manual: true } | |||
// ); | |||
const formatMenus = ( | |||
menus: Menu[], | |||
menuGroup: Record<string, Menu[]>, | |||
routes: Menu[], | |||
parentMenu?: Menu | |||
): Menu[] => { | |||
return menus.map(menu => { | |||
const children = menuGroup[menu.id]; | |||
const parentPaths = parentMenu?.parentPaths || []; | |||
const path = (parentMenu ? `${parentPaths.at(-1)}${menu.route}` : menu.route) || ''; | |||
routes.push({ ...menu, path, parentPaths }); | |||
return { | |||
...menu, | |||
path, | |||
parentPaths, | |||
children: children?.length ? formatMenus(children, menuGroup, routes, { | |||
...menu, | |||
parentPaths: [...parentPaths, path || ''].filter(o => o), | |||
}) : undefined, | |||
}; | |||
}); | |||
} | |||
// 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(() => { | |||
function storageChange(e: StorageEvent) { | |||
if (e.key === useGlobalStore.persist.getOptions().name) { | |||
useGlobalStore.persist.rehydrate(); | |||
} | |||
} | |||
window.addEventListener<'storage'>('storage', storageChange); | |||
return () => { | |||
window.removeEventListener<'storage'>('storage', storageChange); | |||
} | |||
}, []); | |||
setTimeout(()=>{ | |||
setLoading(false); | |||
}, 3000) | |||
if (loading) { | |||
return ( | |||
<GloablLoading /> | |||
) | |||
} | |||
return ( | |||
<div> | |||
<div key={lang} className='bg-primary overflow-hidden'> | |||
<Header /> | |||
<Slide /> | |||
<Content> | |||
<TabsLayout /> | |||
</Content> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
export default BasicLayout; |
@@ -0,0 +1,77 @@ | |||
import { memo } from 'react'; | |||
import { Drawer } from 'antd'; | |||
import { useUpdateEffect } from 'react-use'; | |||
import { IconBuguang } from '@/assets/icons/buguang'; | |||
import { useGlobalStore } from '@/store/global'; | |||
import { usePCScreen } from '@/hooks/use-pc-screen'; | |||
import { defaultSetting } from '@/default-setting'; | |||
import SlideMenu from './menus'; | |||
const SlideIndex = () => { | |||
const isPC = usePCScreen(); | |||
const { | |||
collapsed, | |||
setCollapsed, | |||
} = useGlobalStore(); | |||
useUpdateEffect(() => { | |||
if (!isPC) { | |||
setCollapsed(true); | |||
} else { | |||
setCollapsed(false); | |||
} | |||
}, [isPC]); | |||
function renderMenu() { | |||
return ( | |||
<SlideMenu /> | |||
) | |||
} | |||
if (!isPC) { | |||
return ( | |||
<Drawer | |||
open={!collapsed} | |||
footer={null} | |||
placement="left" | |||
width={defaultSetting.slideWidth} | |||
className="bg-primary" | |||
zIndex={10001} | |||
closable={false} | |||
title={( | |||
<div | |||
className='flex items-center gap-[4px] text-[20px] justify-center' | |||
style={{ width: defaultSetting.slideWidth }} | |||
> | |||
<IconBuguang className="text-blue-500" /> | |||
<h1 className='text-primary font-bold text-[22px]'>fluxy-admin</h1> | |||
</div> | |||
)} | |||
headerStyle={{ padding: '24px 0', border: 'none' }} | |||
bodyStyle={{ padding: '0 16px' }} | |||
onClose={() => { | |||
setCollapsed(true); | |||
}} | |||
> | |||
{renderMenu()} | |||
</Drawer> | |||
) | |||
} | |||
return ( | |||
<div | |||
style={{ width: collapsed ? 112 : defaultSetting.slideWidth }} | |||
className="menu-slide color-transition top-[80px] fixed box-border left-0 bottom-0 overflow-y-auto px-[16px] bg-primary <lg:hidden" | |||
> | |||
{renderMenu()} | |||
</div> | |||
) | |||
} | |||
export default memo(SlideIndex); |
@@ -0,0 +1,311 @@ | |||
import React, { useCallback, useEffect, useState } 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 { | |||
AccountBookOutlined, | |||
AppstoreOutlined, | |||
BarChartOutlined, | |||
BgColorsOutlined, | |||
CustomerServiceOutlined, | |||
DeploymentUnitOutlined, | |||
GlobalOutlined, | |||
HighlightOutlined, | |||
HomeOutlined, | |||
InboxOutlined, | |||
SettingOutlined, | |||
ShopOutlined, | |||
} from '@ant-design/icons'; | |||
import type { MenuProps, MenuTheme } from 'antd/es/menu'; | |||
type MenuItem = Required<MenuProps>['items'][number]; | |||
function getItem( | |||
label: React.ReactNode, | |||
key: React.Key, | |||
icon?: React.ReactNode, | |||
children?: MenuItem[], | |||
type?: 'group', | |||
): MenuItem { | |||
return { | |||
key, | |||
icon, | |||
children, | |||
label, | |||
type | |||
} as MenuItem; | |||
} | |||
const items: MenuItem[] = [ | |||
getItem('首页', 'home', <HomeOutlined />), | |||
getItem('定制选品', 'custom-made', <BgColorsOutlined />, [ | |||
getItem('定制商品', 'custom-product', null, [ | |||
getItem('样机', 'sample'), | |||
getItem('素材', 'material'), | |||
getItem('款式', 'shape'), | |||
getItem('成品', 'finished-product'), | |||
], 'group'), | |||
getItem('模板配置', 'template', null, [ | |||
getItem('数据字典', 'dict'), | |||
getItem('虾皮模板', 'xp-template'), | |||
getItem('规则引擎', 'rules') | |||
], 'group'), | |||
getItem('平台商品', 'platform-product', null, [ | |||
getItem('虾皮', 'xp'), | |||
getItem('亚马逊', 'amazone'), | |||
], 'group'), | |||
getItem('SDS商品', 'sds', null, [ | |||
getItem('成品库', 'finished-product-warehouse'), | |||
getItem('款式', 'sds-shape'), | |||
getItem('图案素材', 'pattern-material'), | |||
], 'group'), | |||
]), | |||
getItem('AI应用', 'ai', <HighlightOutlined />, [ | |||
getItem('AI 作图', 'ai-picture'), | |||
getItem('AI 画背景', 'ai-background'), | |||
getItem('图片裂变', 'picture-split'), | |||
getItem('轮廓出图', 'outline-drawing'), | |||
getItem('一键白底', 'white-background'), | |||
getItem('AI P图', 'ai-ps'), | |||
getItem('AI 试装', 'ai-try'), | |||
getItem('AI 作图2', 'ai-makeup'), | |||
getItem('旺嘉智库', 'vogcom-libs'), | |||
]), | |||
getItem('商品', 'product', <AppstoreOutlined />, [ | |||
getItem('主库', 'main-db', null, [ | |||
getItem('商品列表', 'product-list'), | |||
getItem('类目', 'category'), | |||
getItem('商品属性设置', 'product-attr-settings'), | |||
getItem('关键词组', 'keywords'), | |||
getItem('子属性sku绑定', 'sub-sku-binding'), | |||
getItem('spu商品合成', 'spu-product-compound'), | |||
getItem('海外商品管理', 'overseas-product'), | |||
], 'group'), | |||
getItem('平台库', 'platform-db', null, [ | |||
getItem('敦煌商品列表', 'dh-product-list'), | |||
getItem('速卖通商品列表', 'smt-product-list'), | |||
], 'group'), | |||
getItem('子库', 'sub-db', null, [ | |||
getItem('敦煌商品列表', 'sub-dh-product-list'), | |||
getItem('速卖通商品列表', 'sub-smt-product-list'), | |||
], 'group'), | |||
]), | |||
getItem('订单', 'order', <ShopOutlined />, [ | |||
getItem('订单管理', 'order-manager', null, [ | |||
getItem('订单列表', 'order-list'), | |||
getItem('合并包裹', 'merge-package'), | |||
], 'group'), | |||
getItem('订单异常', 'order-exception', null, [ | |||
getItem('物流异常', 'logistics-anomalies'), | |||
getItem('同步异常', 'sync-exception'), | |||
getItem('订单同步日志', 'order-sync-logs'), | |||
], 'group'), | |||
getItem('RAM管理', 'ram-manager', null, [ | |||
getItem('纠纷订单', 'dispute-order'), | |||
getItem('退款&退货', 'refund-return'), | |||
], 'group'), | |||
]), | |||
getItem('仓库', 'warehouse', <InboxOutlined />, [ | |||
getItem('仓库管理', 'warehouse-manage', null, [ | |||
getItem('库存列表', 'stock-list'), | |||
getItem('仓库列表', 'warehouse-list'), | |||
getItem('篮子管理', 'box-manager'), | |||
getItem('盘点计划管理', 'inventory-plan') | |||
], 'group'), | |||
getItem('包裹', 'packages', null, [ | |||
getItem('包裹列表', 'package-list'), | |||
getItem('重打面单', 're-print'), | |||
getItem('包裹退回清单', 'returned-package-list'), | |||
getItem('包裹扫描换单', 'scan-parcels-exchange-orders'), | |||
getItem('物流汇总报表', 'logistics-summary-report'), | |||
getItem('转单号批量更换', 'batch-replacement-transfer-order-no'), | |||
getItem('包裹换单', 'exchange-pkg'), | |||
getItem('包裹退单', 'return-pkg'), | |||
], 'group'), | |||
getItem('快速出库', 'rapid-exit', null, [ | |||
getItem('配货', 'distribution'), | |||
getItem('单sku复核打包', 'sku-review-packaging'), | |||
getItem('多sku复核', 'multi-sku-review'), | |||
getItem('多sku打包', 'multi-sku-packaging'), | |||
getItem('出库', 'export') | |||
], 'group'), | |||
getItem('柔性定制', 'flexible-customization', null, [ | |||
getItem('柔性定制', 'customization'), | |||
getItem('一次复核', 'first-review'), | |||
getItem('二次复核', 'second-review'), | |||
getItem('印花', 'printing'), | |||
getItem('扫描入库', 'scan-into-warehouse'), | |||
getItem('异常素材补打', 'abnormal-material-reprinting') | |||
], 'group'), | |||
getItem('入库', 'sub-db', null, [ | |||
getItem('签收', 'sign'), | |||
getItem('入库日志管理', 'inbound-log-management') | |||
], 'group'), | |||
]), | |||
getItem('供应链', 'supply-chain', <DeploymentUnitOutlined />, [ | |||
getItem('采购流程', 'main-db', null, [ | |||
getItem('缺货采购列表', 'out-of-stock-purchase-list'), | |||
getItem('采购管理', 'purchase-management'), | |||
getItem('备货管理', 'stocking-management'), | |||
getItem('待申请付款', 'payment-pending'), | |||
getItem('非常规采购入库', 'unconventional-procurement') | |||
], 'group'), | |||
getItem('退货流程', 'return-process', null, [ | |||
getItem('退货单', 'return-form'), | |||
getItem('滞留品列表', 'retained-goods-list'), | |||
], 'group'), | |||
getItem('供应商', 'supplier', null, [ | |||
getItem('供应商品列表', 'supply-product-list'), | |||
getItem('供应商商品信息', 'supply-product-information') | |||
], 'group'), | |||
]), | |||
getItem('物流', 'logistics', <GlobalOutlined />, [ | |||
getItem('物流管理', 'logistics-management', null, [ | |||
getItem('物流管理', 'logistics-management1'), | |||
getItem('物流服务商', 'logistics-server'), | |||
getItem('回邮地址', 'email-address'), | |||
getItem('寄件地址', 'post-address'), | |||
getItem('包材管理', 'package-material-management') | |||
], 'group'), | |||
getItem('物流费用', 'platform-db', null, [ | |||
getItem('包裹物流费用', 'package-logistics-fee'), | |||
getItem('物流资费计算', 'logistics-fee-calc'), | |||
getItem('物流运费模板管理', 'logistics-fee-template-management'), | |||
], 'group'), | |||
]), | |||
getItem('客服', 'service', <CustomerServiceOutlined />, [ | |||
getItem('客户管理', 'customer-management', null, [ | |||
getItem('客户列表', 'customer-list'), | |||
getItem('客户类型列表', 'customer-type-list'), | |||
getItem('推广客户列表', 'promotion-customer-list') | |||
], 'group'), | |||
getItem('客诉任务管理', 'customer-complaint-task-management', null, [ | |||
getItem('敦煌客诉管理', 'dh-customer-complaint') | |||
], 'group'), | |||
]), | |||
getItem('财务', 'financial', <AccountBookOutlined />, [ | |||
getItem('账户', 'account', null, [ | |||
getItem('账户管理', 'account-management'), | |||
getItem('商户充值', 'merchant-recharge'), | |||
getItem('商户流水管理', 'merchant-flow-management'), | |||
], 'group'), | |||
getItem('订单', 'platform-db', null, [ | |||
getItem('订单流水', 'order-flow'), | |||
], 'group'), | |||
getItem('供应处理', 'supply-management', null, [ | |||
getItem('采购付款', 'purchase-payment'), | |||
getItem('供应商商品信息', 'supplier-product-info'), | |||
getItem('退货单审核', 'return-list-review'), | |||
getItem('备货单审核', 'stocking-list-review') | |||
], 'group'), | |||
]), | |||
getItem('报表', 'report', <BarChartOutlined />, [ | |||
getItem('销售报表', 'sale-report', null, [ | |||
getItem('收支报表', 'income-report'), | |||
getItem('订单状态报表', 'order-status-report'), | |||
getItem('订单状态报表2', 'order-status-report-2'), | |||
], 'group'), | |||
getItem('物流报表', 'logistics-report', null, [ | |||
getItem('质检报表', 'quality-inspection-report'), | |||
getItem('发货报表', 'shipping-report') | |||
], 'group'), | |||
]), | |||
getItem('基础配置', 'settings', <SettingOutlined />, [ | |||
getItem('基础配置', 'basic-settings', null, [ | |||
getItem('组织权限', 'organization-permissions'), | |||
getItem('图片空间', 'pic-space'), | |||
getItem('平台内部消息', 'platform-message'), | |||
getItem('汇率管理', 'exchange-rate-manager'), | |||
getItem('国家管理', 'country-manager'), | |||
], 'group'), | |||
getItem('商户&店铺', 'retailer', null, [ | |||
getItem('商铺管理', 'retailer-manager'), | |||
getItem('商户信息', 'merchant-information'), | |||
getItem('商户等级', 'merchant-level') | |||
], 'group'), | |||
]), | |||
]; | |||
const SlideMenu = () => { | |||
const matches = useMatches(); | |||
const [openKeys, setOpenKeys] = useState<string[]>([]); | |||
const [selectKeys, setSelectKeys] = useState<string[]>([]); | |||
const { | |||
collapsed, | |||
} = useGlobalStore(); | |||
useEffect(() => { | |||
if (collapsed) { | |||
setOpenKeys([]); | |||
} else { | |||
const [match] = matches || []; | |||
if (match) { | |||
// 获取当前匹配的路由,默认为最后一个 | |||
const route = matches.at(-1); | |||
// 从匹配的路由中取出自定义参数 | |||
const handle = route?.handle as any; | |||
// 从自定义参数中取出上级path,让菜单自动展开 | |||
setOpenKeys(handle?.parentPaths || []); | |||
// 让当前菜单和所有上级菜单高亮显示 | |||
setSelectKeys([...(handle?.parentPaths || []), handle?.path] || []); | |||
} | |||
} | |||
}, [ | |||
matches, | |||
collapsed, | |||
]); | |||
const getMenuTitle = (menu: MenuType) => { | |||
if (menu?.children?.filter(menu => menu.show)?.length) { | |||
return menu.name; | |||
} | |||
return ( | |||
<Link to={menu.path}>{menu.name}</Link> | |||
); | |||
} | |||
const treeMenuData = useCallback((menus: MenuType[]): ItemType[] => { | |||
return (menus) | |||
.map((menu: MenuType) => { | |||
const children = menu?.children?.filter(menu => menu.show) || []; | |||
return { | |||
key: menu.path, | |||
label: getMenuTitle(menu), | |||
icon: menu.icon && antdIcons[menu.icon] && React.createElement(antdIcons[menu.icon]), | |||
children: children.length ? treeMenuData(children || []) : null, | |||
}; | |||
}) | |||
}, []); | |||
return ( | |||
<Menu | |||
className='bg-primary color-transition' | |||
mode="inline" | |||
selectedKeys={selectKeys} | |||
style={{ height: '100%', borderRight: 0 }} | |||
items={items} | |||
inlineCollapsed={collapsed} | |||
openKeys={openKeys} | |||
onOpenChange={setOpenKeys} | |||
/> | |||
) | |||
} | |||
export default SlideMenu; |
@@ -0,0 +1,18 @@ | |||
/* eslint-disable @typescript-eslint/no-empty-function */ | |||
import { createContext } from 'react' | |||
interface KeepAliveTabContextType { | |||
refreshTab: (path?: string) => void; | |||
closeTab: (path?: string) => void; | |||
closeOtherTab: (path?: string) => void; | |||
} | |||
const defaultValue = { | |||
refreshTab: () => { }, | |||
closeTab: () => { }, | |||
closeOtherTab: () => { }, | |||
} | |||
export const KeepAliveTabContext = createContext<KeepAliveTabContextType>(defaultValue); |
@@ -0,0 +1,124 @@ | |||
import React, { useCallback, useMemo } from "react"; | |||
import { Dropdown } from 'antd'; | |||
import { antdIcons } from '@/assets/antd-icons'; | |||
import { KeepAliveTab, useTabs } from '@/hooks/use-tabs'; | |||
import { router } from '@/router/router'; | |||
import type { MenuItemType } from 'antd/es/menu/hooks/useItems'; | |||
import { KeepAliveTabContext } from './tabs-context'; | |||
import DraggableTab from '@/components/draggable-tab'; | |||
enum OperationType { | |||
REFRESH = 'refresh', | |||
CLOSE = 'close', | |||
CLOSEOTHER = 'close-other', | |||
} | |||
const TabsLayout: React.FC = () => { | |||
const { activeTabRoutePath, tabs, closeTab, refreshTab, closeOtherTab } = useTabs(); | |||
const getIcon = (icon?: string): React.ReactElement | undefined => { | |||
return icon && antdIcons[icon] && React.createElement(antdIcons[icon]); | |||
} | |||
const menuItems: MenuItemType[] = useMemo( | |||
() => [ | |||
{ | |||
label: '刷新', | |||
key: OperationType.REFRESH, | |||
}, | |||
tabs.length <= 1 ? null : { | |||
label: '关闭', | |||
key: OperationType.CLOSE, | |||
}, | |||
tabs.length <= 1 ? null : { | |||
label: '关闭其他', | |||
key: OperationType.CLOSEOTHER, | |||
}, | |||
].filter(o => o !== null) as MenuItemType[], | |||
[tabs] | |||
); | |||
const menuClick = useCallback(({ key, domEvent }: any, tab: KeepAliveTab) => { | |||
domEvent.stopPropagation(); | |||
if (key === OperationType.REFRESH) { | |||
refreshTab(tab.routePath); | |||
} else if (key === OperationType.CLOSE) { | |||
closeTab(tab.routePath); | |||
} else if (key === OperationType.CLOSEOTHER) { | |||
closeOtherTab(tab.routePath); | |||
} | |||
}, [closeOtherTab, closeTab, refreshTab]); | |||
const renderTabTitle = useCallback((tab: KeepAliveTab) => { | |||
return ( | |||
<Dropdown | |||
menu={{ items: menuItems, onClick: (e) => menuClick(e, tab) }} | |||
trigger={['contextMenu']} | |||
> | |||
<div style={{ margin: '-12px 0', padding: '12px 0' }}> | |||
{getIcon(tab.icon)} | |||
{tab.title} | |||
</div> | |||
</Dropdown> | |||
) | |||
}, [menuItems]); | |||
const tabItems = useMemo(() => { | |||
return tabs.map(tab => { | |||
return { | |||
key: tab.routePath, | |||
label: renderTabTitle(tab), | |||
children: ( | |||
<div | |||
key={tab.key} | |||
className='px-[16px]' | |||
> | |||
{tab.children} | |||
</div> | |||
), | |||
closable: tabs.length > 1, // 剩最后一个就不能删除了 | |||
} | |||
}) | |||
}, [tabs]); | |||
const onTabsChange = useCallback((tabRoutePath: string) => { | |||
router.navigate(tabRoutePath); | |||
}, []); | |||
const onTabEdit = ( | |||
targetKey: React.MouseEvent | React.KeyboardEvent | string, | |||
action: 'add' | 'remove', | |||
) => { | |||
if (action === 'remove') { | |||
closeTab(targetKey as string); | |||
} | |||
}; | |||
const keepAliveContextValue = useMemo( | |||
() => ({ | |||
closeTab, | |||
closeOtherTab, | |||
refreshTab, | |||
}), | |||
[closeTab, closeOtherTab, refreshTab] | |||
); | |||
return ( | |||
<KeepAliveTabContext.Provider value={keepAliveContextValue}> | |||
<DraggableTab | |||
activeKey={activeTabRoutePath} | |||
items={tabItems} | |||
type="editable-card" | |||
onChange={onTabsChange} | |||
hideAdd | |||
onEdit={onTabEdit} | |||
size="small" | |||
/> | |||
</KeepAliveTabContext.Provider> | |||
) | |||
} | |||
export default TabsLayout; |
@@ -1,10 +1,18 @@ | |||
import React from 'react' | |||
import ReactDOM from 'react-dom/client' | |||
import App from './App.tsx' | |||
import './index.css' | |||
import ReactDOM from 'react-dom/client'; | |||
import NProgress from 'nprogress'; | |||
import App from './App'; | |||
import 'virtual:windi.css'; | |||
import 'nprogress/nprogress.css'; | |||
import '@/assets/css/overwrite.css'; | |||
ReactDOM.createRoot(document.getElementById('root')!).render( | |||
<React.StrictMode> | |||
<App /> | |||
</React.StrictMode>, | |||
NProgress.configure({ | |||
minimum: 0.3, | |||
easing: 'ease', | |||
speed: 800, | |||
showSpinner: false, | |||
parent: '#root' | |||
}); | |||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( | |||
<App /> | |||
) |
@@ -0,0 +1,6 @@ | |||
export * from './user.ts' | |||
export interface PageData<T> { | |||
data: T[]; | |||
total: number; | |||
} |
@@ -0,0 +1,108 @@ | |||
export interface LoginDTO { | |||
userName: string; | |||
password: string; | |||
} | |||
export interface TokenDTO { | |||
expire: number; | |||
token: string; | |||
refreshExpire: number; | |||
refreshToken: string; | |||
} | |||
export interface RoleDTO { | |||
id: number; | |||
roleName: string; | |||
status: number; | |||
createTime: string; | |||
createOper: string; | |||
operTime: string; | |||
oper: string; | |||
} | |||
export interface MerchantDTO { | |||
id: number; | |||
merchantName: string; | |||
levelId: number; | |||
merchantStatus: number; | |||
balance: number; | |||
frozenBalance: number; | |||
integral: number; | |||
relationName: string; | |||
phone: string; | |||
address: string; | |||
remark: string; | |||
creatorId: number; | |||
createTime: string; | |||
isDelete: number; | |||
} | |||
export interface UserDTO { | |||
id: number; | |||
userName: string; | |||
password: string; | |||
phoneNumber: string; | |||
emailAddress: string; | |||
name: string; | |||
avatarId: number; | |||
status: number; | |||
isDelete: number; | |||
merchantId: number; | |||
roles: Array<RoleDTO>; | |||
avatarUrl: string; | |||
merchant: MerchantDTO; | |||
idToString: string; | |||
} | |||
export interface PopMenu { | |||
id: number; | |||
parentid: number; | |||
homeid: number; | |||
menuName: string; | |||
parentMenuName: string; | |||
pageUrl: string; | |||
sort: number; | |||
level: number; | |||
} | |||
export interface LoginRespDTO { | |||
ack: number; | |||
data: UserDTO; | |||
msg: string; | |||
pop: Array<PopMenu>; | |||
} | |||
export interface Menu { | |||
id: string; | |||
parentId?: string; | |||
name?: string; | |||
icon?: string; | |||
type?: number; | |||
route?: string; | |||
filePath?: string; | |||
orderNumber?: number; | |||
url?: string; | |||
show?: boolean; | |||
children?: Menu[]; | |||
path: string; | |||
Component?: any; | |||
parentPaths?: string[]; | |||
authCode?: string; | |||
} | |||
export interface User { | |||
id: number; | |||
userName: string; | |||
nickName: string; | |||
phoneNumber: string; | |||
email: string; | |||
createDate: string; | |||
updateDate: string; | |||
avatar?: any; | |||
menus: Menu[]; | |||
routes: any[]; | |||
flatMenus: Menu[]; | |||
avatarPath: string; | |||
authList: string[]; | |||
} |
@@ -0,0 +1,43 @@ | |||
.img1 { | |||
animation: img1-anim 10s linear 0ms infinite normal backwards; | |||
} | |||
.img2 { | |||
animation: img2-anim 8s linear 0ms infinite normal backwards; | |||
} | |||
@keyframes img1-anim { | |||
0% { | |||
transform: translate3d(0, 0, 0); | |||
} | |||
50% { | |||
transform: translate3d(0px, 30px, 0) | |||
} | |||
100% { | |||
transform: translate3d(0px, 0px, 0) | |||
} | |||
} | |||
@keyframes img2-anim { | |||
0% { | |||
transform: translate3d(0px, 0px, 0) | |||
} | |||
50% { | |||
transform: translate3d(0px, 20px, 0) | |||
} | |||
100% { | |||
transform: translate3d(0px, 0px, 0) | |||
} | |||
} | |||
.custom.slick-dots .slick-active button { | |||
background: #000 !important; | |||
} | |||
.custom.slick-dots button { | |||
background: rgba(0, 0, 0, .7) !important; | |||
} |
@@ -0,0 +1,148 @@ | |||
import { t } from '@/utils/i18n'; | |||
import { IconBuguang } from '@/assets/icons/buguang' | |||
import { LockOutlined, UserOutlined } from '@ant-design/icons'; | |||
import { Button, Form, Input, Carousel } from 'antd'; | |||
import { useNavigate } from 'react-router-dom'; | |||
import './index.css' | |||
const Login = () => { | |||
const navigate = useNavigate(); | |||
const onFinish = async () => { | |||
navigate('/'); | |||
}; | |||
return ( | |||
<div className="bg-primary light:bg-[rgb(238,242,246)] bg-[rgb(238,242,246)] flex justify-center items-center h-[100vh]"> | |||
<div className='flex-[2.5] flex justify-center'> | |||
<div className='dark:bg-[rgb(33,41,70)] w-[400px] px-[32px] py-[20px] mt-[-12%] bg-white rounded-lg <lg:(w-[94%] mx-auto)'> | |||
<div className='text-center'> | |||
<div className='flex justify-center gap-2'> | |||
<IconBuguang className='text-[20px] text-blue-500' /> | |||
<h1 className='dark:(text-white) ' style={{ marginBottom: '0.2em' }}> 旺嘉-ERP Admin</h1> | |||
</div> | |||
<h3 className='dark:(text-white) text-[rgba(0,0,0,.45)] mb-[1em] text-[14px] font-normal'> | |||
{t("wbTMzvDM" /* 一个高颜值后台管理系统 */)} | |||
</h3> | |||
</div> | |||
<Form | |||
name="super-admin" | |||
className="login-form" | |||
initialValues={{ userName: 'admin', password: '1' }} | |||
onFinish={onFinish} | |||
size="large" | |||
> | |||
<Form.Item | |||
name="userName" | |||
rules={[{ required: true, message: t("wVzXBuYs" /* 请输入账号 */) }]} | |||
> | |||
<Input | |||
prefix={<UserOutlined className="site-form-item-icon" />} | |||
placeholder={t("RNISycbR" /* 账号 */)} | |||
size="large" | |||
/> | |||
</Form.Item> | |||
<Form.Item | |||
name="password" | |||
rules={[{ required: true, message: t("DjMcEMAe" /* 请输入密码 */) }]} | |||
> | |||
<Input | |||
prefix={<LockOutlined className="site-form-item-icon" />} | |||
type="password" | |||
placeholder={t("HplkKxdY" /* 密码 */)} | |||
/> | |||
</Form.Item> | |||
<Form.Item noStyle style={{ marginBottom: 0 }} > | |||
<div | |||
className='text-right mb-[18px]' | |||
> | |||
<a | |||
onClick={() => { | |||
}} | |||
className='text-[16px] !text-[rgb(124,77,255)] select-none' | |||
type='link' | |||
> | |||
忘记密码? | |||
</a> | |||
</div> | |||
</Form.Item> | |||
<Form.Item style={{ marginBottom: 18 }}> | |||
<Button | |||
type="primary" | |||
loading={false} | |||
block | |||
htmlType='submit' | |||
> | |||
{t("dDdqAAve" /* 登录 */)} | |||
</Button> | |||
</Form.Item> | |||
</Form> | |||
</div> | |||
</div> | |||
<div | |||
className='flex-[1.7] dark:bg-[rgb(33,41,70)] bg-white h-[100vh] relative <lg:hidden' | |||
style={{ | |||
backgroundImage: 'url(/images/login-right-bg.svg)' | |||
}} | |||
> | |||
<div | |||
className='img1 w-[243px] h-[210px] bg-center absolute top-[23%] left-[37%]' | |||
style={{ | |||
backgroundSize: 380, | |||
backgroundImage: 'url(/images/login-right-before.svg)' | |||
}} | |||
/> | |||
<div | |||
className='img2 w-[313px] h-[280px] bg-center absolute top-[32%] left-[40%]' | |||
style={{ | |||
backgroundSize: 380, | |||
backgroundImage: 'url(/images/login-right-after.svg)' | |||
}} | |||
/> | |||
<div className='absolute left-[100px] right-[100px] bottom-[50px] h-[200px]'> | |||
<Carousel autoplay dots={{ className: 'custom' }}> | |||
<div> | |||
<div className='h-[160px] bg-transparent flex items-center justify-center'> | |||
<div> | |||
<h3 className='dark:text-[rgb(215,220,236)] text-[rgb(18,25,38)] text-[34px]'> | |||
fluxy-admin | |||
</h3> | |||
<div className='dark:text-[rgb(132,146,196)] text-[rgb(105,117,134)] text-[12px] my-[20px] '> | |||
一个高颜值后台管理系统 | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div> | |||
<div className='h-[160px] bg-transparent flex items-center justify-center'> | |||
<div> | |||
<h3 className='dark:text-[rgb(215,220,236)] text-[rgb(18,25,38)] text-[34px]'> | |||
fluxy-admin | |||
</h3> | |||
<div className='dark:text-[rgb(132,146,196)] text-[rgb(105,117,134)] text-[12px] my-[20px]'> | |||
一个高颜值后台管理系统 | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div> | |||
<div className='h-[160px] bg-transparent flex items-center justify-center'> | |||
<div> | |||
<h3 className='dark:text-[rgb(215,220,236)] text-[rgb(18,25,38)] text-[34px]'> | |||
fluxy-admin | |||
</h3> | |||
<div className='dark:text-[rgb(132,146,196)] text-[rgb(105,117,134)] text-[12px] my-[20px] '> | |||
一个高颜值后台管理系统 | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</Carousel> | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
export default Login; |
@@ -0,0 +1,176 @@ | |||
import axios, { | |||
AxiosInstance, | |||
AxiosRequestConfig, | |||
AxiosResponse, | |||
CreateAxiosDefaults, | |||
InternalAxiosRequestConfig, | |||
} from 'axios'; | |||
import {useGlobalStore} from '@/store/global'; | |||
import {antdUtils} from '@/utils/antd'; | |||
const refreshTokenUrl = '/api/auth/refresh/token'; | |||
export type Response<T> = Promise<[boolean, T, AxiosResponse<T>]>; | |||
class Request { | |||
constructor(config?: CreateAxiosDefaults) { | |||
this.axiosInstance = axios.create(config); | |||
this.axiosInstance.interceptors.request.use( | |||
(axiosConfig: InternalAxiosRequestConfig) => | |||
this.requestInterceptor(axiosConfig) | |||
); | |||
this.axiosInstance.interceptors.response.use( | |||
(response: AxiosResponse<unknown, unknown>) => | |||
this.responseSuccessInterceptor(response), | |||
(error: any) => this.responseErrorInterceptor(error) | |||
); | |||
} | |||
private axiosInstance: AxiosInstance; | |||
private refreshTokenFlag = false; | |||
private requestQueue: { | |||
resolve: any; | |||
config: any; | |||
type: 'reuqest' | 'response'; | |||
}[] = []; | |||
private limit = 3; | |||
private requestingCount = 0; | |||
setLimit(limit: number) { | |||
this.limit = limit; | |||
} | |||
private async requestInterceptor( | |||
axiosConfig: InternalAxiosRequestConfig | |||
): Promise<any> { | |||
if ([refreshTokenUrl].includes(axiosConfig.url || '')) { | |||
return Promise.resolve(axiosConfig); | |||
} | |||
if (this.refreshTokenFlag || this.requestingCount >= this.limit) { | |||
return new Promise((resolve) => { | |||
this.requestQueue.push({ | |||
resolve, | |||
config: axiosConfig, | |||
type: 'reuqest', | |||
}); | |||
}); | |||
} | |||
this.requestingCount += 1; | |||
const {token} = useGlobalStore.getState(); | |||
if (token) { | |||
axiosConfig.headers.Authorization = `Bearer ${token}`; | |||
} | |||
return Promise.resolve(axiosConfig); | |||
} | |||
private requestByQueue() { | |||
if (!this.requestQueue.length) return; | |||
console.log( | |||
this.requestingCount, | |||
this.limit - this.requestingCount, | |||
'count' | |||
); | |||
Array.from({length: this.limit - this.requestingCount}).forEach( | |||
async () => { | |||
const record = this.requestQueue.shift(); | |||
if (!record) { | |||
return; | |||
} | |||
const {config, resolve, type} = record; | |||
if (type === 'response') { | |||
resolve(await this.request(config)); | |||
} else if (type === 'reuqest') { | |||
this.requestingCount += 1; | |||
const {token} = useGlobalStore.getState(); | |||
config.headers.Authorization = `Bearer ${token}`; | |||
resolve(config); | |||
} | |||
} | |||
); | |||
} | |||
private async responseSuccessInterceptor( | |||
response: AxiosResponse<any, any> | |||
): Promise<any> { | |||
if (response.config.url !== refreshTokenUrl) { | |||
this.requestingCount -= 1; | |||
if (this.requestQueue.length) { | |||
this.requestByQueue(); | |||
} | |||
} | |||
return Promise.resolve([false, response.data, response]); | |||
} | |||
private async responseErrorInterceptor(error: any): Promise<any> { | |||
this.requestingCount -= 1; | |||
const {config, status} = error?.response || {}; | |||
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({ | |||
message: '出错了', | |||
description: error?.response?.data?.message, | |||
}); | |||
return Promise.resolve([true, error?.response?.data]); | |||
} | |||
} | |||
private reset() { | |||
this.requestQueue = []; | |||
this.refreshTokenFlag = false; | |||
this.requestingCount = 0; | |||
} | |||
private toLoginPage() { | |||
this.reset(); | |||
} | |||
request<T, D = any>(config: AxiosRequestConfig<D>): Response<T> { | |||
return this.axiosInstance(config); | |||
} | |||
get<T, D = any>(url: string, config?: AxiosRequestConfig<D>): Response<T> { | |||
return this.axiosInstance.get(url, config); | |||
} | |||
post<T, D = any>( | |||
url: string, | |||
data?: D, | |||
config?: AxiosRequestConfig<D> | |||
): Response<T> { | |||
return this.axiosInstance.post(url, data, config); | |||
} | |||
put<T, D = any>( | |||
url: string, | |||
data?: D, | |||
config?: AxiosRequestConfig<D> | |||
): Response<T> { | |||
return this.axiosInstance.put(url, data, config); | |||
} | |||
delete<T, D = any>(url: string, config?: AxiosRequestConfig<D>): Response<T> { | |||
return this.axiosInstance.delete(url, config); | |||
} | |||
} | |||
const request = new Request({timeout: 60 * 1000 * 5, baseURL: 'https://test.vogocm.com:9697'}); | |||
export default request; |
@@ -0,0 +1,15 @@ | |||
import request from '@/request'; | |||
import { LoginDTO, LoginRespDTO } from '@/models' | |||
const loginService = { | |||
// 登录 | |||
login: (loginDTO: LoginDTO) => { | |||
return request.post<LoginRespDTO>('/api/login', loginDTO); | |||
}, | |||
logout: () => { | |||
return request.get<LoginRespDTO>('/api/logout'); | |||
} | |||
}; | |||
export default loginService; |
@@ -0,0 +1,5 @@ | |||
const userService = { | |||
}; | |||
export default userService; |
@@ -0,0 +1,8 @@ | |||
import { useRouteError } from 'react-router-dom'; | |||
const RouterErrorElement = () => { | |||
const error = useRouteError(); | |||
throw error; | |||
} | |||
export default RouterErrorElement; |
@@ -0,0 +1,89 @@ | |||
import { RouteObject, RouterProvider, createBrowserRouter, Navigate } from 'react-router-dom'; | |||
import Login from '@/pages/login'; | |||
import BasicLayout from '@/layout'; | |||
import { App } from 'antd'; | |||
import { useEffect } from 'react'; | |||
import { antdUtils } from '@/utils/antd'; | |||
import RouterErrorElement from './router-error-element'; | |||
export const router = createBrowserRouter( | |||
[ | |||
{ | |||
path: '/login', | |||
Component: Login, | |||
}, | |||
{ | |||
path: '/', | |||
element: ( | |||
<Navigate to="/dashboard" /> | |||
), | |||
}, | |||
{ | |||
path: '*', | |||
Component: BasicLayout, | |||
children: [], | |||
errorElement: <RouterErrorElement /> | |||
}, | |||
] | |||
); | |||
export const toLoginPage = () => { | |||
router.navigate('/login'); | |||
} | |||
function findNodeByPath(routes: RouteObject[], path: string) { | |||
for (let i = 0; i < routes.length; i += 1) { | |||
const element = routes[i]; | |||
if (element.path === path) return element; | |||
findNodeByPath(element.children || [], path); | |||
} | |||
} | |||
export const addRoutes = (parentPath: string, routes: RouteObject[]) => { | |||
if (!parentPath) { | |||
router.routes.push(...routes as any); | |||
return; | |||
} | |||
const curNode = findNodeByPath(router.routes, parentPath); | |||
if (curNode?.children) { | |||
curNode?.children.push(...routes); | |||
} else if (curNode) { | |||
curNode.children = routes; | |||
} | |||
} | |||
export const replaceRoutes = (parentPath: string, routes: RouteObject[]) => { | |||
if (!parentPath) { | |||
router.routes.push(...routes as any); | |||
return; | |||
} | |||
const curNode = findNodeByPath(router.routes, parentPath); | |||
if (curNode) { | |||
curNode.children = routes; | |||
} | |||
} | |||
const Router = () => { | |||
const { notification, message, modal } = App.useApp(); | |||
useEffect(() => { | |||
antdUtils.setMessageInstance(message); | |||
antdUtils.setNotificationInstance(notification); | |||
antdUtils.setModalInstance(modal); | |||
}, [notification, message, modal]); | |||
return ( | |||
<RouterProvider router={router} /> | |||
) | |||
}; | |||
export default Router; | |||
@@ -0,0 +1,53 @@ | |||
import { create } from 'zustand' | |||
import { devtools, persist, createJSONStorage } from 'zustand/middleware'; | |||
interface State { | |||
darkMode: boolean; | |||
collapsed: boolean; | |||
lang: string; | |||
token: string; | |||
refreshToken: string; | |||
} | |||
interface Action { | |||
setDarkMode: (darkMode: State['darkMode']) => void; | |||
setCollapsed: (collapsed: State['collapsed']) => void; | |||
setLang: (lang: State['lang']) => void; | |||
setToken: (lang: State['token']) => void; | |||
setRefreshToken: (lang: State['refreshToken']) => void; | |||
} | |||
export const useGlobalStore = create<State & Action>()( | |||
devtools(persist( | |||
(set) => { | |||
return { | |||
darkMode: false, | |||
collapsed: false, | |||
lang: 'zh', | |||
token: '', | |||
refreshToken: '', | |||
setDarkMode: (darkMode: State['darkMode']) => set({ | |||
darkMode, | |||
}), | |||
setCollapsed: (collapsed: State['collapsed']) => set({ | |||
collapsed, | |||
}), | |||
setLang: (lang: State['lang']) => set({ | |||
lang, | |||
}), | |||
setToken: (token: State['token']) => set({ | |||
token, | |||
}), | |||
setRefreshToken: (refreshToken: State['refreshToken']) => set({ | |||
refreshToken, | |||
}), | |||
}; | |||
}, | |||
{ | |||
name: 'globalStore', | |||
storage: createJSONStorage(() => localStorage), | |||
} | |||
), | |||
{ name: 'globalStore' } | |||
) | |||
) |
@@ -0,0 +1,24 @@ | |||
import { UserDTO } from '@/models/user'; | |||
import { create } from 'zustand'; | |||
import { devtools } from 'zustand/middleware'; | |||
interface State { | |||
currentUser: UserDTO | null; | |||
} | |||
interface Action { | |||
setCurrentUser: (currentUser: State['currentUser']) => void; | |||
} | |||
export const useUserStore = create<State & Action>()( | |||
devtools( | |||
(set) => { | |||
return { | |||
currentUser: null, | |||
setCurrentUser: (currentUser: State['currentUser']) => | |||
set({currentUser}), | |||
}; | |||
}, | |||
{name: 'globalUserStore'} | |||
) | |||
); |
@@ -0,0 +1,26 @@ | |||
import { MessageInstance } from 'antd/es/message/interface'; | |||
import { ModalStaticFunctions } from 'antd/es/modal/confirm'; | |||
import { NotificationInstance } from 'antd/es/notification/interface'; | |||
type ModalInstance = Omit<ModalStaticFunctions, 'warn'>; | |||
class AntdUtils { | |||
message: MessageInstance | null = null; | |||
notification: NotificationInstance | null = null; | |||
modal: ModalInstance | null = null; | |||
setMessageInstance(message: MessageInstance) { | |||
this.message = message; | |||
this.message.success | |||
} | |||
setNotificationInstance(notification: NotificationInstance) { | |||
this.notification = notification; | |||
} | |||
setModalInstance(modal: ModalInstance) { | |||
this.modal = modal; | |||
} | |||
} | |||
export const antdUtils = new AntdUtils(); |
@@ -0,0 +1,27 @@ | |||
import i18n from "i18next"; | |||
import enUS from '@/assets/locales/en-US' | |||
import zhCN from '@/assets/locales/zh-CN' | |||
import { defaultSetting } from '@/default-setting'; | |||
i18n | |||
.init({ | |||
resources: { | |||
'en': { | |||
translation: enUS, | |||
}, | |||
'zh': { | |||
translation: zhCN, | |||
}, | |||
}, | |||
lng: defaultSetting.defaultLang || 'zh', | |||
fallbackLng: defaultSetting.defaultLang || 'zh', | |||
interpolation: { | |||
escapeValue: false | |||
}, | |||
}); | |||
export const t = (key: string) => { | |||
return i18n.t(key) || key; | |||
}; | |||
export { i18n }; |
@@ -0,0 +1,12 @@ | |||
export function getParamsBySearchParams<T = any>(query: URLSearchParams) { | |||
const params = [...query.keys()].reduce<Record<string, any>>( | |||
(prev, cur: string) => { | |||
if (cur) { | |||
prev[cur] = query.get(cur); | |||
} | |||
return prev; | |||
}, | |||
{} | |||
); | |||
return params as T; | |||
} |
@@ -18,7 +18,11 @@ | |||
"strict": true, | |||
"noUnusedLocals": true, | |||
"noUnusedParameters": true, | |||
"noFallthroughCasesInSwitch": true | |||
"noFallthroughCasesInSwitch": true, | |||
"baseUrl": "./", | |||
"paths": { | |||
"@/*": ["./src/*"] | |||
} | |||
}, | |||
"include": ["src"], | |||
"references": [{ "path": "./tsconfig.node.json" }] | |||