From 5bdd8603a1aa2dcf679c056215f1a788de3546d3 Mon Sep 17 00:00:00 2001 From: xie2can <384968446@qq.com> Date: Fri, 15 May 2026 12:52:00 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E4=B8=BA=E4=B8=8A=E4=BE=A7=E5=8F=B3=E7=BB=93=E6=9E=84?= =?UTF-8?q?=EF=BC=8C=E9=9B=86=E6=88=90=20antd=20=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=B8=8E=E4=B8=AD=E6=96=87=E8=AF=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 3 ++ src/App.css | 27 +++-------- src/App.tsx | 15 ++++++- src/layouts/RootLayout.tsx | 91 +++++++++++++++++++++++++++++++++----- src/routes/index.tsx | 3 ++ 6 files changed, 106 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index a4ac3a3..539ab53 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@ant-design/icons": "^6.2.3", "antd": "^6.4.2", + "dayjs": "^1.11.20", "react": "^19.2.6", "react-dom": "^19.2.6", "react-router": "^7.15.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80ce0d7..4dba754 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: antd: specifier: ^6.4.2 version: 6.4.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + dayjs: + specifier: ^1.11.20 + version: 1.11.20 react: specifier: ^19.2.6 version: 19.2.6 diff --git a/src/App.css b/src/App.css index 164c0a6..a2b2cba 100644 --- a/src/App.css +++ b/src/App.css @@ -1,26 +1,9 @@ +* { + box-sizing: border-box; +} + body { margin: 0; - color: #fff; + padding: 0; font-family: Inter, Avenir, Helvetica, Arial, sans-serif; - background-image: linear-gradient(to bottom, #020917, #101725); -} - -.content { - display: flex; - min-height: 100vh; - line-height: 1.1; - text-align: center; - flex-direction: column; - justify-content: center; -} - -.content h1 { - font-size: 3.6rem; - font-weight: 700; -} - -.content p { - font-size: 1.2rem; - font-weight: 400; - opacity: 0.5; } diff --git a/src/App.tsx b/src/App.tsx index aef4ae4..ecbf7c2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,19 @@ +import { App as AntdApp, ConfigProvider } from 'antd'; +import zhCN from 'antd/locale/zh_CN'; +import dayjs from 'dayjs'; +import 'dayjs/locale/zh-cn'; import { RouterProvider } from 'react-router'; import router from './router'; +import './App.css'; -const App = () => ; +dayjs.locale('zh-cn'); + +const App = () => ( + + + + + +); export default App; diff --git a/src/layouts/RootLayout.tsx b/src/layouts/RootLayout.tsx index e1f6f40..675c642 100644 --- a/src/layouts/RootLayout.tsx +++ b/src/layouts/RootLayout.tsx @@ -1,17 +1,86 @@ -import { Link, Outlet } from 'react-router'; +import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; +import { Layout, Menu } from 'antd'; +import { useState } from 'react'; +import { Outlet, useLocation, useNavigate } from 'react-router'; +import { routes } from '../routes'; + +const { Header, Sider, Content } = Layout; + +const menuItems = routes + .filter((r) => !r.hideInMenu) + .map((r) => ({ + key: r.path, + icon: r.icon, + label: r.label, + })); const RootLayout = () => { + const navigate = useNavigate(); + const location = useLocation(); + const [collapsed, setCollapsed] = useState(false); + return ( - <> - - 首页 - {' | '} - 关于 - - - - - > + + + TaoTie + + + + navigate(key)} + style={{ height: '100%', borderRight: 0, paddingBottom: 48 }} + /> + setCollapsed(!collapsed)} + style={{ + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: 48, + display: 'flex', + alignItems: 'center', + justifyContent: collapsed ? 'center' : 'flex-end', + padding: collapsed ? 0 : '0 24px', + borderTop: '1px solid #f0f0f0', + cursor: 'pointer', + color: '#666', + background: '#fff', + transition: 'all 0.2s', + userSelect: 'none', + }} + > + {collapsed ? ( + + ) : ( + <> + + 收起 + > + )} + + + + + + + ); }; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 166c40b..02982c7 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,3 +1,4 @@ +import { HomeOutlined, InfoCircleOutlined } from '@ant-design/icons'; import About from '../pages/About'; import Home from '../pages/Home'; import NotFound from '../pages/NotFound'; @@ -7,11 +8,13 @@ export const routes: RouteItem[] = [ { path: '/', label: '首页', + icon: , component: , }, { path: '/about', label: '关于', + icon: , component: , }, { -- 2.49.1 From 3ff06427060114a8889badb958fd21b4589c5a11 Mon Sep 17 00:00:00 2001 From: xie2can <384968446@qq.com> Date: Fri, 15 May 2026 13:08:21 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20axios=20?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E5=B0=81=E8=A3=85=E4=B8=8E=E5=85=A8=E5=B1=80?= =?UTF-8?q?=20API=20=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 + package.json | 1 + pnpm-lock.yaml | 214 +++++++++++++++++++++++++++++++++++++++++++ src/env.d.ts | 4 + src/types/http.d.ts | 15 +++ src/utils/request.ts | 43 +++++++++ 6 files changed, 278 insertions(+) create mode 100644 .env create mode 100644 src/types/http.d.ts create mode 100644 src/utils/request.ts diff --git a/.env b/.env new file mode 100644 index 0000000..a237fb8 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +PUBLIC_BASE_URL=http://localhost:8080 diff --git a/package.json b/package.json index 539ab53..2724978 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@ant-design/icons": "^6.2.3", "antd": "^6.4.2", + "axios": "^1.16.1", "dayjs": "^1.11.20", "react": "^19.2.6", "react-dom": "^19.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4dba754..e2d3950 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: antd: specifier: ^6.4.2 version: 6.4.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + axios: + specifier: ^1.16.1 + version: 1.16.1 dayjs: specifier: ^1.11.20 version: 1.11.20 @@ -732,6 +735,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} @@ -741,6 +748,12 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.16.1: + resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -759,6 +772,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001792: resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} @@ -766,6 +783,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} @@ -798,9 +819,33 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + electron-to-chromium@1.5.356: resolution: {integrity: sha512-9NgFd7m5t5MCJ5rUSjJITUXAH9mEGlrlofnMf4YEr+pz6JlP7cWmTAH+JFmbPnaSW8koVTkuW7pacORWAnA5Yw==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -895,10 +940,34 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -907,12 +976,32 @@ packages: resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -978,6 +1067,18 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -1027,6 +1128,10 @@ packages: engines: {node: '>=14'} hasBin: true + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1934,6 +2039,12 @@ snapshots: acorn@8.16.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -1997,6 +2108,18 @@ snapshots: - luxon - moment + asynckit@0.4.0: {} + + axios@1.16.1: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + balanced-match@4.0.4: {} baseline-browser-mapping@2.10.29: {} @@ -2013,10 +2136,19 @@ snapshots: node-releases: 2.0.44 update-browserslist-db: 1.2.3(browserslist@4.28.2) + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + caniuse-lite@1.0.30001792: {} clsx@2.1.1: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + compute-scroll-into-view@3.1.1: {} convert-source-map@2.0.0: {} @@ -2039,8 +2171,31 @@ snapshots: deep-is@0.1.4: {} + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + electron-to-chromium@1.5.356: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -2150,20 +2305,69 @@ snapshots: flatted@3.4.2: {} + follow-redirects@1.16.0: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 globals@17.6.0: {} + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + hermes-estree@0.25.1: {} hermes-parser@0.25.1: dependencies: hermes-estree: 0.25.1 + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + ignore@5.3.2: {} ignore@7.0.5: {} @@ -2213,6 +2417,14 @@ snapshots: dependencies: yallist: 3.1.1 + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@10.2.5: dependencies: brace-expansion: 5.0.6 @@ -2252,6 +2464,8 @@ snapshots: prettier@3.8.3: {} + proxy-from-env@2.1.0: {} + punycode@2.3.1: {} react-dom@19.2.6(react@19.2.6): diff --git a/src/env.d.ts b/src/env.d.ts index 698ba8b..0b3795f 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -9,3 +9,7 @@ declare module '*.svg?react' { const ReactComponent: React.FunctionComponent>; export default ReactComponent; } + +interface ImportMetaEnv { + readonly PUBLIC_BASE_URL: string; +} diff --git a/src/types/http.d.ts b/src/types/http.d.ts new file mode 100644 index 0000000..4aa3ff1 --- /dev/null +++ b/src/types/http.d.ts @@ -0,0 +1,15 @@ +declare namespace API { + /** 全局 HTTP 响应结构体 */ + interface Response { + /** 业务状态码,成功为 "0" */ + code: string; + /** 提示信息 */ + msg: string; + /** 响应数据 */ + data?: T; + /** 服务端时间戳 */ + time: number; + /** 是否成功(code === "0" 时为 true) */ + ok: boolean; + } +} diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..c4bbae2 --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,43 @@ +import axios from 'axios'; + +const request = axios.create({ + baseURL: import.meta.env.PUBLIC_BASE_URL, + timeout: 10000, +}); + +// 请求拦截器 +request.interceptors.request.use( + (config) => { + // TODO: 添加 token 等请求头 + return config; + }, + (error) => { + return Promise.reject(error); + }, +); + +// 响应拦截器 +request.interceptors.response.use( + (response) => { + // TODO: 统一处理业务错误码 + return response; + }, + (error) => { + // TODO: 统一处理网络错误 + return Promise.reject(error); + }, +); + +export const get = (url: string, params?: object) => + request.get>(url, { params }).then((res) => res.data); + +export const post = (url: string, data?: object) => + request.post>(url, data).then((res) => res.data); + +export const put = (url: string, data?: object) => + request.put>(url, data).then((res) => res.data); + +export const del = (url: string, params?: object) => + request.delete>(url, { params }).then((res) => res.data); + +export default request; -- 2.49.1 From 7925c924781a98b618953484a48614a25818b4af Mon Sep 17 00:00:00 2001 From: xie2can <384968446@qq.com> Date: Fri, 15 May 2026 13:09:12 +0800 Subject: [PATCH 3/7] =?UTF-8?q?chore:=20=E5=BF=BD=E7=95=A5=20.env=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E7=A7=BB=E9=99=A4=E5=B7=B2=E8=BF=BD?= =?UTF-8?q?=E8=B8=AA=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 - .gitignore | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index a237fb8..0000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -PUBLIC_BASE_URL=http://localhost:8080 diff --git a/.gitignore b/.gitignore index 6f3092c..c71d1f1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,11 @@ *.local *.log* +# Environment variables +.env +.env.* +!.env.example + # Dist node_modules dist/ -- 2.49.1 From 0ffa0e1ddf6da890db3f043783e6507ed2fdc44d Mon Sep 17 00:00:00 2001 From: xie2can <384968446@qq.com> Date: Fri, 15 May 2026 13:09:53 +0800 Subject: [PATCH 4/7] =?UTF-8?q?chore:=20=E6=96=B0=E5=A2=9E=20.env.example?= =?UTF-8?q?=20=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 1 + 1 file changed, 1 insertion(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a237fb8 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +PUBLIC_BASE_URL=http://localhost:8080 -- 2.49.1 From f31c459610db512bb0bef65c70207ba3bf699f2f Mon Sep 17 00:00:00 2001 From: xie2can <384968446@qq.com> Date: Fri, 15 May 2026 13:11:16 +0800 Subject: [PATCH 5/7] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20AGENTS.md?= =?UTF-8?q?=EF=BC=8C=E8=A1=A5=E5=85=85=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F?= =?UTF-8?q?=E4=B8=8E=20HTTP=20=E8=AF=B7=E6=B1=82=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 46 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 96a328e..66912e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,27 +23,46 @@ ``` src/ index.tsx # 应用入口,将 React 根节点挂载到 #root - App.tsx # 根组件,渲染 - App.css # 根组件样式 + App.tsx # 根组件,渲染 ,包含 ConfigProvider / AntdApp 全局配置 + App.css # 全局样式(reset) router.tsx # createBrowserRouter,由路由树自动生成 - env.d.ts # Rsbuild 环境类型声明 + env.d.ts # Rsbuild 环境变量类型声明(ImportMetaEnv) routes/ types.ts # RouteItem 类型定义 index.tsx # 路由树数据(唯一数据源),导出 routes / RouteItem utils.tsx # toRouteObjects():将路由树转为 React Router RouteObject[] layouts/ - RootLayout.tsx # 根布局,包含导航链接和 + RootLayout.tsx # 根布局(Header + Sider + Content) pages/ Home.tsx # "/" 首页 About.tsx # "/about" 关于页 NotFound.tsx # "*" 兜底 404 页 + types/ + http.d.ts # 全局 API 命名空间(无需 import 直接使用 API.Response) + utils/ + request.ts # axios 实例封装,导出 get / post / put / del +.env # 本地环境变量(已 gitignore,勿提交) +.env.example # 环境变量模板(提交到仓库供参考) public/ favicon.png -rsbuild.config.ts # 构建配置 -eslint.config.mjs # ESLint 扁平配置(仅作用于 TS/TSX,忽略 dist/) +rsbuild.config.ts # 构建配置 +eslint.config.mjs # ESLint 扁平配置(仅作用于 TS/TSX,忽略 dist/) tsconfig.json ``` +## 环境变量 + +- 变量文件:`.env`(本地,已 gitignore) +- 模板文件:`.env.example`(提交到仓库) +- 新成员初始化:`cp .env.example .env` +- Rsbuild 规则:**以 `PUBLIC_` 为前缀**的变量会暴露给客户端,通过 `import.meta.env.PUBLIC_XXX` 读取 + +| 变量 | 说明 | +|------|------| +| `PUBLIC_BASE_URL` | 后端接口 baseURL | + +不同环境可创建 `.env.development` / `.env.production` 覆盖默认值,`.env.example` 中同步维护所有变量。 + ## 路由 使用 **React Router v7**(`react-router`)的 `createBrowserRouter`。 @@ -87,6 +106,21 @@ import { ConfigProvider } from 'antd'; ``` +## HTTP 请求 + +- 所有请求通过 `src/utils/request.ts` 封装的方法发出,不直接使用 `axios` +- 导出方法:`get` / `post` / `put` / `del`,返回值类型自动推断为 `API.Response` +- 全局响应结构 `API.Response` 定义在 `src/types/http.d.ts`,无需 import 直接使用 +- 请求拦截器(添加 token)和响应拦截器(处理错误码)统一在 `request.ts` 中维护 + +```ts +// 使用示例 +import { get, post } from '../utils/request'; + +const res = await get('/api/users'); +// res.code / res.msg / res.data / res.ok / res.time +``` + ## 参考文档 - Rsbuild: https://rsbuild.rs/llms.txt -- 2.49.1 From 790b6905ef59aa6c6e148a6d805c5f146077097f Mon Sep 17 00:00:00 2001 From: xie2can <384968446@qq.com> Date: Fri, 15 May 2026 14:05:34 +0800 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E9=85=8D=E7=BD=AE/=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=A1=B5=E9=9D=A2=EF=BC=8C=E9=87=8D=E6=9E=84=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E7=9B=AE=E5=BD=95=E7=BB=93=E6=9E=84=E4=B8=BA=20/xxx/i?= =?UTF-8?q?ndex.tsx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 1 + src/layouts/RootLayout.tsx | 10 +++----- src/layouts/SystemLayout.tsx | 5 ++++ src/pages/{About.tsx => about/index.tsx} | 0 src/pages/{Home.tsx => home/index.tsx} | 0 .../{NotFound.tsx => not-found/index.tsx} | 0 src/pages/system/user/index.tsx | 21 ++++++++++++++++ src/routes/index.tsx | 24 +++++++++++++++---- src/routes/utils.tsx | 18 ++++++++++++++ 9 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 .gitattributes create mode 100644 src/layouts/SystemLayout.tsx rename src/pages/{About.tsx => about/index.tsx} (100%) rename src/pages/{Home.tsx => home/index.tsx} (100%) rename src/pages/{NotFound.tsx => not-found/index.tsx} (100%) create mode 100644 src/pages/system/user/index.tsx diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/src/layouts/RootLayout.tsx b/src/layouts/RootLayout.tsx index 675c642..a0c56ba 100644 --- a/src/layouts/RootLayout.tsx +++ b/src/layouts/RootLayout.tsx @@ -3,16 +3,11 @@ import { Layout, Menu } from 'antd'; import { useState } from 'react'; import { Outlet, useLocation, useNavigate } from 'react-router'; import { routes } from '../routes'; +import { toMenuItems } from '../routes/utils'; const { Header, Sider, Content } = Layout; -const menuItems = routes - .filter((r) => !r.hideInMenu) - .map((r) => ({ - key: r.path, - icon: r.icon, - label: r.label, - })); +const menuItems = toMenuItems(routes); const RootLayout = () => { const navigate = useNavigate(); @@ -42,6 +37,7 @@ const RootLayout = () => { item.key)} items={menuItems} onClick={({ key }) => navigate(key)} style={{ height: '100%', borderRight: 0, paddingBottom: 48 }} diff --git a/src/layouts/SystemLayout.tsx b/src/layouts/SystemLayout.tsx new file mode 100644 index 0000000..f308459 --- /dev/null +++ b/src/layouts/SystemLayout.tsx @@ -0,0 +1,5 @@ +import { Outlet } from 'react-router'; + +const SystemLayout = () => ; + +export default SystemLayout; diff --git a/src/pages/About.tsx b/src/pages/about/index.tsx similarity index 100% rename from src/pages/About.tsx rename to src/pages/about/index.tsx diff --git a/src/pages/Home.tsx b/src/pages/home/index.tsx similarity index 100% rename from src/pages/Home.tsx rename to src/pages/home/index.tsx diff --git a/src/pages/NotFound.tsx b/src/pages/not-found/index.tsx similarity index 100% rename from src/pages/NotFound.tsx rename to src/pages/not-found/index.tsx diff --git a/src/pages/system/user/index.tsx b/src/pages/system/user/index.tsx new file mode 100644 index 0000000..673ab1f --- /dev/null +++ b/src/pages/system/user/index.tsx @@ -0,0 +1,21 @@ +import { Table, Typography } from 'antd'; + +const { Title } = Typography; + +const columns = [ + { title: '用户名', dataIndex: 'username', key: 'username' }, + { title: '姓名', dataIndex: 'name', key: 'name' }, + { title: '邮箱', dataIndex: 'email', key: 'email' }, + { title: '状态', dataIndex: 'status', key: 'status' }, +]; + +const UserManagement = () => { + return ( + + 用户管理 + + + ); +}; + +export default UserManagement; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 02982c7..3974c8e 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,7 +1,9 @@ -import { HomeOutlined, InfoCircleOutlined } from '@ant-design/icons'; -import About from '../pages/About'; -import Home from '../pages/Home'; -import NotFound from '../pages/NotFound'; +import { HomeOutlined, InfoCircleOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons'; +import SystemLayout from '../layouts/SystemLayout'; +import About from '../pages/about'; +import Home from '../pages/home'; +import NotFound from '../pages/not-found'; +import UserManagement from '../pages/system/user'; import type { RouteItem } from './types'; export const routes: RouteItem[] = [ @@ -17,6 +19,20 @@ export const routes: RouteItem[] = [ icon: , component: , }, + { + path: '/system', + label: '系统配置', + icon: , + component: , + children: [ + { + path: '/system/user', + label: '用户管理', + icon: , + component: , + }, + ], + }, { path: '*', label: '404', diff --git a/src/routes/utils.tsx b/src/routes/utils.tsx index c057110..3f9a94b 100644 --- a/src/routes/utils.tsx +++ b/src/routes/utils.tsx @@ -21,3 +21,21 @@ export function toRouteObjects(items: RouteItem[]): RouteObject[] { return route; }); } + +type MenuItem = { + key: string; + icon?: React.ReactNode; + label: string; + children?: MenuItem[]; +}; + +export function toMenuItems(items: RouteItem[]): MenuItem[] { + return items + .filter((r) => !r.hideInMenu) + .map((r) => ({ + key: r.path, + icon: r.icon, + label: r.label, + children: r.children ? toMenuItems(r.children) : undefined, + })); +} -- 2.49.1 From 311ce55fb7e94f0c8ab99db0b05fdfa4317b691b Mon Sep 17 00:00:00 2001 From: xie2can <384968446@qq.com> Date: Fri, 15 May 2026 15:44:27 +0800 Subject: [PATCH 7/7] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=EF=BC=8C=E9=9B=86?= =?UTF-8?q?=E6=88=90=20msw=20mock=EF=BC=8C=E7=BB=9F=E4=B8=80=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=88=AB=E5=90=8D=E5=92=8C=20API=20=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 1 + AGENTS.md | 39 ++- package.json | 8 +- pnpm-lock.yaml | 401 ++++++++++++++++++++++++++++ public/mockServiceWorker.js | 349 ++++++++++++++++++++++++ src/App.tsx | 2 +- src/api/system/user.ts | 55 ++++ src/index.tsx | 24 +- src/layouts/RootLayout.tsx | 4 +- src/mock/index.ts | 7 + src/mock/system.ts | 71 +++++ src/pages/system/user/DeptModal.tsx | 52 ++++ src/pages/system/user/DeptTree.tsx | 214 +++++++++++++++ src/pages/system/user/UserTable.tsx | 66 +++++ src/pages/system/user/index.tsx | 27 +- src/router.tsx | 6 +- src/routes/index.tsx | 10 +- src/utils/request.ts | 6 - tsconfig.json | 5 + 19 files changed, 1307 insertions(+), 40 deletions(-) create mode 100644 public/mockServiceWorker.js create mode 100644 src/api/system/user.ts create mode 100644 src/mock/index.ts create mode 100644 src/mock/system.ts create mode 100644 src/pages/system/user/DeptModal.tsx create mode 100644 src/pages/system/user/DeptTree.tsx create mode 100644 src/pages/system/user/UserTable.tsx diff --git a/.env.example b/.env.example index a237fb8..b470335 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ +# 生产环境后端接口地址 PUBLIC_BASE_URL=http://localhost:8080 diff --git a/AGENTS.md b/AGENTS.md index 66912e0..0533607 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,6 +84,30 @@ tsconfig.json - **单引号**(`.prettierrc` 中 `singleQuote: true`) - ESLint 仅作用于 `**/*.{ts,tsx}`,启用了 `react-hooks` 和 `react-refresh` 插件 +## 注释规范 + +**所有代码都应附带必要的注释,说明意图而非重复代码本身。** + +- 函数 / Hook:用 JSDoc 说明用途、参数含义 +- 复杂逻辑、非直觉的实现:行内注释解释原因 +- 模拟数据 / 临时代码:标注 `// TODO:` 或 `// FIXME:` 方便后续替换 +- 类型字段:用 JSDoc 注释说明每个字段的含义 + +```ts +// ✅ 说明意图 +// 用 cancelled 标志位防止组件卸载后的竞态更新 +let cancelled = false; + +/** + * 根据部门 ID 获取用户列表 + * @param deptKey 部门节点 key + */ +const fetchUsersByDept = (deptKey: string): Promise => { ... }; + +// TODO: 替换为真实接口 +const mockData = [...]; +``` + ## UI 组件规范(antd) **页面 UI 优先使用 antd 组件,不自行实现已有组件的功能。** @@ -109,9 +133,22 @@ import { ConfigProvider } from 'antd'; ## HTTP 请求 - 所有请求通过 `src/utils/request.ts` 封装的方法发出,不直接使用 `axios` -- 导出方法:`get` / `post` / `put` / `del`,返回值类型自动推断为 `API.Response` +- **只使用 `get` 和 `post` 两种方法**,不使用 `put` / `delete` 等其他方法 +- 编辑接口用 `post`,路径加 `/edit` 后缀;删除接口用 `post`,路径加 `/del` 后缀 - 全局响应结构 `API.Response` 定义在 `src/types/http.d.ts`,无需 import 直接使用 - 请求拦截器(添加 token)和响应拦截器(处理错误码)统一在 `request.ts` 中维护 +- 接口函数统一放在 `src/api/` 下,按模块分文件管理(如 `src/api/system/user.ts`) + +**接口函数命名规范:** + +| 操作 | 前缀 | 示例 | +|------|------|------| +| 新增 | `add` | `addDept` | +| 编辑 | `edit` | `editDept` | +| 删除 | `del` | `delDept` | +| 获取列表 | `list` | `listUser` | +| 获取详情 | `detail` | `detailUser` | +| 特殊查询(树等) | 语义命名 | `deptTree` | ```ts // 使用示例 diff --git a/package.json b/package.json index 2724978..a6b11bb 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,14 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "msw": "^2.14.6", "prettier": "^3.8.3", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1" + }, + "msw": { + "workerDirectory": [ + "public" + ] } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2d3950..6587d29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: globals: specifier: ^17.6.0 version: 17.6.0 + msw: + specifier: ^2.14.6 + version: 2.14.6(@types/node@25.8.0)(typescript@6.0.3) prettier: specifier: ^3.8.3 version: 3.8.3 @@ -249,6 +252,41 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@inquirer/ansi@2.0.5': + resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/confirm@6.0.13': + resolution: {integrity: sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.1.10': + resolution: {integrity: sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.5': + resolution: {integrity: sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/type@4.0.5': + resolution: {integrity: sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -265,12 +303,28 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mswjs/interceptors@0.41.9': + resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} + engines: {node: '>=18'} + '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/deferred-promise@3.0.0': + resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@rc-component/async-validator@5.1.0': resolution: {integrity: sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==} engines: {node: '>=14.x'} @@ -658,6 +712,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@25.8.0': + resolution: {integrity: sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -666,6 +723,12 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@typescript-eslint/eslint-plugin@8.59.3': resolution: {integrity: sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -742,6 +805,14 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + antd@6.4.2: resolution: {integrity: sha512-PNJz8Vxc/mC3EsOg/h3e2YuaZduJ1RDp4RmySDuDmKPCxVgyp4Da4kB36o87p9hbLbOWdAWCKQlnyopsN8utKQ==} peerDependencies: @@ -779,10 +850,25 @@ packages: caniuse-lite@1.0.30001792: resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -830,6 +916,9 @@ packages: electron-to-chromium@1.5.356: resolution: {integrity: sha512-9NgFd7m5t5MCJ5rUSjJITUXAH9mEGlrlofnMf4YEr+pz6JlP7cWmTAH+JFmbPnaSW8koVTkuW7pacORWAnA5Yw==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -916,6 +1005,15 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.0: + resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -960,6 +1058,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -980,6 +1082,10 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graphql@16.14.0: + resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -992,6 +1098,9 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + headers-polyfill@5.0.1: + resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -1018,6 +1127,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1025,6 +1138,9 @@ packages: is-mobile@5.0.0: resolution: {integrity: sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1086,6 +1202,20 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.14.6: + resolution: {integrity: sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -1096,6 +1226,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1112,6 +1245,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1162,6 +1298,13 @@ packages: resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1180,6 +1323,9 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1188,12 +1334,35 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-convert@0.2.1: resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + stylis@4.4.0: resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + throttle-debounce@5.0.2: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} @@ -1202,6 +1371,17 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tldts-core@7.0.30: + resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==} + + tldts@7.0.30: + resolution: {integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==} + hasBin: true + + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -1215,6 +1395,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@5.6.0: + resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} + engines: {node: '>=20'} + typescript-eslint@8.59.3: resolution: {integrity: sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1227,6 +1411,12 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -1245,9 +1435,25 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -1481,6 +1687,33 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@inquirer/ansi@2.0.5': {} + + '@inquirer/confirm@6.0.13(@types/node@25.8.0)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@25.8.0) + '@inquirer/type': 4.0.5(@types/node@25.8.0) + optionalDependencies: + '@types/node': 25.8.0 + + '@inquirer/core@11.1.10(@types/node@25.8.0)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@25.8.0) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.0 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 25.8.0 + + '@inquirer/figures@2.0.5': {} + + '@inquirer/type@4.0.5(@types/node@25.8.0)': + optionalDependencies: + '@types/node': 25.8.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1500,6 +1733,15 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mswjs/interceptors@0.41.9': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -1507,6 +1749,17 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/deferred-promise@3.0.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@rc-component/async-validator@5.1.0': dependencies: '@babel/runtime': 7.29.2 @@ -1934,6 +2187,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/node@25.8.0': + dependencies: + undici-types: 7.24.6 + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -1942,6 +2199,12 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 25.8.0 + + '@types/statuses@2.0.6': {} + '@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -2052,6 +2315,12 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + antd@6.4.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@ant-design/colors': 8.0.1 @@ -2143,8 +2412,22 @@ snapshots: caniuse-lite@1.0.30001792: {} + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clsx@2.1.1: {} + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -2181,6 +2464,8 @@ snapshots: electron-to-chromium@1.5.356: {} + emoji-regex@8.0.0: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -2285,6 +2570,16 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.0: + dependencies: + fast-string-width: 3.0.2 + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -2319,6 +2614,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2345,6 +2642,8 @@ snapshots: gopd@1.2.0: {} + graphql@16.14.0: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -2355,6 +2654,11 @@ snapshots: dependencies: function-bind: 1.1.2 + headers-polyfill@5.0.1: + dependencies: + '@types/set-cookie-parser': 2.4.10 + set-cookie-parser: 3.1.0 + hermes-estree@0.25.1: {} hermes-parser@0.25.1: @@ -2376,12 +2680,16 @@ snapshots: is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 is-mobile@5.0.0: {} + is-node-process@1.2.0: {} + isexe@2.0.0: {} js-tokens@4.0.0: {} @@ -2431,6 +2739,33 @@ snapshots: ms@2.1.3: {} + msw@2.14.6(@types/node@25.8.0)(typescript@6.0.3): + dependencies: + '@inquirer/confirm': 6.0.13(@types/node@25.8.0) + '@mswjs/interceptors': 0.41.9 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.0 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.6.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - '@types/node' + + mute-stream@3.0.0: {} + natural-compare@1.4.0: {} node-releases@2.0.44: {} @@ -2444,6 +2779,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + outvariant@1.4.3: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -2456,6 +2793,8 @@ snapshots: path-key@3.1.1: {} + path-to-regexp@6.3.0: {} + picocolors@1.1.1: {} picomatch@4.0.4: {} @@ -2487,6 +2826,10 @@ snapshots: react@19.2.6: {} + require-directory@2.1.1: {} + + rettime@0.11.11: {} + scheduler@0.27.0: {} scroll-into-view-if-needed@3.1.0: @@ -2499,16 +2842,36 @@ snapshots: set-cookie-parser@2.7.2: {} + set-cookie-parser@3.1.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + signal-exit@4.1.0: {} + + statuses@2.0.2: {} + + strict-event-emitter@0.5.1: {} + string-convert@0.2.1: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + stylis@4.4.0: {} + tagged-tag@1.0.0: {} + throttle-debounce@5.0.2: {} tinyglobby@0.2.16: @@ -2516,6 +2879,16 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tldts-core@7.0.30: {} + + tldts@7.0.30: + dependencies: + tldts-core: 7.0.30 + + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.30 + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 @@ -2526,6 +2899,10 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@5.6.0: + dependencies: + tagged-tag: 1.0.0 + typescript-eslint@8.59.3(eslint@10.3.0)(typescript@6.0.3): dependencies: '@typescript-eslint/eslint-plugin': 8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0)(typescript@6.0.3) @@ -2539,6 +2916,10 @@ snapshots: typescript@6.0.3: {} + undici-types@7.24.6: {} + + until-async@3.0.2: {} + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -2555,8 +2936,28 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + y18n@5.0.8: {} + yallist@3.1.1: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} zod-validation-error@4.0.2(zod@4.4.3): diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js new file mode 100644 index 0000000..33dde9e --- /dev/null +++ b/public/mockServiceWorker.js @@ -0,0 +1,349 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.14.6' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/src/App.tsx b/src/App.tsx index ecbf7c2..f072a18 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,7 @@ import zhCN from 'antd/locale/zh_CN'; import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn'; import { RouterProvider } from 'react-router'; -import router from './router'; +import router from '@/router'; import './App.css'; dayjs.locale('zh-cn'); diff --git a/src/api/system/user.ts b/src/api/system/user.ts new file mode 100644 index 0000000..b0f70b4 --- /dev/null +++ b/src/api/system/user.ts @@ -0,0 +1,55 @@ +import { get, post } from '@/utils/request'; + +/** 部门节点数据结构 */ +export interface DeptNode { + key: string; + title: string; + parentKey: string | null; + children?: DeptNode[]; +} + +/** 新增/编辑部门的请求参数 */ +export interface DeptParams { + title: string; + parentKey?: string | null; +} + +/** 用户记录数据结构 */ +export interface UserRecord { + username: string; + name: string; + email: string; + status: string; +} + +/** + * 获取部门树 + */ +export const deptTree = () => get('/api/system/dept/tree'); + +/** + * 新增部门 + * @param params 部门参数 + */ +export const addDept = (params: DeptParams) => post('/api/system/dept/add', params); + +/** + * 编辑部门 + * @param key 部门 key + * @param params 部门参数 + */ +export const editDept = (key: string, params: DeptParams) => + post('/api/system/dept/edit', { key, ...params }); + +/** + * 删除部门 + * @param key 部门 key + */ +export const delDept = (key: string) => post('/api/system/dept/del', { key }); + +/** + * 获取用户列表 + * @param deptKey 部门 key + */ +export const listUser = (deptKey: string) => + get('/api/system/user/list', { deptKey }); diff --git a/src/index.tsx b/src/index.tsx index 55f29bf..8e1f5bc 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,23 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import App from '@/App'; + +async function prepare() { + // 仅开发环境启动 mock service worker + if (import.meta.env.DEV) { + const { default: worker } = await import('@/mock'); + await worker.start({ onUnhandledRequest: 'bypass' }); + } +} const rootEl = document.getElementById('root'); if (rootEl) { - const root = ReactDOM.createRoot(rootEl); - root.render( - - - , - ); + prepare().then(() => { + const root = ReactDOM.createRoot(rootEl); + root.render( + + + , + ); + }); } diff --git a/src/layouts/RootLayout.tsx b/src/layouts/RootLayout.tsx index a0c56ba..9dee121 100644 --- a/src/layouts/RootLayout.tsx +++ b/src/layouts/RootLayout.tsx @@ -2,8 +2,8 @@ import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; import { Layout, Menu } from 'antd'; import { useState } from 'react'; import { Outlet, useLocation, useNavigate } from 'react-router'; -import { routes } from '../routes'; -import { toMenuItems } from '../routes/utils'; +import { routes } from '@/routes'; +import { toMenuItems } from '@/routes/utils'; const { Header, Sider, Content } = Layout; diff --git a/src/mock/index.ts b/src/mock/index.ts new file mode 100644 index 0000000..8571738 --- /dev/null +++ b/src/mock/index.ts @@ -0,0 +1,7 @@ +import { setupWorker } from 'msw/browser'; +import { systemHandlers } from './system'; + +/** 汇总所有模块的 mock handlers */ +const worker = setupWorker(...systemHandlers); + +export default worker; diff --git a/src/mock/system.ts b/src/mock/system.ts new file mode 100644 index 0000000..6fec522 --- /dev/null +++ b/src/mock/system.ts @@ -0,0 +1,71 @@ +import { http, HttpResponse } from 'msw'; + +/** 部门树 mock 数据 */ +const deptList = [ + { + key: '1', + title: '总公司', + parentKey: null, + children: [ + { + key: '1-1', + title: '技术部', + parentKey: '1', + children: [ + { key: '1-1-1', title: '前端组', parentKey: '1-1', children: [] }, + { key: '1-1-2', title: '后端组', parentKey: '1-1', children: [] }, + ], + }, + { key: '1-2', title: '产品部', parentKey: '1', children: [] }, + { key: '1-3', title: '运营部', parentKey: '1', children: [] }, + ], + }, +]; + +/** 用户列表 mock 数据,按部门 key 分组 */ +const userMap: Record = { + '1': [{ username: 'admin', name: '管理员', email: 'admin@example.com', status: '启用' }], + '1-1': [ + { username: 'zhangsan', name: '张三', email: 'zhangsan@example.com', status: '启用' }, + { username: 'lisi', name: '李四', email: 'lisi@example.com', status: '启用' }, + ], + '1-1-1': [{ username: 'wangwu', name: '王五', email: 'wangwu@example.com', status: '启用' }], + '1-1-2': [{ username: 'zhaoliu', name: '赵六', email: 'zhaoliu@example.com', status: '禁用' }], + '1-2': [{ username: 'sunqi', name: '孙七', email: 'sunqi@example.com', status: '启用' }], + '1-3': [], +}; + +/** 包装为统一响应格式 */ +const ok = (data: unknown) => ({ code: '0', msg: 'ok', data, time: Date.now(), ok: true }); + +export const systemHandlers = [ + // 获取部门树 + http.get('/api/system/dept/tree', () => { + return HttpResponse.json(ok(deptList)); + }), + + // 新增部门 + http.post('/api/system/dept/add', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json(ok({ key: `dept_${Date.now()}`, ...body })); + }), + + // 编辑部门 + http.post('/api/system/dept/edit', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json(ok(body)); + }), + + // 删除部门 + http.post('/api/system/dept/del', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json(ok({ key: body.key })); + }), + + // 根据部门获取用户列表 + http.get('/api/system/user/list', ({ request }) => { + const url = new URL(request.url); + const deptKey = url.searchParams.get('deptKey') ?? ''; + return HttpResponse.json(ok(userMap[deptKey] ?? [])); + }), +]; diff --git a/src/pages/system/user/DeptModal.tsx b/src/pages/system/user/DeptModal.tsx new file mode 100644 index 0000000..70aef58 --- /dev/null +++ b/src/pages/system/user/DeptModal.tsx @@ -0,0 +1,52 @@ +import { Form, Input, Modal } from 'antd'; +import { useEffect } from 'react'; + +export interface DeptFormValues { + name: string; +} + +interface DeptModalProps { + /** 弹窗是否可见 */ + open: boolean; + /** 弹窗标题 */ + title: string; + /** 编辑时的初始值,新增时为 undefined */ + initialValues?: DeptFormValues; + /** 确认回调,返回表单数据 */ + onOk: (values: DeptFormValues) => void; + /** 取消回调 */ + onCancel: () => void; +} + +const DeptModal = ({ open, title, initialValues, onOk, onCancel }: DeptModalProps) => { + const [form] = Form.useForm(); + + // 每次打开时重置并填充表单 + useEffect(() => { + if (open) { + form.resetFields(); + if (initialValues) form.setFieldsValue(initialValues); + } + }, [open, initialValues, form]); + + const handleOk = async () => { + const values = await form.validateFields(); + onOk(values); + }; + + return ( + + + + + + + + ); +}; + +export default DeptModal; diff --git a/src/pages/system/user/DeptTree.tsx b/src/pages/system/user/DeptTree.tsx new file mode 100644 index 0000000..fde5711 --- /dev/null +++ b/src/pages/system/user/DeptTree.tsx @@ -0,0 +1,214 @@ +import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Card, Col, Popconfirm, Space, Spin, Tree } from 'antd'; +import type { DataNode } from 'antd/es/tree'; +import { useEffect, useState } from 'react'; +import { + addDept, + delDept, + deptTree, + editDept, + type DeptNode, +} from '@/api/system/user'; +import DeptModal from './DeptModal'; +import type { DeptFormValues } from './DeptModal'; + +/** 单个树节点标题,含悬浮操作按钮 */ +const DeptTreeNode = ({ + node, + onAdd, + onEdit, + onDelete, +}: { + node: DeptNode; + onAdd: (key: string) => void; + onEdit: (node: DeptNode) => void; + onDelete: (key: string) => Promise; +}) => { + const [hovered, setHovered] = useState(false); + + return ( + setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {node.title} + {/* 仅悬浮时显示操作按钮 */} + e.stopPropagation()} + > + } + title="新增子部门" + onClick={() => onAdd(node.key)} + /> + } + title="编辑" + onClick={() => onEdit(node)} + /> + {/* 删除使用气泡卡片二次确认,不打开 Modal */} + onDelete(node.key)} + > + } title="删除" /> + + + + ); +}; + +/** 递归将 DeptNode 转为 antd Tree 的 DataNode */ +const toTreeData = ( + nodes: DeptNode[], + onAdd: (key: string) => void, + onEdit: (node: DeptNode) => void, + onDelete: (key: string) => Promise, +): DataNode[] => + nodes.map((node) => ({ + key: node.key, + title: , + children: node.children ? toTreeData(node.children, onAdd, onEdit, onDelete) : undefined, + })); + +interface DeptTreeProps { + /** 当前选中的部门 key */ + selectedKey: string; + /** 选中部门回调 */ + onSelect: (key: string) => void; +} + +/** 弹窗模式:新增根部门 | 新增子部门 | 编辑 */ +type ModalMode = 'addRoot' | 'addChild' | 'edit'; + +const DeptTree = ({ selectedKey, onSelect }: DeptTreeProps) => { + const [loading, setLoading] = useState(false); + const [deptData, setDeptData] = useState([]); + + // tick 变化时触发部门树重新加载 + const [tick, setTick] = useState(0); + + /** 触发部门树刷新 */ + const loadDeptTree = () => setTick((n) => n + 1); + + useEffect(() => { + let cancelled = false; + + const load = async () => { + setLoading(true); + try { + const res = await deptTree(); + // 用 cancelled 标志位防止卸载后的竞态更新 + if (!cancelled) setDeptData(res.data ?? []); + } finally { + if (!cancelled) setLoading(false); + } + }; + + load(); + return () => { cancelled = true; }; + }, [tick]); + + // 弹窗状态 + const [modalOpen, setModalOpen] = useState(false); + const [modalMode, setModalMode] = useState('addRoot'); + const [modalTitle, setModalTitle] = useState('新增部门'); + const [editingNode, setEditingNode] = useState(null); + const [parentKey, setParentKey] = useState(''); + + /** 打开新增根部门弹窗 */ + const handleAddRoot = () => { + setModalMode('addRoot'); + setModalTitle('新增根部门'); + setEditingNode(null); + setModalOpen(true); + }; + + /** 打开新增子部门弹窗 */ + const handleAddChild = (pKey: string) => { + setModalMode('addChild'); + setModalTitle('新增子部门'); + setParentKey(pKey); + setEditingNode(null); + setModalOpen(true); + }; + + /** 打开编辑弹窗 */ + const handleEdit = (node: DeptNode) => { + setModalMode('edit'); + setModalTitle('编辑部门'); + setEditingNode(node); + setModalOpen(true); + }; + + /** 删除部门,由 Popconfirm 确认后调用 */ + const handleDelete = async (key: string) => { + await delDept(key); + // 删除成功后重新加载部门树 + loadDeptTree(); + }; + + /** Modal 确认,根据模式调用对应接口后刷新部门树 */ + const handleModalOk = async (values: DeptFormValues) => { + if (modalMode === 'addRoot') { + await addDept({ title: values.name, parentKey: null }); + } else if (modalMode === 'addChild') { + await addDept({ title: values.name, parentKey: parentKey }); + } else if (modalMode === 'edit' && editingNode) { + await editDept(editingNode.key, { title: values.name }); + } + setModalOpen(false); + // 操作成功后重新加载部门树 + loadDeptTree(); + }; + + const treeData = toTreeData(deptData, handleAddChild, handleEdit, handleDelete); + + return ( + + } onClick={handleAddRoot}> + 新增 + + } + > + + { + if (keys.length > 0) onSelect(String(keys[0])); + }} + /> + + + + setModalOpen(false)} + /> + + ); +}; + +export default DeptTree; diff --git a/src/pages/system/user/UserTable.tsx b/src/pages/system/user/UserTable.tsx new file mode 100644 index 0000000..cd8a838 --- /dev/null +++ b/src/pages/system/user/UserTable.tsx @@ -0,0 +1,66 @@ +import { Card, Col, Table, Tag } from 'antd'; +import { useEffect, useState } from 'react'; +import { listUser, type UserRecord } from '@/api/system/user'; + +/** 状态值与 Tag 颜色的映射 */ +const statusColorMap: Record = { + 启用: 'success', + 禁用: 'error', +}; + +const columns = [ + { title: '用户名', dataIndex: 'username', key: 'username' }, + { title: '姓名', dataIndex: 'name', key: 'name' }, + { title: '邮箱', dataIndex: 'email', key: 'email' }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (status: string) => ( + {status} + ), + }, +]; + +interface UserTableProps { + deptKey: string; +} + +const UserTable = ({ deptKey }: UserTableProps) => { + const [loading, setLoading] = useState(false); + const [data, setData] = useState([]); + + useEffect(() => { + let cancelled = false; + + const load = async () => { + setLoading(true); + try { + const res = await listUser(deptKey); + // 用 cancelled 标志位防止组件卸载后的竞态更新 + if (!cancelled) setData(res.data ?? []); + } finally { + if (!cancelled) setLoading(false); + } + }; + + load(); + return () => { cancelled = true; }; + }, [deptKey]); + + return ( + + + + + + ); +}; + +export default UserTable; diff --git a/src/pages/system/user/index.tsx b/src/pages/system/user/index.tsx index 673ab1f..0025253 100644 --- a/src/pages/system/user/index.tsx +++ b/src/pages/system/user/index.tsx @@ -1,20 +1,19 @@ -import { Table, Typography } from 'antd'; - -const { Title } = Typography; - -const columns = [ - { title: '用户名', dataIndex: 'username', key: 'username' }, - { title: '姓名', dataIndex: 'name', key: 'name' }, - { title: '邮箱', dataIndex: 'email', key: 'email' }, - { title: '状态', dataIndex: 'status', key: 'status' }, -]; +import { Row } from 'antd'; +import { useState } from 'react'; +import DeptTree from './DeptTree'; +import UserTable from './UserTable'; const UserManagement = () => { + const [selectedDeptKey, setSelectedDeptKey] = useState('1'); + return ( - - 用户管理 - - + + + + ); }; diff --git a/src/router.tsx b/src/router.tsx index 1026a0c..a90c273 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,7 +1,7 @@ import { createBrowserRouter } from 'react-router'; -import RootLayout from './layouts/RootLayout'; -import { routes } from './routes'; -import { toRouteObjects } from './routes/utils'; +import RootLayout from '@/layouts/RootLayout'; +import { routes } from '@/routes'; +import { toRouteObjects } from '@/routes/utils'; const router = createBrowserRouter([ { diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 3974c8e..6c1220e 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,9 +1,9 @@ import { HomeOutlined, InfoCircleOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons'; -import SystemLayout from '../layouts/SystemLayout'; -import About from '../pages/about'; -import Home from '../pages/home'; -import NotFound from '../pages/not-found'; -import UserManagement from '../pages/system/user'; +import SystemLayout from '@/layouts/SystemLayout'; +import About from '@/pages/about'; +import Home from '@/pages/home'; +import NotFound from '@/pages/not-found'; +import UserManagement from '@/pages/system/user'; import type { RouteItem } from './types'; export const routes: RouteItem[] = [ diff --git a/src/utils/request.ts b/src/utils/request.ts index c4bbae2..65783b5 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -34,10 +34,4 @@ export const get = (url: string, params?: object) => export const post = (url: string, data?: object) => request.post>(url, data).then((res) => res.data); -export const put = (url: string, data?: object) => - request.put>(url, data).then((res) => res.data); - -export const del = (url: string, params?: object) => - request.delete>(url, { params }).then((res) => res.data); - export default request; diff --git a/tsconfig.json b/tsconfig.json index 2950d72..7aed00a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,11 @@ "resolveJsonModule": true, "allowImportingTsExtensions": true, + /* 路径别名 */ + "paths": { + "@/*": ["./src/*"] + }, + /* type checking */ "noUnusedLocals": true, "noUnusedParameters": true -- 2.49.1