diff --git a/README.md b/README.md index 1ebe379..b68daaa 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/index.html b/index.html index e4b78ea..1d90108 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + vogocm erp admin
diff --git a/package.json b/package.json index 257eeb6..c98862e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/images/login-right-after.svg b/public/images/login-right-after.svg new file mode 100644 index 0000000..c724e0a --- /dev/null +++ b/public/images/login-right-after.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/login-right-before.svg b/public/images/login-right-before.svg new file mode 100644 index 0000000..6c9fe3e --- /dev/null +++ b/public/images/login-right-before.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/login-right-bg.svg b/public/images/login-right-bg.svg new file mode 100644 index 0000000..aa0e4ab --- /dev/null +++ b/public/images/login-right-bg.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx index afe48ac..474d38a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- + + + + + ) } diff --git a/src/assets/antd-icons/index.tsx b/src/assets/antd-icons/index.tsx new file mode 100644 index 0000000..10bc1e5 --- /dev/null +++ b/src/assets/antd-icons/index.tsx @@ -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, +}; \ No newline at end of file diff --git a/src/assets/css/overwrite.css b/src/assets/css/overwrite.css new file mode 100644 index 0000000..096e8b5 --- /dev/null +++ b/src/assets/css/overwrite.css @@ -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); +} \ No newline at end of file diff --git a/src/assets/icons/3.tsx b/src/assets/icons/3.tsx new file mode 100644 index 0000000..d7037e1 --- /dev/null +++ b/src/assets/icons/3.tsx @@ -0,0 +1,22 @@ +import Icon from "@ant-design/icons"; +import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; + +const SVG3 = () => ( + + + +); + +export const Icon3 = (props: Partial) => ( + +); diff --git a/src/assets/icons/buguang.tsx b/src/assets/icons/buguang.tsx new file mode 100644 index 0000000..ad47b51 --- /dev/null +++ b/src/assets/icons/buguang.tsx @@ -0,0 +1,22 @@ +import Icon from "@ant-design/icons"; +import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; + +const SVGBuguang = () => ( + + + +); + +export const IconBuguang = (props: Partial) => ( + +); diff --git a/src/assets/icons/fangdajing.tsx b/src/assets/icons/fangdajing.tsx new file mode 100644 index 0000000..2370a9f --- /dev/null +++ b/src/assets/icons/fangdajing.tsx @@ -0,0 +1,22 @@ +import Icon from "@ant-design/icons"; +import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; + +const SVGFangdajing = () => ( + + + +); + +export const IconFangdajing = (props: Partial) => ( + +); diff --git a/src/assets/icons/jiaretaiyang.tsx b/src/assets/icons/jiaretaiyang.tsx new file mode 100644 index 0000000..b827a37 --- /dev/null +++ b/src/assets/icons/jiaretaiyang.tsx @@ -0,0 +1,22 @@ +import Icon from "@ant-design/icons"; +import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; + +const SVGJiaretaiyang = () => ( + + + +); + +export const IconJiaretaiyang = (props: Partial) => ( + +); diff --git a/src/assets/icons/moon.tsx b/src/assets/icons/moon.tsx new file mode 100644 index 0000000..ce45157 --- /dev/null +++ b/src/assets/icons/moon.tsx @@ -0,0 +1,22 @@ +import Icon from "@ant-design/icons"; +import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; + +const SVGMoon = () => ( + + + +); + +export const IconMoon = (props: Partial) => ( + +); diff --git a/src/assets/icons/shuyi_fanyi-36.tsx b/src/assets/icons/shuyi_fanyi-36.tsx new file mode 100644 index 0000000..0d2be85 --- /dev/null +++ b/src/assets/icons/shuyi_fanyi-36.tsx @@ -0,0 +1,22 @@ +import Icon from "@ant-design/icons"; +import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; + +const SVGShuyi_fanyi36 = () => ( + + + +); + +export const IconShuyi_fanyi36 = (props: Partial) => ( + +); diff --git a/src/assets/icons/sun.tsx b/src/assets/icons/sun.tsx new file mode 100644 index 0000000..2fd67cf --- /dev/null +++ b/src/assets/icons/sun.tsx @@ -0,0 +1,22 @@ +import Icon from "@ant-design/icons"; +import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; + +const SVGSun = () => ( + + + +); + +export const IconSun = (props: Partial) => ( + +); diff --git a/src/assets/locales/en-US.ts b/src/assets/locales/en-US.ts new file mode 100644 index 0000000..da584f5 --- /dev/null +++ b/src/assets/locales/en-US.ts @@ -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", +}; diff --git a/src/assets/locales/zh-CN.ts b/src/assets/locales/zh-CN.ts new file mode 100644 index 0000000..41aba95 --- /dev/null +++ b/src/assets/locales/zh-CN.ts @@ -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: "邮箱格式不正确", +}; diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/draggable-tab/index.css b/src/components/draggable-tab/index.css new file mode 100644 index 0000000..a309e91 --- /dev/null +++ b/src/components/draggable-tab/index.css @@ -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; +} \ No newline at end of file diff --git a/src/components/draggable-tab/index.tsx b/src/components/draggable-tab/index.tsx new file mode 100644 index 0000000..898ffb3 --- /dev/null +++ b/src/components/draggable-tab/index.tsx @@ -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 { + '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 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 ( + ( + + i.key)} strategy={horizontalListSortingStrategy}> + + {(node) => ( + + {node} + + )} + + + + )} + {...props} + items={items} + className='tab-layout' + /> + ); +}; + +export default DraggableTab; diff --git a/src/components/global-loading/index.css b/src/components/global-loading/index.css new file mode 100644 index 0000000..8644d80 --- /dev/null +++ b/src/components/global-loading/index.css @@ -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; + } +} \ No newline at end of file diff --git a/src/components/global-loading/index.tsx b/src/components/global-loading/index.tsx new file mode 100644 index 0000000..2b12389 --- /dev/null +++ b/src/components/global-loading/index.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +import './index.css' + +const GloablLoading: React.FC = () => ( + <> +
+
+
+ +) + +export default GloablLoading; diff --git a/src/components/loading/index.tsx b/src/components/loading/index.tsx new file mode 100644 index 0000000..2eeafce --- /dev/null +++ b/src/components/loading/index.tsx @@ -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 ( +
+ +
+ ); +} \ No newline at end of file diff --git a/src/default-setting.ts b/src/default-setting.ts new file mode 100644 index 0000000..b3ceb8f --- /dev/null +++ b/src/default-setting.ts @@ -0,0 +1,14 @@ +export const defaultSetting = { + slideWidth: 280, + languages: [ + { + key: 'zh', + name: 'wrQwwbSV', + }, + { + key: 'en', + name: 'hGtEfNnp', + }, + ], + defaultLang: 'zh', +} diff --git a/src/hooks/use-match-router/index.tsx b/src/hooks/use-match-router/index.tsx new file mode 100644 index 0000000..37d315e --- /dev/null +++ b/src/hooks/use-match-router/index.tsx @@ -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(); + + // 监听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; +} diff --git a/src/hooks/use-pc-screen/index.tsx b/src/hooks/use-pc-screen/index.tsx new file mode 100644 index 0000000..3425d6d --- /dev/null +++ b/src/hooks/use-pc-screen/index.tsx @@ -0,0 +1,6 @@ +import { useMedia } from 'react-use'; + +export const usePCScreen = () => { + const isPC = useMedia('(min-width: 1024px)'); + return isPC; +} \ No newline at end of file diff --git a/src/hooks/use-request/index.ts b/src/hooks/use-request/index.ts new file mode 100644 index 0000000..3126441 --- /dev/null +++ b/src/hooks/use-request/index.ts @@ -0,0 +1,77 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { Response } from '@/request'; + +interface RequestOptions { + manual?: boolean; + defaultParams?: any[]; +} + +interface RequestResponse { + error: boolean | undefined; + data: T | undefined; + loading: boolean; + run(...params: any): void; + runAsync(...params: any): Response; + refresh(): void; +} + +export function useRequest( + serviceMethod: (...args: any) => Response, + options?: RequestOptions +): RequestResponse { + const [loading, setLoading] = useState(false); + const [data, setData] = useState(); + const [error, setError] = useState(); + + const paramsRef = useRef([]); + + 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, + }; +} diff --git a/src/hooks/use-tabs/index.tsx b/src/hooks/use-tabs/index.tsx new file mode 100644 index 0000000..423df34 --- /dev/null +++ b/src/hooks/use-tabs/index.tsx @@ -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([]); + // 当前激活的tab + const [activeTabRoutePath, setActiveTabRoutePath] = useState(''); + + 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, + } +} diff --git a/src/layout/404.tsx b/src/layout/404.tsx new file mode 100644 index 0000000..a160cac --- /dev/null +++ b/src/layout/404.tsx @@ -0,0 +1,17 @@ +import { Button, Result } from 'antd'; +import { Link } from 'react-router-dom'; + +const Result404 = () => ( + + 首页 + + )} + /> +); + +export default Result404; diff --git a/src/layout/content/index.tsx b/src/layout/content/index.tsx new file mode 100644 index 0000000..611cab5 --- /dev/null +++ b/src/layout/content/index.tsx @@ -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 = ({ children }) => { + + const isPC = usePCScreen(); + + const { + collapsed, + } = useGlobalStore(); + + return ( +
+
+ + )} + > + {children} + +
+
+ ) +} + +export default Content; diff --git a/src/layout/content/tab.tsx b/src/layout/content/tab.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/layout/header/index.tsx b/src/layout/header/index.tsx new file mode 100644 index 0000000..0f6f498 --- /dev/null +++ b/src/layout/header/index.tsx @@ -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 ( +
+
+
+ +

VOGOCM-ERP

+
+
{ + setCollapsed(!collapsed); + }} + > + +
+
+
+ + } + placeholder={t("jhqxJPbn" /* 搜索菜单 */)} + allowClear + /> +
+
{ + setCollapsed(!collapsed); + }} + > + +
+
+
+
{ setDarkMode(!darkMode) }} className='btn-icon text-[20px]'> + {darkMode ? ( + + ) : ( + + )} +
+ ({ + 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]' + > +
+ {lang === 'zh' ? ( + + ) : ( + + {lang.toUpperCase()} + + )} +
+
+
+ +
+ node.parentElement!} + dropdownRender={() => { + return ( +
+
+

+ {currentUser?.nickName} +

+

+ {currentUser?.phoneNumber} +

+

+ {currentUser?.email} +

+
+
+
+ +
+
+ ) + }} + > +
+ {currentUser?.avatarPath ? ( + + ) : ( + } /> + )} + +
+
+
+
+
+ ) +} + +export default memo(Header); diff --git a/src/layout/index.css b/src/layout/index.css new file mode 100644 index 0000000..5c2cd79 --- /dev/null +++ b/src/layout/index.css @@ -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; +} \ No newline at end of file diff --git a/src/layout/index.tsx b/src/layout/index.tsx new file mode 100644 index 0000000..30c6225 --- /dev/null +++ b/src/layout/index.tsx @@ -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, + 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>((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 ( + + ) + } + + return ( +
+
+
+ + + + +
+
+ ); +}; + +export default BasicLayout; diff --git a/src/layout/slide/index.tsx b/src/layout/slide/index.tsx new file mode 100644 index 0000000..d2d3587 --- /dev/null +++ b/src/layout/slide/index.tsx @@ -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 ( + + ) + } + + if (!isPC) { + return ( + + +

fluxy-admin

+ + )} + headerStyle={{ padding: '24px 0', border: 'none' }} + bodyStyle={{ padding: '0 16px' }} + onClose={() => { + setCollapsed(true); + }} + > + {renderMenu()} +
+ ) + } + + return ( +
+ {renderMenu()} +
+ ) +} + +export default memo(SlideIndex); diff --git a/src/layout/slide/menus.tsx b/src/layout/slide/menus.tsx new file mode 100644 index 0000000..056dacb --- /dev/null +++ b/src/layout/slide/menus.tsx @@ -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['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', ), + + getItem('定制选品', 'custom-made', , [ + 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', , [ + 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', , [ + 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', , [ + 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', , [ + 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', , [ + 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', , [ + 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', , [ + 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', , [ + 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', , [ + 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', , [ + 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([]); + const [selectKeys, setSelectKeys] = useState([]); + + 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 ( + {menu.name} + ); + } + + 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 ( + + ) +} + +export default SlideMenu; diff --git a/src/layout/tabs-context.tsx b/src/layout/tabs-context.tsx new file mode 100644 index 0000000..c26d4a7 --- /dev/null +++ b/src/layout/tabs-context.tsx @@ -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(defaultValue); diff --git a/src/layout/tabs-layout.tsx b/src/layout/tabs-layout.tsx new file mode 100644 index 0000000..889d813 --- /dev/null +++ b/src/layout/tabs-layout.tsx @@ -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 ( + menuClick(e, tab) }} + trigger={['contextMenu']} + > +
+ {getIcon(tab.icon)} + {tab.title} +
+
+ ) + }, [menuItems]); + + const tabItems = useMemo(() => { + return tabs.map(tab => { + return { + key: tab.routePath, + label: renderTabTitle(tab), + children: ( +
+ {tab.children} +
+ ), + 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 ( + + + + ) +} + +export default TabsLayout; diff --git a/src/main.tsx b/src/main.tsx index 3d7150d..871074f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( - - - , +NProgress.configure({ + minimum: 0.3, + easing: 'ease', + speed: 800, + showSpinner: false, + parent: '#root' +}); + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + ) diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..a864daa --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,6 @@ +export * from './user.ts' + +export interface PageData { + data: T[]; + total: number; +} diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 0000000..d171ca5 --- /dev/null +++ b/src/models/user.ts @@ -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; + 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; +} + + +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[]; +} diff --git a/src/pages/login/index.css b/src/pages/login/index.css new file mode 100644 index 0000000..6ac1533 --- /dev/null +++ b/src/pages/login/index.css @@ -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; +} \ No newline at end of file diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx new file mode 100644 index 0000000..fd2c6d2 --- /dev/null +++ b/src/pages/login/index.tsx @@ -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 ( +
+
+
+
+
+ +

旺嘉-ERP Admin

+
+

+ {t("wbTMzvDM" /* 一个高颜值后台管理系统 */)} +

+
+
+ + } + placeholder={t("RNISycbR" /* 账号 */)} + size="large" + /> + + + } + type="password" + placeholder={t("HplkKxdY" /* 密码 */)} + /> + + + + + + + +
+
+
+
+
+
+
+ +
+
+
+

+ fluxy-admin +

+
+ 一个高颜值后台管理系统 +
+
+
+
+
+
+
+

+ fluxy-admin +

+
+ 一个高颜值后台管理系统 +
+
+
+
+
+
+
+

+ fluxy-admin +

+
+ 一个高颜值后台管理系统 +
+
+
+
+
+
+
+
+ ); +}; + +export default Login; diff --git a/src/request/index.ts b/src/request/index.ts new file mode 100644 index 0000000..b23842c --- /dev/null +++ b/src/request/index.ts @@ -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 = Promise<[boolean, T, AxiosResponse]>; + +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) => + 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 { + 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 + ): Promise { + 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 { + 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(config: AxiosRequestConfig): Response { + return this.axiosInstance(config); + } + + get(url: string, config?: AxiosRequestConfig): Response { + return this.axiosInstance.get(url, config); + } + + post( + url: string, + data?: D, + config?: AxiosRequestConfig + ): Response { + return this.axiosInstance.post(url, data, config); + } + + put( + url: string, + data?: D, + config?: AxiosRequestConfig + ): Response { + return this.axiosInstance.put(url, data, config); + } + + delete(url: string, config?: AxiosRequestConfig): Response { + return this.axiosInstance.delete(url, config); + } +} + +const request = new Request({timeout: 60 * 1000 * 5, baseURL: 'https://test.vogocm.com:9697'}); + +export default request; diff --git a/src/request/service/login.ts b/src/request/service/login.ts new file mode 100644 index 0000000..a9e4109 --- /dev/null +++ b/src/request/service/login.ts @@ -0,0 +1,15 @@ +import request from '@/request'; +import { LoginDTO, LoginRespDTO } from '@/models' + +const loginService = { + // 登录 + login: (loginDTO: LoginDTO) => { + return request.post('/api/login', loginDTO); + }, + + logout: () => { + return request.get('/api/logout'); + } +}; + +export default loginService; diff --git a/src/request/service/user.ts b/src/request/service/user.ts new file mode 100644 index 0000000..5d2923e --- /dev/null +++ b/src/request/service/user.ts @@ -0,0 +1,5 @@ +const userService = { + +}; + +export default userService; diff --git a/src/router/router-error-element.tsx b/src/router/router-error-element.tsx new file mode 100644 index 0000000..117260e --- /dev/null +++ b/src/router/router-error-element.tsx @@ -0,0 +1,8 @@ +import { useRouteError } from 'react-router-dom'; + +const RouterErrorElement = () => { + const error = useRouteError(); + throw error; +} + +export default RouterErrorElement; \ No newline at end of file diff --git a/src/router/router.tsx b/src/router/router.tsx new file mode 100644 index 0000000..f5b4c81 --- /dev/null +++ b/src/router/router.tsx @@ -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: ( + + ), + }, + { + path: '*', + Component: BasicLayout, + children: [], + errorElement: + }, + ] +); + +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 ( + + ) +}; + +export default Router; + diff --git a/src/store/global/index.ts b/src/store/global/index.ts new file mode 100644 index 0000000..3c7636d --- /dev/null +++ b/src/store/global/index.ts @@ -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()( + 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' } + ) +) diff --git a/src/store/global/user.ts b/src/store/global/user.ts new file mode 100644 index 0000000..15cd99f --- /dev/null +++ b/src/store/global/user.ts @@ -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()( + devtools( + (set) => { + return { + currentUser: null, + setCurrentUser: (currentUser: State['currentUser']) => + set({currentUser}), + }; + }, + {name: 'globalUserStore'} + ) +); diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/antd.ts b/src/utils/antd.ts new file mode 100644 index 0000000..ba28089 --- /dev/null +++ b/src/utils/antd.ts @@ -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; + +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(); \ No newline at end of file diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts new file mode 100644 index 0000000..5ceeea2 --- /dev/null +++ b/src/utils/i18n.ts @@ -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 }; diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..0e1dc22 --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,12 @@ +export function getParamsBySearchParams(query: URLSearchParams) { + const params = [...query.keys()].reduce>( + (prev, cur: string) => { + if (cur) { + prev[cur] = query.get(cur); + } + return prev; + }, + {} + ); + return params as T; +} diff --git a/tsconfig.json b/tsconfig.json index a7fc6fb..e7b52dd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,11 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "baseUrl": "./", + "paths": { + "@/*": ["./src/*"] + } }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }]