From ac7615091500909392d65a1a9007b01de7eb02ba Mon Sep 17 00:00:00 2001 From: zihluwang Date: Tue, 14 Apr 2026 11:17:31 +0800 Subject: [PATCH 1/5] feat: implement user authentication with login functionality - Add auth-api for handling login requests. - Update index.ts to export AuthApi. - Modify HeroLayout to display username or login link based on authentication state. - Create LoginPage component for user login. - Update router to include login route with EmptyLayout. - Configure WebClient to include credentials in requests. - Add auth-slice for managing authentication state in Redux. - Update Redux store to include auth reducer. - Define LoginRequest and User types in types/index.ts. - Configure Vite to proxy API requests to the backend server. --- package.json | 10 +- pnpm-lock.yaml | 184 +++++++++++++++---------------- src/api/auth-api.ts | 9 ++ src/api/index.ts | 1 + src/layout/hero-layout/index.tsx | 14 +++ src/page/login/index.tsx | 67 +++++++++++ src/router/index.tsx | 11 ++ src/shared/web-client/index.ts | 1 + src/store/auth-slice.ts | 26 +++++ src/store/index.ts | 4 +- src/types/index.ts | 11 ++ vite.config.ts | 9 ++ 12 files changed, 249 insertions(+), 98 deletions(-) create mode 100644 src/api/auth-api.ts create mode 100644 src/page/login/index.tsx create mode 100644 src/store/auth-slice.ts diff --git a/package.json b/package.json index ee3fbfd..a88e178 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,13 @@ "@tailwindcss/vite": "^4.2.2", "@tanstack/react-virtual": "^3.13.23", "antd": "^6.3.5", - "axios": "^1.14.0", + "axios": "^1.15.0", "dayjs": "^1.11.20", "react": "^19.2.5", "react-dom": "^19.2.5", "react-redux": "^9.2.0", - "react-router": "^7.14.0", - "react-router-dom": "^7.14.0", + "react-router": "^7.14.1", + "react-router-dom": "^7.14.1", "redux-persist": "^6.0.0", "tailwindcss": "^4.2.2" }, @@ -33,8 +33,8 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", - "globals": "^17.4.0", - "prettier": "^3.8.1", + "globals": "^17.5.0", + "prettier": "^3.8.2", "typescript": "~6.0.2", "vite": "^8.0.8" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 631d2f9..ef94ac3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^6.3.5 version: 6.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5) axios: - specifier: ^1.14.0 - version: 1.14.0 + specifier: ^1.15.0 + version: 1.15.0 dayjs: specifier: ^1.11.20 version: 1.11.20 @@ -39,11 +39,11 @@ importers: specifier: ^9.2.0 version: 9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1) react-router: - specifier: ^7.14.0 - version: 7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^7.14.1 + version: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-router-dom: - specifier: ^7.14.0 - version: 7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^7.14.1 + version: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) redux-persist: specifier: ^6.0.0 version: 6.0.0(react@19.2.5)(redux@5.0.1) @@ -64,11 +64,11 @@ importers: specifier: ^6.0.1 version: 6.0.1(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.4)(jiti@2.6.1)) globals: - specifier: ^17.4.0 - version: 17.4.0 + specifier: ^17.5.0 + version: 17.5.0 prettier: - specifier: ^3.8.1 - version: 3.8.1 + specifier: ^3.8.2 + version: 3.8.2 typescript: specifier: ~6.0.2 version: 6.0.2 @@ -372,8 +372,8 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/image@1.8.0': - resolution: {integrity: sha512-Dr41bFevLB5NgVaJhEUmNvbEf+ynAhim6W98ZW2xvCsdFISc2TYP4ZvCVdie3eaZdum2kieVcvpNHu+UrzAAHA==} + '@rc-component/image@1.8.1': + resolution: {integrity: sha512-JfPCijmMl+EaMvbftsEs/4VHmTyJKsZBh5ujFowSA45i9NTVYS1vuHtgpVV/QrGa27kXwbVOZriffCe/PNKuMw==} peerDependencies: react: '>=16.9.0' react-dom: '>=16.9.0' @@ -583,8 +583,8 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/util@1.10.0': - resolution: {integrity: sha512-aY9GLBuiUdpyfIUpAWSYer4Tu3mVaZCo5A0q9NtXcazT3MRiI3/WNHCR+DUn5VAtR6iRRf0ynCqQUcHli5UdYw==} + '@rc-component/util@1.10.1': + resolution: {integrity: sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' @@ -856,8 +856,8 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axios@1.14.0: - resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==} + axios@1.15.0: + resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} @@ -930,8 +930,8 @@ packages: picomatch: optional: true - follow-redirects@1.15.11: - resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -959,8 +959,8 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - globals@17.4.0: - resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} + globals@17.5.0: + resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} engines: {node: '>=18'} gopd@1.2.0: @@ -1096,12 +1096,12 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.9: + resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} - prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + prettier@3.8.2: + resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==} engines: {node: '>=14'} hasBin: true @@ -1129,15 +1129,15 @@ packages: redux: optional: true - react-router-dom@7.14.0: - resolution: {integrity: sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==} + react-router-dom@7.14.1: + resolution: {integrity: sha512-ZkrQuwwhGibjQLqH1eCdyiZyLWglPxzxdl5tgwgKEyCSGC76vmAjleGocRe3J/MLfzMUIKwaFJWpFVJhK3d2xA==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' react-dom: '>=18' - react-router@7.14.0: - resolution: {integrity: sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==} + react-router@7.14.1: + resolution: {integrity: sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -1197,16 +1197,16 @@ packages: tailwindcss@4.2.2: resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} engines: {node: '>=6'} throttle-debounce@5.0.2: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} tslib@2.8.1: @@ -1278,7 +1278,7 @@ snapshots: dependencies: '@ant-design/cssinjs': 2.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@babel/runtime': 7.29.2 - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1287,7 +1287,7 @@ snapshots: '@babel/runtime': 7.29.2 '@emotion/hash': 0.8.0 '@emotion/unitless': 0.7.5 - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 csstype: 3.2.3 react: 19.2.5 @@ -1302,7 +1302,7 @@ snapshots: dependencies: '@ant-design/colors': 8.0.1 '@ant-design/icons-svg': 4.4.2 - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1452,14 +1452,14 @@ snapshots: dependencies: '@rc-component/select': 1.6.15(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/tree': 1.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) '@rc-component/checkbox@2.0.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1468,7 +1468,7 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 '@rc-component/motion': 1.3.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1476,14 +1476,14 @@ snapshots: '@rc-component/color-picker@3.1.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@ant-design/fast-color': 3.0.1 - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) '@rc-component/context@2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1491,7 +1491,7 @@ snapshots: dependencies: '@rc-component/motion': 1.3.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/portal': 2.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1500,7 +1500,7 @@ snapshots: dependencies: '@rc-component/motion': 1.3.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/portal': 2.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1508,7 +1508,7 @@ snapshots: '@rc-component/dropdown@1.0.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@rc-component/trigger': 3.9.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1516,16 +1516,16 @@ snapshots: '@rc-component/form@1.8.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@rc-component/async-validator': 5.1.0 - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@rc-component/image@1.8.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@rc-component/image@1.8.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@rc-component/motion': 1.3.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/portal': 2.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1533,14 +1533,14 @@ snapshots: '@rc-component/input-number@1.6.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@rc-component/mini-decimal': 1.1.3 - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) '@rc-component/input@1.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1551,7 +1551,7 @@ snapshots: '@rc-component/menu': 1.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/textarea': 1.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/trigger': 3.9.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1561,7 +1561,7 @@ snapshots: '@rc-component/motion': 1.3.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/overflow': 1.0.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/trigger': 3.9.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1572,21 +1572,21 @@ snapshots: '@rc-component/motion@1.3.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) '@rc-component/mutate-observer@2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) '@rc-component/notification@1.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@rc-component/motion': 1.3.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1595,14 +1595,14 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 '@rc-component/resize-observer': 1.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) '@rc-component/pagination@1.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1612,7 +1612,7 @@ snapshots: '@rc-component/overflow': 1.0.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/resize-observer': 1.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/trigger': 3.9.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1621,14 +1621,14 @@ snapshots: '@rc-component/portal@2.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) '@rc-component/progress@1.0.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1641,14 +1641,14 @@ snapshots: '@rc-component/rate@1.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) '@rc-component/resize-observer@1.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1656,7 +1656,7 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 '@rc-component/motion': 1.3.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1665,7 +1665,7 @@ snapshots: dependencies: '@rc-component/overflow': 1.0.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/trigger': 3.9.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/virtual-list': 1.0.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 @@ -1673,21 +1673,21 @@ snapshots: '@rc-component/slider@1.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) '@rc-component/steps@1.2.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) '@rc-component/switch@1.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1696,7 +1696,7 @@ snapshots: dependencies: '@rc-component/context': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/resize-observer': 1.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/virtual-list': 1.0.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 @@ -1708,7 +1708,7 @@ snapshots: '@rc-component/menu': 1.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/motion': 1.3.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/resize-observer': 1.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1717,7 +1717,7 @@ snapshots: dependencies: '@rc-component/input': 1.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/resize-observer': 1.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1725,7 +1725,7 @@ snapshots: '@rc-component/tooltip@1.4.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@rc-component/trigger': 3.9.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1734,7 +1734,7 @@ snapshots: dependencies: '@rc-component/portal': 2.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/trigger': 3.9.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1743,7 +1743,7 @@ snapshots: dependencies: '@rc-component/select': 1.6.15(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/tree': 1.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1751,7 +1751,7 @@ snapshots: '@rc-component/tree@1.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@rc-component/motion': 1.3.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/virtual-list': 1.0.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 @@ -1762,19 +1762,19 @@ snapshots: '@rc-component/motion': 1.3.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/portal': 2.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/resize-observer': 1.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) '@rc-component/upload@1.1.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@rc-component/util@1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@rc-component/util@1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: is-mobile: 5.0.0 react: 19.2.5 @@ -1785,7 +1785,7 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 '@rc-component/resize-observer': 1.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -1976,7 +1976,7 @@ snapshots: '@rc-component/drawer': 1.4.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/dropdown': 1.0.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/form': 1.8.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/image': 1.8.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/image': 1.8.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/input': 1.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/input-number': 1.6.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/mentions': 1.6.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -2004,7 +2004,7 @@ snapshots: '@rc-component/tree-select': 1.8.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/trigger': 3.9.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@rc-component/upload': 1.1.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@rc-component/util': 1.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rc-component/util': 1.10.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) clsx: 2.1.1 dayjs: 1.11.20 react: 19.2.5 @@ -2018,9 +2018,9 @@ snapshots: asynckit@0.4.0: {} - axios@1.14.0: + axios@1.15.0: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.16.0 form-data: 4.0.5 proxy-from-env: 2.1.0 transitivePeerDependencies: @@ -2058,7 +2058,7 @@ snapshots: enhanced-resolve@5.20.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.0 + tapable: 2.3.2 es-define-property@1.0.1: {} @@ -2109,7 +2109,7 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - follow-redirects@1.15.11: {} + follow-redirects@1.16.0: {} form-data@4.0.5: dependencies: @@ -2142,7 +2142,7 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - globals@17.4.0: {} + globals@17.5.0: {} gopd@1.2.0: {} @@ -2235,13 +2235,13 @@ snapshots: picomatch@4.0.4: {} - postcss@8.5.8: + postcss@8.5.9: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.8.1: {} + prettier@3.8.2: {} proxy-from-env@2.1.0: {} @@ -2261,13 +2261,13 @@ snapshots: '@types/react': 19.2.14 redux: 5.0.1 - react-router-dom@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + react-router-dom@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - react-router: 7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react-router: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react-router@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: cookie: 1.1.1 react: 19.2.5 @@ -2328,11 +2328,11 @@ snapshots: tailwindcss@4.2.2: {} - tapable@2.3.0: {} + tapable@2.3.2: {} throttle-debounce@5.0.2: {} - tinyglobby@0.2.15: + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 @@ -2352,9 +2352,9 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.8 + postcss: 8.5.9 rolldown: 1.0.0-rc.15 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 optionalDependencies: '@types/node': 22.19.17 esbuild: 0.27.4 diff --git a/src/api/auth-api.ts b/src/api/auth-api.ts new file mode 100644 index 0000000..745e60d --- /dev/null +++ b/src/api/auth-api.ts @@ -0,0 +1,9 @@ +import { LoginRequest, User } from "@/types" +import { WebClient } from "@/shared/web-client" + +export async function login(loginRequest: LoginRequest): Promise { + const { data } = await WebClient.post("/auth/login", { + ...loginRequest, + }) + return data +} diff --git a/src/api/index.ts b/src/api/index.ts index eb0551b..edb4561 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,3 +1,4 @@ export * as FirearmApi from "./firearm-api" export * as ModificationApi from "./modification-api" export * as TagApi from "./tag-api" +export * as AuthApi from "./auth-api" diff --git a/src/layout/hero-layout/index.tsx b/src/layout/hero-layout/index.tsx index 113005e..d1ef79c 100644 --- a/src/layout/hero-layout/index.tsx +++ b/src/layout/hero-layout/index.tsx @@ -1,6 +1,7 @@ import { Outlet, Link } from "react-router-dom" import { useMemo } from "react" import dayjs from "dayjs" +import { useAppSelector } from "@/store" /** * Main application component that serves as the root layout. @@ -8,6 +9,7 @@ import dayjs from "dayjs" */ export default function HeroLayout() { const today = useMemo(() => dayjs(), []) + const user = useAppSelector((state) => state.auth.user) return (
@@ -33,6 +35,18 @@ export default function HeroLayout() { > 改枪码 + {user ? ( + + {user.username} + + ) : ( + + 登录 + + )} +
+ + + 登录 + + + 使用你的帐号登录后即可继续操作 + + + layout="vertical" onFinish={onFinish} requiredMark={false}> + + name="principle" + label="帐号" + rules={[{ required: true, message: "请输入帐号" }]} + > + + + + + name="credential" + label="密码" + rules={[{ required: true, message: "请输入密码" }]} + > + + + + + + + + +
+
+ ) +} diff --git a/src/router/index.tsx b/src/router/index.tsx index b506a3c..1a23589 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -1,6 +1,7 @@ import { ComponentType } from "react" import { createBrowserRouter } from "react-router-dom" import ErrorPage from "@/components/error-page" +import EmptyLayout from "@/layout/empty-layout" import HeroLayout from "@/layout/hero-layout" function lazy }>(importer: () => Promise) { @@ -37,6 +38,16 @@ const router = createBrowserRouter( }, ], }, + { + element: , + errorElement: , + children: [ + { + path: "login", + lazy: lazy(() => import("@/page/login")), + }, + ], + }, ], { basename: "/", diff --git a/src/shared/web-client/index.ts b/src/shared/web-client/index.ts index b6b2081..a797175 100644 --- a/src/shared/web-client/index.ts +++ b/src/shared/web-client/index.ts @@ -4,6 +4,7 @@ import dayjs from "dayjs" const WebClient = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: dayjs.duration({ seconds: 10 }).asMilliseconds(), + withCredentials: true }) export { WebClient } diff --git a/src/store/auth-slice.ts b/src/store/auth-slice.ts new file mode 100644 index 0000000..71dc980 --- /dev/null +++ b/src/store/auth-slice.ts @@ -0,0 +1,26 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit" +import { User } from "@/types" + +interface AuthState { + user: User | null +} + +const initialState: AuthState = { + user: null, +} + +const authSlice = createSlice({ + name: "auth", + initialState, + reducers: { + setCurrentUser(state, action: PayloadAction) { + state.user = action.payload + }, + clearCurrentUser(state) { + state.user = null + }, + }, +}) + +export const { setCurrentUser, clearCurrentUser } = authSlice.actions +export const authReducer = authSlice.reducer diff --git a/src/store/index.ts b/src/store/index.ts index 8e33ca8..aa18f6b 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -11,6 +11,7 @@ import { REGISTER, } from "redux-persist" import createWebStorage from "redux-persist/es/storage/createWebStorage" +import { authReducer } from "./auth-slice" import { firearmsReducer } from "./firearms-slice" const storage = createWebStorage(import.meta.env.VITE_REDUX_STORAGE ?? "local") @@ -18,10 +19,11 @@ const storage = createWebStorage(import.meta.env.VITE_REDUX_STORAGE ?? "local") const persistConfig = { key: "root", storage, - whitelist: ["firearms"], + whitelist: ["auth", "firearms"], } const rootReducer = combineReducers({ + auth: authReducer, firearms: firearmsReducer }) diff --git a/src/types/index.ts b/src/types/index.ts index 94c898b..76ccf4a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -47,3 +47,14 @@ export interface PageQueryParams { sortBy?: string direction?: Direction } + +export interface LoginRequest { + principle: string + credential: string +} + +export interface User { + id: number + username: string + email: string +} diff --git a/vite.config.ts b/vite.config.ts index 45b6aba..4eec181 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,4 +12,13 @@ export default defineConfig({ "@": fileURLToPath(new URL("./src", import.meta.url)), }, }, + server: { + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, '') + } + } + } }) From d6b8d12b2e5caf29ef737ab5f4a2e4d71a46e6f0 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Tue, 14 Apr 2026 11:57:43 +0800 Subject: [PATCH 2/5] feat: add user-based conditional rendering for firearms and mod codes pages --- src/page/firearms/index.tsx | 9 +++++ src/page/mod-codes/index.tsx | 70 +++++++++++++++++++++--------------- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/page/firearms/index.tsx b/src/page/firearms/index.tsx index ff63e20..611041f 100644 --- a/src/page/firearms/index.tsx +++ b/src/page/firearms/index.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react" import { Link } from "react-router-dom" import { FirearmApi } from "@/api" +import { useAppSelector } from "@/store" import { Firearm, FirearmType } from "@/types" import { Button, Card, Col, Pagination, Row, Select, Tag, Typography } from "antd" @@ -23,6 +24,7 @@ function asDps(fireRate: number, damage: number) { } export default function FirearmsPage() { + const user = useAppSelector((state) => state.auth.user) const [page, setPage] = useState(1) const [typeFilter, setTypeFilter] = useState(allTypeValue) const [firearms, setFirearms] = useState([]) @@ -66,6 +68,13 @@ export default function FirearmsPage() { + 编辑 + + ) : null + } variant="outlined" styles={{ header: { minHeight: 56 }, diff --git a/src/page/mod-codes/index.tsx b/src/page/mod-codes/index.tsx index c18f29b..fa122b4 100644 --- a/src/page/mod-codes/index.tsx +++ b/src/page/mod-codes/index.tsx @@ -2,11 +2,13 @@ import { useEffect, useMemo, useState } from "react" import { Link, useSearchParams } from "react-router-dom" import { Button, Card, Col, Pagination, Row, Select, Space, Tag, Typography } from "antd" import { ModificationApi, TagApi } from "@/api" +import { useAppSelector } from "@/store" import { Modification } from "@/types" const pageSize = 12 export default function ModCodesPage() { + const user = useAppSelector((state) => state.auth.user) const [searchParams] = useSearchParams() const firearmId = useMemo(() => searchParams.get("firearmId") || undefined, [searchParams]) @@ -47,38 +49,41 @@ export default function ModCodesPage() { return ( <> -
+
改枪码列表 - - 标签: - - mode="multiple" - allowClear - placeholder="请选择标签" - className="w-64" - value={selectedTags} - options={tagOptions.map((tag) => ({ value: tag, label: tag }))} - onChange={(values) => { - setSelectedTags(values) - }} - /> - {firearmId && 武器 ID: {firearmId}} - {(firearmId || selectedTags.length > 0) && ( - - - - )} - +
+ + 标签: + + mode="multiple" + allowClear + placeholder="请选择标签" + className="w-64" + value={selectedTags} + options={tagOptions.map((tag) => ({ value: tag, label: tag }))} + onChange={(values) => { + setSelectedTags(values) + }} + /> + {firearmId && 武器 ID: {firearmId}} + {(firearmId || selectedTags.length > 0) && ( + + + + )} + + {user && } +
@@ -87,6 +92,13 @@ export default function ModCodesPage() { + 编辑 + + ) : null + } variant="outlined" styles={{ header: { minHeight: 56 }, From a2e3676d0526aec04f1d9f376c5fb93eef9bb1dc Mon Sep 17 00:00:00 2001 From: zihluwang Date: Tue, 21 Apr 2026 11:28:07 +0800 Subject: [PATCH 3/5] feat: add logout functionality with dropdown menu for user authentication --- src/api/auth-api.ts | 4 ++++ src/layout/hero-layout/index.tsx | 34 ++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/api/auth-api.ts b/src/api/auth-api.ts index 745e60d..1727cde 100644 --- a/src/api/auth-api.ts +++ b/src/api/auth-api.ts @@ -7,3 +7,7 @@ export async function login(loginRequest: LoginRequest): Promise { }) return data } + +export async function logout() { + await WebClient.get("/auth/logout") +} diff --git a/src/layout/hero-layout/index.tsx b/src/layout/hero-layout/index.tsx index d1ef79c..6607963 100644 --- a/src/layout/hero-layout/index.tsx +++ b/src/layout/hero-layout/index.tsx @@ -1,7 +1,10 @@ import { Outlet, Link } from "react-router-dom" import { useMemo } from "react" import dayjs from "dayjs" -import { useAppSelector } from "@/store" +import { Dropdown } from "antd" +import { AuthApi } from "@/api" +import { useAppDispatch, useAppSelector } from "@/store" +import { clearCurrentUser } from "@/store/auth-slice" /** * Main application component that serves as the root layout. @@ -10,6 +13,15 @@ import { useAppSelector } from "@/store" export default function HeroLayout() { const today = useMemo(() => dayjs(), []) const user = useAppSelector((state) => state.auth.user) + const dispatch = useAppDispatch() + + async function handleLogout() { + try { + await AuthApi.logout() + } finally { + dispatch(clearCurrentUser()) + } + } return (
@@ -36,9 +48,23 @@ export default function HeroLayout() { 改枪码 {user ? ( - - {user.username} - + + + {user.username} + + ) : ( Date: Tue, 21 Apr 2026 14:30:13 +0800 Subject: [PATCH 4/5] feat: add functionality to create, edit, and remove firearms --- src/api/firearm-api.ts | 20 +++++++++++++++++++- src/types/index.ts | 4 +++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/api/firearm-api.ts b/src/api/firearm-api.ts index f2f8e0b..9b0bbba 100644 --- a/src/api/firearm-api.ts +++ b/src/api/firearm-api.ts @@ -1,4 +1,4 @@ -import { Direction, Firearm, FirearmType, Page, PageQueryParams } from "@/types" +import { AddFirearmRequest, Direction, Firearm, FirearmType, Page, PageQueryParams } from "@/types" import { WebClient } from "@/shared/web-client" import { asUrlSearchParam } from "@/utils/query-param-utils.ts" @@ -36,3 +36,21 @@ export async function getFirearm(id: number): Promise { const { data } = await WebClient.get(`/firearms/${id}`) return data } + +/** + * 新建武器 + * @param request + */ +export async function addFirearm(request: AddFirearmRequest): Promise { + const { data } = await WebClient.post("/firearms", request) + return data +} + +export async function editFirearm(id: number, request: AddFirearmRequest): Promise { + const { data } = await WebClient.put(`/firearms/${id}`, request) + return data +} + +export async function removeFirearm(id: number) { + await WebClient.delete(`/firearms/${id}`) +} diff --git a/src/types/index.ts b/src/types/index.ts index 76ccf4a..ae82bae 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -27,9 +27,11 @@ export interface Firearm { fireRate: number armourDamage: number bodyDamage: number - review: string + review: string | null } +export interface AddFirearmRequest extends Omit {} + export interface Modification { id: number firearmId: number From 745c98bc206416578c140ad23463289d04e775de Mon Sep 17 00:00:00 2001 From: zihluwang Date: Tue, 21 Apr 2026 23:40:00 +0800 Subject: [PATCH 5/5] feat: add create and edit modals for firearms management --- src/components/firearm-create-modal/index.tsx | 58 ++++++++++++ src/components/firearm-edit-modal/index.tsx | 67 ++++++++++++++ src/components/firearm-form/index.tsx | 91 +++++++++++++++++++ src/page/firearms/index.tsx | 83 +++++++++++++++-- 4 files changed, 291 insertions(+), 8 deletions(-) create mode 100644 src/components/firearm-create-modal/index.tsx create mode 100644 src/components/firearm-edit-modal/index.tsx create mode 100644 src/components/firearm-form/index.tsx diff --git a/src/components/firearm-create-modal/index.tsx b/src/components/firearm-create-modal/index.tsx new file mode 100644 index 0000000..100e571 --- /dev/null +++ b/src/components/firearm-create-modal/index.tsx @@ -0,0 +1,58 @@ +import { useState } from "react" +import { App, Form, Modal } from "antd" +import { FirearmApi } from "@/api" +import FirearmForm from "@/components/firearm-form" +import { AddFirearmRequest, Firearm } from "@/types" + +interface FirearmCreateModalProps { + open: boolean + onCancel: () => void + onSuccess: (firearm: Firearm) => void +} + +function normalizeRequest(values: AddFirearmRequest): AddFirearmRequest { + return { + ...values, + review: values.review?.trim() || null, + } +} + +export default function FirearmCreateModal({ open, onCancel, onSuccess }: FirearmCreateModalProps) { + const { message } = App.useApp() + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + + async function onFinish(values: AddFirearmRequest) { + setLoading(true) + try { + const firearm = await FirearmApi.addFirearm(normalizeRequest(values)) + message.success("武器创建成功") + form.resetFields() + onSuccess(firearm) + } catch { + message.error("武器创建失败,请稍后重试") + } finally { + setLoading(false) + } + } + + return ( + form.submit()} + okText="创建" + cancelText="取消" + confirmLoading={loading} + destroyOnHidden + afterOpenChange={(visible) => { + if (!visible) { + form.resetFields() + } + }}> + + + ) +} + diff --git a/src/components/firearm-edit-modal/index.tsx b/src/components/firearm-edit-modal/index.tsx new file mode 100644 index 0000000..c0c1b49 --- /dev/null +++ b/src/components/firearm-edit-modal/index.tsx @@ -0,0 +1,67 @@ +import { useEffect, useState } from "react" +import { App, Form, Modal } from "antd" +import { FirearmApi } from "@/api" +import FirearmForm from "@/components/firearm-form" +import { AddFirearmRequest, Firearm } from "@/types" + +interface FirearmEditModalProps { + open: boolean + firearm: Firearm | null + onCancel: () => void + onSuccess: (firearm: Firearm) => void +} + +function normalizeRequest(values: AddFirearmRequest): AddFirearmRequest { + return { + ...values, + review: values.review?.trim() || null, + } +} + +export default function FirearmEditModal({ open, firearm, onCancel, onSuccess }: FirearmEditModalProps) { + const { message } = App.useApp() + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + + useEffect(() => { + if (!open || !firearm) { + return + } + + const { id: _id, ...editableValues } = firearm + form.setFieldsValue(editableValues) + }, [open, firearm, form]) + + async function onFinish(values: AddFirearmRequest) { + if (!firearm) { + return + } + + setLoading(true) + try { + const updated = await FirearmApi.editFirearm(firearm.id, normalizeRequest(values)) + message.success("武器更新成功") + onSuccess(updated) + } catch { + message.error("武器更新失败,请稍后重试") + } finally { + setLoading(false) + } + } + + return ( + form.submit()} + okText="保存" + cancelText="取消" + confirmLoading={loading} + destroyOnHidden + > + + + ) +} + diff --git a/src/components/firearm-form/index.tsx b/src/components/firearm-form/index.tsx new file mode 100644 index 0000000..476218e --- /dev/null +++ b/src/components/firearm-form/index.tsx @@ -0,0 +1,91 @@ +import { Form, Input, InputNumber, Select } from "antd" +import { AddFirearmRequest, FirearmType } from "@/types" + +const firearmTypeText: Record = { + RIFLE: "步枪", + SUB_MACHINE_GUN: "冲锋枪", + SHOTGUN: "霰弹枪", + LIGHT_MACHINE_GUN: "轻机枪", + DESIGNATED_MARKSMAN_RIFLE: "射手步枪", + SNIPER_RIFLE: "狙击步枪", + PISTOL: "手枪", + SPECIAL: "特殊", +} + +interface FirearmFormProps { + form: ReturnType>[0] + onFinish: (values: AddFirearmRequest) => void +} + +export default function FirearmForm({ form, onFinish }: FirearmFormProps) { + return ( + form={form} layout="vertical" onFinish={onFinish} requiredMark={false}> + + name="name" + label="武器名称" + rules={[{ required: true, message: "请输入武器名称" }]} + > + + + + + name="type" + label="武器类型" + rules={[{ required: true, message: "请选择武器类型" }]} + > + + + + + name="calibre" + label="子弹口径" + rules={[{ required: true, message: "请输入子弹口径" }]} + > + + + + + name="fireRate" + label="射速(每分钟发数)" + rules={[{ required: true, message: "请输入射速" }]} + > + + + + + name="armourDamage" + label="甲伤" + rules={[{ required: true, message: "请输入甲伤" }]} + > + + + + + name="bodyDamage" + label="肉伤" + rules={[{ required: true, message: "请输入肉伤" }]} + > + + + + name="review" label="描述"> + + + + ) +} + diff --git a/src/page/firearms/index.tsx b/src/page/firearms/index.tsx index 611041f..1824770 100644 --- a/src/page/firearms/index.tsx +++ b/src/page/firearms/index.tsx @@ -1,9 +1,11 @@ -import { useEffect, useState } from "react" +import { useCallback, useEffect, useState } from "react" import { Link } from "react-router-dom" import { FirearmApi } from "@/api" +import FirearmCreateModal from "@/components/firearm-create-modal" +import FirearmEditModal from "@/components/firearm-edit-modal" import { useAppSelector } from "@/store" import { Firearm, FirearmType } from "@/types" -import { Button, Card, Col, Pagination, Row, Select, Tag, Typography } from "antd" +import { Button, Card, Col, Pagination, Popconfirm, Row, Select, Tag, Typography, App } from "antd" const firearmTypeText: Record = { RIFLE: "步枪", @@ -25,13 +27,17 @@ function asDps(fireRate: number, damage: number) { export default function FirearmsPage() { const user = useAppSelector((state) => state.auth.user) + const { message } = App.useApp() const [page, setPage] = useState(1) const [typeFilter, setTypeFilter] = useState(allTypeValue) const [firearms, setFirearms] = useState([]) const [total, setTotal] = useState(0) + const [createModalOpen, setCreateModalOpen] = useState(false) + const [editingFirearm, setEditingFirearm] = useState(null) + const [deletingId, setDeletingId] = useState(null) - useEffect(() => { - FirearmApi.getFirearms({ + const loadFirearms = useCallback(() => { + return FirearmApi.getFirearms({ page: page - 1, size: 12, sortBy: "id", @@ -43,9 +49,37 @@ export default function FirearmsPage() { }) }, [page, typeFilter]) + useEffect(() => { + loadFirearms() + }, [loadFirearms]) + + async function handleDelete(firearm: Firearm) { + setDeletingId(firearm.id) + try { + await FirearmApi.removeFirearm(firearm.id) + message.success("武器删除成功") + if (firearms.length === 1 && page > 1) { + setPage(page - 1) + } else { + loadFirearms() + } + } catch { + message.error("武器删除失败,请稍后重试") + } finally { + setDeletingId(null) + } + } + return ( <> -
+
+
+ {user && ( + + )} +
className="w-full sm:w-64" value={typeFilter} @@ -70,9 +104,23 @@ export default function FirearmsPage() { title={firearm.name} extra={ user ? ( - +
+ + handleDelete(firearm)} + > + + +
) : null } variant="outlined" @@ -136,6 +184,25 @@ export default function FirearmsPage() { showSizeChanger={false} />
+ + setCreateModalOpen(false)} + onSuccess={() => { + setCreateModalOpen(false) + loadFirearms() + }} + /> + + setEditingFirearm(null)} + onSuccess={() => { + setEditingFirearm(null) + loadFirearms() + }} + /> ) }