From ac7615091500909392d65a1a9007b01de7eb02ba Mon Sep 17 00:00:00 2001 From: zihluwang Date: Tue, 14 Apr 2026 11:17:31 +0800 Subject: [PATCH 01/13] 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 02/13] 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 088b0e87ce28a64cd56a23b575b08d051e87da85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:57:56 +0000 Subject: [PATCH 03/13] chore: bump the dependency-updates group with 5 updates Bumps the dependency-updates group with 5 updates: | Package | From | To | | --- | --- | --- | | [axios](https://github.com/axios/axios) | `1.14.0` | `1.15.0` | | [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.14.0` | `7.14.1` | | [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.14.0` | `7.14.1` | | [globals](https://github.com/sindresorhus/globals) | `17.4.0` | `17.5.0` | | [prettier](https://github.com/prettier/prettier) | `3.8.1` | `3.8.3` | Updates `axios` from 1.14.0 to 1.15.0 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.14.0...v1.15.0) Updates `react-router` from 7.14.0 to 7.14.1 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.14.1/packages/react-router) Updates `react-router-dom` from 7.14.0 to 7.14.1 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.14.1/packages/react-router-dom) Updates `globals` from 17.4.0 to 17.5.0 - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v17.4.0...v17.5.0) Updates `prettier` from 3.8.1 to 3.8.3 - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.8.1...3.8.3) --- updated-dependencies: - dependency-name: axios dependency-version: 1.15.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependency-updates - dependency-name: react-router dependency-version: 7.14.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependency-updates - dependency-name: react-router-dom dependency-version: 7.14.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependency-updates - dependency-name: globals dependency-version: 17.5.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependency-updates - dependency-name: prettier dependency-version: 3.8.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependency-updates ... Signed-off-by: dependabot[bot] --- package.json | 10 +++---- pnpm-lock.yaml | 74 ++++++++++++++++++++------------------------------ 2 files changed, 35 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index ee3fbfd..1a7814c 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.3", "typescript": "~6.0.2", "vite": "^8.0.8" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 631d2f9..67b8091 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.3 + version: 3.8.3 typescript: specifier: ~6.0.2 version: 6.0.2 @@ -642,42 +642,36 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} @@ -752,28 +746,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.2': resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} @@ -856,8 +846,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 +920,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 +949,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: @@ -1030,28 +1020,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1100,8 +1086,8 @@ packages: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} - prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} hasBin: true @@ -1129,15 +1115,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' @@ -2018,9 +2004,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: @@ -2109,7 +2095,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 +2128,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: {} @@ -2241,7 +2227,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.8.1: {} + prettier@3.8.3: {} proxy-from-env@2.1.0: {} @@ -2261,13 +2247,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 From a2e3676d0526aec04f1d9f376c5fb93eef9bb1dc Mon Sep 17 00:00:00 2001 From: zihluwang Date: Tue, 21 Apr 2026 11:28:07 +0800 Subject: [PATCH 04/13] 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 05/13] 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 06/13] 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() + }} + /> ) } From ff487064a27f0a50f128f9723e5e868e505646a2 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Wed, 22 Apr 2026 16:52:09 +0800 Subject: [PATCH 07/13] refactor: split interfaces by module --- src/types/auth.ts | 11 +++++++ src/types/common.ts | 17 ++++++++++ src/types/firearm.ts | 24 ++++++++++++++ src/types/index.ts | 66 +++------------------------------------ src/types/modification.ts | 12 +++++++ 5 files changed, 68 insertions(+), 62 deletions(-) create mode 100644 src/types/auth.ts create mode 100644 src/types/common.ts create mode 100644 src/types/firearm.ts create mode 100644 src/types/modification.ts diff --git a/src/types/auth.ts b/src/types/auth.ts new file mode 100644 index 0000000..34c2b14 --- /dev/null +++ b/src/types/auth.ts @@ -0,0 +1,11 @@ +export interface LoginRequest { + principle: string + credential: string +} + +export interface User { + id: number + username: string + email: string +} + diff --git a/src/types/common.ts b/src/types/common.ts new file mode 100644 index 0000000..9f98731 --- /dev/null +++ b/src/types/common.ts @@ -0,0 +1,17 @@ +export type Direction = "ASC" | "DESC" + +export interface Page { + items: T[] + page: number + size: number + totalElements: number + totalPages: number +} + +export interface PageQueryParams { + page?: number + size?: number + sortBy?: string + direction?: Direction +} + diff --git a/src/types/firearm.ts b/src/types/firearm.ts new file mode 100644 index 0000000..596f02d --- /dev/null +++ b/src/types/firearm.ts @@ -0,0 +1,24 @@ +export type FirearmType = + | "RIFLE" + | "SUB_MACHINE_GUN" + | "SHOTGUN" + | "LIGHT_MACHINE_GUN" + | "DESIGNATED_MARKSMAN_RIFLE" + | "SNIPER_RIFLE" + | "PISTOL" + | "SPECIAL" + +export interface Firearm { + id: number + name: string + type: FirearmType + level: string + calibre: string + fireRate: number + armourDamage: number + bodyDamage: number + review: string | null +} + +export interface AddFirearmRequest extends Omit {} + diff --git a/src/types/index.ts b/src/types/index.ts index ae82bae..785c148 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,62 +1,4 @@ -export type Direction = "ASC" | "DESC" - -export type FirearmType = - | "RIFLE" - | "SUB_MACHINE_GUN" - | "SHOTGUN" - | "LIGHT_MACHINE_GUN" - | "DESIGNATED_MARKSMAN_RIFLE" - | "SNIPER_RIFLE" - | "PISTOL" - | "SPECIAL" - -export interface Page { - items: T[] - page: number - size: number - totalElements: number - totalPages: number -} - -export interface Firearm { - id: number - name: string - type: FirearmType - level: string - calibre: string - fireRate: number - armourDamage: number - bodyDamage: number - review: string | null -} - -export interface AddFirearmRequest extends Omit {} - -export interface Modification { - id: number - firearmId: number - name: string - code: string - tags: string[] - note: string - author: string - videoUrl: string -} - -export interface PageQueryParams { - page?: number - size?: number - sortBy?: string - direction?: Direction -} - -export interface LoginRequest { - principle: string - credential: string -} - -export interface User { - id: number - username: string - email: string -} +export * from "./common" +export * from "./firearm" +export * from "./modification" +export * from "./auth" diff --git a/src/types/modification.ts b/src/types/modification.ts new file mode 100644 index 0000000..f1286b9 --- /dev/null +++ b/src/types/modification.ts @@ -0,0 +1,12 @@ +export interface Modification { + id: number + firearmId: number + name: string + code: string + tags: string[] + note: string + author: string + videoUrl: string +} + +export interface ModificationRequest extends Omit {} From abc4c68a0f1279a7a33955ebff9c1abba08849dd Mon Sep 17 00:00:00 2001 From: zihluwang Date: Wed, 22 Apr 2026 16:52:19 +0800 Subject: [PATCH 08/13] feat: add CRUD operations for modifications in modification-api --- src/api/modification-api.ts | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/api/modification-api.ts b/src/api/modification-api.ts index 1d95436..84254c2 100644 --- a/src/api/modification-api.ts +++ b/src/api/modification-api.ts @@ -1,4 +1,4 @@ -import { Modification, Page, PageQueryParams } from "@/types" +import { Modification, ModificationRequest, Page, PageQueryParams } from "@/types" import { WebClient } from "@/shared/web-client" import { asUrlSearchParam } from "@/utils/query-param-utils.ts" @@ -30,4 +30,38 @@ export async function getModifications(params?: ModificationParams): Promise { const { data } = await WebClient.get(`/modifications/${id}`) return data +} + +export async function addModification(modification: ModificationRequest): Promise { + const { data } = await WebClient.post("/modifications", modification) + return data +} + +export async function addModifications( + modifications: ModificationRequest[] +): Promise { + const { data } = await WebClient.post("/modifications/batch", { + modifications, + }) + return data +} + +export async function editModification( + id: number, + modification: ModificationRequest +): Promise { + const { data } = await WebClient.put(`/modifications/${id}`, modification) + return data +} + +export async function removeModification( + id: number +): Promise { + await WebClient.delete(`/modifications/${id}`) +} + +export async function removeModifications(ids: number[]) { + const urlSearchParams = new URLSearchParams() + ids.forEach((id) => urlSearchParams.append("ids", "" + id)) + await WebClient.delete(`/modifications/batch-delete?${urlSearchParams.toString()}`) } \ No newline at end of file From 2fc865ea57e204b241ce15ce99fb981fbea8f08f Mon Sep 17 00:00:00 2001 From: zihluwang Date: Thu, 23 Apr 2026 16:12:48 +0800 Subject: [PATCH 09/13] feat: add create and edit modals for modifications management --- .../modification-create-modal/index.tsx | 100 +++++++++ .../modification-edit-modal/index.tsx | 100 +++++++++ src/components/modification-form/index.tsx | 209 ++++++++++++++++++ src/constant/slots.json | 22 ++ src/page/mod-codes/index.tsx | 136 ++++++++++-- src/types/modification.ts | 20 +- 6 files changed, 570 insertions(+), 17 deletions(-) create mode 100644 src/components/modification-create-modal/index.tsx create mode 100644 src/components/modification-edit-modal/index.tsx create mode 100644 src/components/modification-form/index.tsx create mode 100644 src/constant/slots.json diff --git a/src/components/modification-create-modal/index.tsx b/src/components/modification-create-modal/index.tsx new file mode 100644 index 0000000..905aef5 --- /dev/null +++ b/src/components/modification-create-modal/index.tsx @@ -0,0 +1,100 @@ +import { useEffect, useState } from "react" +import { App, Form, Modal } from "antd" +import { ModificationApi } from "@/api" +import ModificationForm from "@/components/modification-form" +import { Modification, ModificationRequest } from "@/types" + +interface ModificationCreateModalProps { + open: boolean + defaultFirearmId?: number + lockedFirearmId?: number + onCancel: () => void + onSuccess: (modification: Modification) => void +} + +function normalizeRequest(values: ModificationRequest): ModificationRequest { + return { + firearmId: values.firearmId, + name: values.name.trim(), + code: values.code.trim(), + tags: values.tags?.map((tag) => tag.trim()).filter(Boolean) || [], + note: values.note?.trim() || undefined, + author: values.author?.trim() || undefined, + videoUrl: values.videoUrl?.trim() || undefined, + accessories: (values.accessories || []).map((accessory) => ({ + slotName: accessory.slotName.trim(), + accessoryName: accessory.accessoryName.trim(), + tunings: (accessory.tunings || []).map((tuning) => ({ + tuningName: tuning.tuningName.trim(), + tuningValue: tuning.tuningValue, + })), + })), + } +} + +export default function ModificationCreateModal({ + open, + defaultFirearmId, + lockedFirearmId, + onCancel, + onSuccess, +}: ModificationCreateModalProps) { + const { message } = App.useApp() + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + + useEffect(() => { + if (!open) { + return + } + + form.setFieldsValue({ + firearmId: lockedFirearmId ?? defaultFirearmId, + accessories: [{ slotName: "", accessoryName: "", tunings: [] }], + tags: [], + }) + }, [open, defaultFirearmId, lockedFirearmId, form]) + + async function onFinish(values: ModificationRequest) { + setLoading(true) + try { + const modification = await ModificationApi.addModification( + normalizeRequest({ + ...values, + firearmId: lockedFirearmId ?? values.firearmId, + }) + ) + message.success("改枪码创建成功") + form.resetFields() + onSuccess(modification) + } catch { + message.error("改枪码创建失败,请稍后重试") + } finally { + setLoading(false) + } + } + + return ( + form.submit()} + okText="创建" + cancelText="取消" + confirmLoading={loading} + width={820} + destroyOnHidden + afterOpenChange={(visible) => { + if (!visible) { + form.resetFields() + } + }} + > + + + ) +} + + + diff --git a/src/components/modification-edit-modal/index.tsx b/src/components/modification-edit-modal/index.tsx new file mode 100644 index 0000000..6ba8c63 --- /dev/null +++ b/src/components/modification-edit-modal/index.tsx @@ -0,0 +1,100 @@ +import { useEffect, useState } from "react" +import { App, Form, Modal } from "antd" +import { ModificationApi } from "@/api" +import ModificationForm from "@/components/modification-form" +import { Modification, ModificationRequest } from "@/types" + +interface ModificationEditModalProps { + open: boolean + modification: Modification | null + lockedFirearmId?: number + onCancel: () => void + onSuccess: (modification: Modification) => void +} + +function normalizeRequest(values: ModificationRequest): ModificationRequest { + return { + firearmId: values.firearmId, + name: values.name.trim(), + code: values.code.trim(), + tags: values.tags?.map((tag) => tag.trim()).filter(Boolean) || [], + note: values.note?.trim() || undefined, + author: values.author?.trim() || undefined, + videoUrl: values.videoUrl?.trim() || undefined, + accessories: (values.accessories || []).map((accessory) => ({ + slotName: accessory.slotName.trim(), + accessoryName: accessory.accessoryName.trim(), + tunings: (accessory.tunings || []).map((tuning) => ({ + tuningName: tuning.tuningName.trim(), + tuningValue: tuning.tuningValue, + })), + })), + } +} + +export default function ModificationEditModal({ + open, + modification, + lockedFirearmId, + onCancel, + onSuccess, +}: ModificationEditModalProps) { + const { message } = App.useApp() + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + + useEffect(() => { + if (!open || !modification) { + return + } + + const { id: _id, ...editableValues } = modification + form.setFieldsValue({ + ...editableValues, + firearmId: lockedFirearmId ?? editableValues.firearmId, + tags: editableValues.tags || [], + accessories: editableValues.accessories || [], + }) + }, [open, modification, lockedFirearmId, form]) + + async function onFinish(values: ModificationRequest) { + if (!modification) { + return + } + + setLoading(true) + try { + const updated = await ModificationApi.editModification( + modification.id, + normalizeRequest({ + ...values, + firearmId: lockedFirearmId ?? values.firearmId, + }) + ) + message.success("改枪码更新成功") + onSuccess(updated) + } catch { + message.error("改枪码更新失败,请稍后重试") + } finally { + setLoading(false) + } + } + + return ( + form.submit()} + okText="保存" + cancelText="取消" + confirmLoading={loading} + width={820} + destroyOnHidden + > + + + ) +} + + diff --git a/src/components/modification-form/index.tsx b/src/components/modification-form/index.tsx new file mode 100644 index 0000000..2c0cdd1 --- /dev/null +++ b/src/components/modification-form/index.tsx @@ -0,0 +1,209 @@ +import { useEffect, useMemo, useState } from "react" +import { FirearmApi } from "@/api" +import slotNames from "@/constant/slots.json" +import { Firearm, ModificationRequest } from "@/types" +import { AutoComplete, Button, Card, Form, Input, InputNumber, Select, Space } from "antd" + +interface ModificationFormProps { + form: ReturnType>[0] + onFinish: (values: ModificationRequest) => void + lockFirearmId?: number +} + +const slotOptions = slotNames.map((slotName) => ({ value: slotName })) + +export default function ModificationForm({ form, onFinish, lockFirearmId }: ModificationFormProps) { + const [firearmOptions, setFirearmOptions] = useState>([]) + const [firearmLoading, setFirearmLoading] = useState(false) + + useEffect(() => { + let active = true + + async function loadAllFirearms() { + setFirearmLoading(true) + try { + const allFirearms: Firearm[] = [] + let page = 0 + let totalPages = 1 + + while (page < totalPages) { + const paged = await FirearmApi.getFirearms({ + page, + size: 100, + sortBy: "id", + direction: "ASC", + }) + + allFirearms.push(...paged.items) + totalPages = paged.totalPages + page += 1 + } + + if (!active) { + return + } + + setFirearmOptions( + allFirearms.map((firearm) => ({ + value: firearm.id, + label: `${firearm.name}`, + })) + ) + } finally { + if (active) { + setFirearmLoading(false) + } + } + } + + void loadAllFirearms() + + return () => { + active = false + } + }, []) + + const mergedFirearmOptions = useMemo(() => { + if (lockFirearmId === undefined || firearmOptions.some((option) => option.value === lockFirearmId)) { + return firearmOptions + } + + return [{ value: lockFirearmId, label: `武器 ID: ${lockFirearmId}` }, ...firearmOptions] + }, [firearmOptions, lockFirearmId]) + + return ( + + form={form} + layout="vertical" + onFinish={onFinish} + requiredMark={false}> + + name="firearmId" + label="武器" + rules={[{ required: true, message: "请输入武器" }]}> + + className="w-full" + placeholder="请选择武器" + options={mergedFirearmOptions} + loading={firearmLoading} + disabled={lockFirearmId !== undefined} + showSearch={{ + filterOption: (input, option) => { + const labelText = String(option?.label ?? "") + return labelText.toLowerCase().includes(input.toLowerCase()) + }, + }} + /> + + + + name="name" + label="改装名称" + rules={[{ required: true, message: "请输入改装名称" }]}> + + + + + name="code" + label="改枪码" + rules={[{ required: true, message: "请输入改枪码" }]}> + + + + name="tags" label="标签"> + + + + name="videoUrl" label="视频链接"> + + + + name="note" label="备注"> + + + + + {(accessoryFields, { add: addAccessory, remove: removeAccessory }) => ( +
+ {accessoryFields.map((accessoryField) => ( + removeAccessory(accessoryField.name)}> + 删除配件 + + }> + + + + + + + + + + {(tuningFields, { add: addTuning, remove: removeTuning }) => ( +
+ {tuningFields.map((tuningField) => ( + + + + + + + + + + ))} + +
+ )} +
+
+ ))} + +
+ )} +
+ + ) +} + + + diff --git a/src/constant/slots.json b/src/constant/slots.json new file mode 100644 index 0000000..a45a22b --- /dev/null +++ b/src/constant/slots.json @@ -0,0 +1,22 @@ +[ + "枪口", + "左导轨", + "右导轨", + "枪管", + "左贴片", + "右贴片", + "上导轨", + "上贴片", + "下导轨", + "瞄准镜", + "战术设备", + "增高座瞄具", + "侧瞄具", + "枪托", + "枪托套件", + "后握把", + "前握把", + "导轨脚架", + "弹匣座", + "弹匣" +] diff --git a/src/page/mod-codes/index.tsx b/src/page/mod-codes/index.tsx index fa122b4..4695d7e 100644 --- a/src/page/mod-codes/index.tsx +++ b/src/page/mod-codes/index.tsx @@ -1,7 +1,9 @@ -import { useEffect, useMemo, useState } from "react" +import { App, Button, Card, Col, Pagination, Popconfirm, Row, Select, Space, Tag, Typography } from "antd" +import { useCallback, 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 ModificationCreateModal from "@/components/modification-create-modal" +import ModificationEditModal from "@/components/modification-edit-modal" import { useAppSelector } from "@/store" import { Modification } from "@/types" @@ -9,24 +11,36 @@ const pageSize = 12 export default function ModCodesPage() { const user = useAppSelector((state) => state.auth.user) + const { message } = App.useApp() const [searchParams] = useSearchParams() const firearmId = useMemo(() => searchParams.get("firearmId") || undefined, [searchParams]) + const parsedFirearmId = useMemo(() => { + if (!firearmId) { + return undefined + } + + const value = Number(firearmId) + return Number.isFinite(value) ? value : undefined + }, [firearmId]) const [page, setPage] = useState(1) const [modifications, setModifications] = useState([]) const [tagOptions, setTagOptions] = useState([]) const [selectedTags, setSelectedTags] = useState([]) const [total, setTotal] = useState(0) + const [deletingId, setDeletingId] = useState(null) + const [createModalOpen, setCreateModalOpen] = useState(false) + const [editingModification, setEditingModification] = useState(null) useEffect(() => { - const _firearmId = firearmId ? +firearmId : (void 0) + const _firearmId = firearmId ? +firearmId : void 0 TagApi.getTags(_firearmId).then((tags) => { setTagOptions(tags) }) - }, []) + }, [firearmId]) - useEffect(() => { - ModificationApi.getModifications({ + const loadModifications = useCallback(() => { + return ModificationApi.getModifications({ page: page - 1, size: pageSize, sortBy: "id", @@ -39,6 +53,31 @@ export default function ModCodesPage() { }) }, [page, firearmId, selectedTags]) + useEffect(() => { + loadModifications() + }, [loadModifications]) + + async function handleDelete(modification: Modification) { + if (!user) { + return + } + + setDeletingId(modification.id) + try { + await ModificationApi.removeModification(modification.id) + message.success("改枪码删除成功") + if (modifications.length === 1 && page > 1) { + setPage(page - 1) + } else { + loadModifications() + } + } catch { + message.error("改枪码删除失败,请稍后重试") + } finally { + setDeletingId(null) + } + } + useEffect(() => { setPage(1) }, [firearmId]) @@ -82,21 +121,39 @@ export default function ModCodesPage() { )} - {user && } + {user && ( + + )}
{modifications.map((modification) => ( - + - 编辑 - +
+ + handleDelete(modification)} + > + + +
) : null } variant="outlined" @@ -124,14 +181,45 @@ export default function ModCodesPage() { {modification.author || "未知"} - {modification.tags.length > 0 && ( + {(modification.tags?.length || 0) > 0 && (
- {modification.tags.map((tag) => ( + {(modification.tags || []).map((tag) => ( {tag} ))}
)} +
+ 配件配置: + {(modification.accessories?.length || 0) > 0 ? ( +
+
+ {(modification.accessories || []).map((accessory, accessoryIndex) => ( +
+
+ {accessory.slotName || "未填写槽位"} + {accessory.accessoryName || "未填写配件"} +
+ {(accessory.tunings?.length || 0) > 0 ? ( +
+ {accessory.tunings.map((tuning, tuningIndex) => ( + + {tuning.tuningName || "未命名"}: {tuning.tuningValue ?? "-"} + + ))} +
+ ) : null} +
+ ))} +
+
+ ) : ( + + 暂无配件信息 + + )} +
+
+ + setCreateModalOpen(false)} + onSuccess={() => { + setCreateModalOpen(false) + loadModifications() + }} + /> + + setEditingModification(null)} + onSuccess={() => { + setEditingModification(null) + loadModifications() + }} + /> ) } diff --git a/src/types/modification.ts b/src/types/modification.ts index f1286b9..20b6e58 100644 --- a/src/types/modification.ts +++ b/src/types/modification.ts @@ -1,12 +1,24 @@ +export interface Tuning { + tuningName: string + tuningValue: number +} + +export interface Accessory { + slotName: string + accessoryName: string + tunings: Tuning[] +} + export interface Modification { id: number firearmId: number name: string code: string - tags: string[] - note: string - author: string - videoUrl: string + tags?: string[] + note?: string + author?: string + videoUrl?: string, + accessories: Accessory[] } export interface ModificationRequest extends Omit {} From e76e684b4dccdb22c466c96600e30a7314160a9f Mon Sep 17 00:00:00 2001 From: zihluwang Date: Thu, 23 Apr 2026 20:37:43 +0800 Subject: [PATCH 10/13] feat: integrate dayjs with duration plugin and update imports --- src/init/index.ts | 1 - src/main.tsx | 1 - src/{init => shared}/dayjs/index.ts | 2 +- src/shared/web-client/index.ts | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 src/init/index.ts rename src/{init => shared}/dayjs/index.ts (66%) diff --git a/src/init/index.ts b/src/init/index.ts deleted file mode 100644 index fc6e552..0000000 --- a/src/init/index.ts +++ /dev/null @@ -1 +0,0 @@ -import "./dayjs" \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 91dc8c0..26957b7 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,7 +6,6 @@ import { PersistGate } from "redux-persist/integration/react" import { App as AntApp, ConfigProvider as AntConfigProvider } from "antd" import { StyleProvider as AntStyleProvider } from "@ant-design/cssinjs" import AntSimplifiedChinese from "antd/locale/zh_CN" -import "@/init" import router from "@/router" import store, { persistor } from "@/store" import "./index.css" diff --git a/src/init/dayjs/index.ts b/src/shared/dayjs/index.ts similarity index 66% rename from src/init/dayjs/index.ts rename to src/shared/dayjs/index.ts index 35a84ed..42e67bc 100644 --- a/src/init/dayjs/index.ts +++ b/src/shared/dayjs/index.ts @@ -3,4 +3,4 @@ import duration from "dayjs/plugin/duration" dayjs.extend(duration) -console.log("Global Dayjs plugins initialised.") \ No newline at end of file +export default dayjs diff --git a/src/shared/web-client/index.ts b/src/shared/web-client/index.ts index a797175..b2fc077 100644 --- a/src/shared/web-client/index.ts +++ b/src/shared/web-client/index.ts @@ -1,5 +1,5 @@ import axios from "axios" -import dayjs from "dayjs" +import dayjs from "@/shared/dayjs" const WebClient = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, From 05b355e709a6953e0b00a1676e8776c32114612a Mon Sep 17 00:00:00 2001 From: zihluwang Date: Fri, 24 Apr 2026 19:44:24 +0800 Subject: [PATCH 11/13] feat: add a new commonly used slot --- src/constant/slots.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/constant/slots.json b/src/constant/slots.json index a45a22b..d5716ec 100644 --- a/src/constant/slots.json +++ b/src/constant/slots.json @@ -18,5 +18,6 @@ "前握把", "导轨脚架", "弹匣座", - "弹匣" + "弹匣", + "托腮板" ] From 3f3ff08d254deb8817321ca36cb5ffd8a5681079 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Fri, 24 Apr 2026 19:47:03 +0800 Subject: [PATCH 12/13] feat: make code more strict --- src/page/mod-codes/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/page/mod-codes/index.tsx b/src/page/mod-codes/index.tsx index 4695d7e..305e967 100644 --- a/src/page/mod-codes/index.tsx +++ b/src/page/mod-codes/index.tsx @@ -269,7 +269,7 @@ export default function ModCodesPage() { onCancel={() => setCreateModalOpen(false)} onSuccess={() => { setCreateModalOpen(false) - loadModifications() + void loadModifications() }} /> @@ -280,7 +280,7 @@ export default function ModCodesPage() { onCancel={() => setEditingModification(null)} onSuccess={() => { setEditingModification(null) - loadModifications() + void loadModifications() }} /> From 0a2f58a91be799387ec66bffdd5cddc0426b2365 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Sat, 25 Apr 2026 11:06:49 +0800 Subject: [PATCH 13/13] feat: add calibres data and update firearm form to use select input for calibre --- src/components/firearm-form/index.tsx | 16 +++++++++++++--- src/constant/calibres.json | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 src/constant/calibres.json diff --git a/src/components/firearm-form/index.tsx b/src/components/firearm-form/index.tsx index 476218e..2095383 100644 --- a/src/components/firearm-form/index.tsx +++ b/src/components/firearm-form/index.tsx @@ -1,5 +1,6 @@ import { Form, Input, InputNumber, Select } from "antd" import { AddFirearmRequest, FirearmType } from "@/types" +import calibres from "@/constant/calibres.json" const firearmTypeText: Record = { RIFLE: "步枪", @@ -12,6 +13,11 @@ const firearmTypeText: Record = { SPECIAL: "特殊", } +const calibreOptions = calibres.map((calibre) => ({ + value: calibre, + label: calibre, +})) + interface FirearmFormProps { form: ReturnType>[0] onFinish: (values: AddFirearmRequest) => void @@ -53,9 +59,14 @@ export default function FirearmForm({ form, onFinish }: FirearmFormProps) { name="calibre" label="子弹口径" - rules={[{ required: true, message: "请输入子弹口径" }]} + rules={[{ required: true, message: "请选择子弹口径" }]} > - +