ソースを参照

layout struct

dev
powersir 1年前
コミット
7791e46949
56個のファイルの変更3732行の追加56行の削除
  1. +45
    -18
      README.md
  2. +1
    -1
      index.html
  3. +1
    -0
      package.json
  4. +69
    -0
      public/images/login-right-after.svg
  5. +65
    -0
      public/images/login-right-before.svg
  6. +39
    -0
      public/images/login-right-bg.svg
  7. +59
    -27
      src/App.tsx
  8. +846
    -0
      src/assets/antd-icons/index.tsx
  9. +148
    -0
      src/assets/css/overwrite.css
  10. +22
    -0
      src/assets/icons/3.tsx
  11. +22
    -0
      src/assets/icons/buguang.tsx
  12. +22
    -0
      src/assets/icons/fangdajing.tsx
  13. +22
    -0
      src/assets/icons/jiaretaiyang.tsx
  14. +22
    -0
      src/assets/icons/moon.tsx
  15. +22
    -0
      src/assets/icons/shuyi_fanyi-36.tsx
  16. +22
    -0
      src/assets/icons/sun.tsx
  17. +78
    -0
      src/assets/locales/en-US.ts
  18. +78
    -0
      src/assets/locales/zh-CN.ts
  19. +0
    -1
      src/assets/react.svg
  20. +15
    -0
      src/components/draggable-tab/index.css
  21. +88
    -0
      src/components/draggable-tab/index.tsx
  22. +47
    -0
      src/components/global-loading/index.css
  23. +13
    -0
      src/components/global-loading/index.tsx
  24. +20
    -0
      src/components/loading/index.tsx
  25. +14
    -0
      src/default-setting.ts
  26. +47
    -0
      src/hooks/use-match-router/index.tsx
  27. +6
    -0
      src/hooks/use-pc-screen/index.tsx
  28. +77
    -0
      src/hooks/use-request/index.ts
  29. +106
    -0
      src/hooks/use-tabs/index.tsx
  30. +17
    -0
      src/layout/404.tsx
  31. +41
    -0
      src/layout/content/index.tsx
  32. +0
    -0
      src/layout/content/tab.tsx
  33. +175
    -0
      src/layout/header/index.tsx
  34. +24
    -0
      src/layout/index.css
  35. +168
    -0
      src/layout/index.tsx
  36. +77
    -0
      src/layout/slide/index.tsx
  37. +311
    -0
      src/layout/slide/menus.tsx
  38. +18
    -0
      src/layout/tabs-context.tsx
  39. +124
    -0
      src/layout/tabs-layout.tsx
  40. +16
    -8
      src/main.tsx
  41. +6
    -0
      src/models/index.ts
  42. +108
    -0
      src/models/user.ts
  43. +43
    -0
      src/pages/login/index.css
  44. +148
    -0
      src/pages/login/index.tsx
  45. +176
    -0
      src/request/index.ts
  46. +15
    -0
      src/request/service/login.ts
  47. +5
    -0
      src/request/service/user.ts
  48. +8
    -0
      src/router/router-error-element.tsx
  49. +89
    -0
      src/router/router.tsx
  50. +53
    -0
      src/store/global/index.ts
  51. +24
    -0
      src/store/global/user.ts
  52. +0
    -0
      src/store/index.ts
  53. +26
    -0
      src/utils/antd.ts
  54. +27
    -0
      src/utils/i18n.ts
  55. +12
    -0
      src/utils/utils.ts
  56. +5
    -1
      tsconfig.json

+ 45
- 18
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

+ 1
- 1
index.html ファイルの表示

@@ -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>


+ 1
- 0
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",


+ 69
- 0
public/images/login-right-after.svg
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 65
- 0
public/images/login-right-before.svg
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 39
- 0
public/images/login-right-bg.svg ファイルの表示

@@ -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>

+ 59
- 27
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 (
<>
<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>
)
}



+ 846
- 0
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,
};

+ 148
- 0
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);
}

+ 22
- 0
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 = () => (
<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} />
);

+ 22
- 0
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 = () => (
<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} />
);

+ 22
- 0
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 = () => (
<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} />
);

+ 22
- 0
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 = () => (
<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} />
);

+ 22
- 0
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 = () => (
<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} />
);

+ 22
- 0
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 = () => (
<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} />
);

+ 22
- 0
src/assets/icons/sun.tsx
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 78
- 0
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",
};

+ 78
- 0
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: "邮箱格式不正确",
};

+ 0
- 1
src/assets/react.svg ファイルの表示

@@ -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>

+ 15
- 0
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;
}

+ 88
- 0
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<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;

+ 47
- 0
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;
}
}

+ 13
- 0
src/components/global-loading/index.tsx ファイルの表示

@@ -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;

+ 20
- 0
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 (
<div className='flex justify-center'>
<Spin />
</div>
);
}

+ 14
- 0
src/default-setting.ts ファイルの表示

@@ -0,0 +1,14 @@
export const defaultSetting = {
slideWidth: 280,
languages: [
{
key: 'zh',
name: 'wrQwwbSV',
},
{
key: 'en',
name: 'hGtEfNnp',
},
],
defaultLang: 'zh',
}

+ 47
- 0
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<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;
}

+ 6
- 0
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;
}

+ 77
- 0
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<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,
};
}

+ 106
- 0
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<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,
}
}

+ 17
- 0
src/layout/404.tsx ファイルの表示

@@ -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;

+ 41
- 0
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<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
src/layout/content/tab.tsx ファイルの表示


+ 175
- 0
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 (
<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);

+ 24
- 0
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;
}

+ 168
- 0
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<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;

+ 77
- 0
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 (
<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);

+ 311
- 0
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<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;

+ 18
- 0
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<KeepAliveTabContextType>(defaultValue);

+ 124
- 0
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 (
<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;

+ 16
- 8
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(
<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 />
)

+ 6
- 0
src/models/index.ts ファイルの表示

@@ -0,0 +1,6 @@
export * from './user.ts'

export interface PageData<T> {
data: T[];
total: number;
}

+ 108
- 0
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<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[];
}

+ 43
- 0
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;
}

+ 148
- 0
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 (
<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;

+ 176
- 0
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<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;

+ 15
- 0
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<LoginRespDTO>('/api/login', loginDTO);
},

logout: () => {
return request.get<LoginRespDTO>('/api/logout');
}
};

export default loginService;

+ 5
- 0
src/request/service/user.ts ファイルの表示

@@ -0,0 +1,5 @@
const userService = {

};

export default userService;

+ 8
- 0
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;

+ 89
- 0
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: (
<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;


+ 53
- 0
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<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' }
)
)

+ 24
- 0
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<State & Action>()(
devtools(
(set) => {
return {
currentUser: null,
setCurrentUser: (currentUser: State['currentUser']) =>
set({currentUser}),
};
},
{name: 'globalUserStore'}
)
);

+ 0
- 0
src/store/index.ts ファイルの表示


+ 26
- 0
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<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();

+ 27
- 0
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 };

+ 12
- 0
src/utils/utils.ts ファイルの表示

@@ -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;
}

+ 5
- 1
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" }]


読み込み中…
キャンセル
保存