88 Commits
Author SHA1 Message Date
zihluwang 1dfe3f7221 refactor: remove unused icon images 2026-05-11 07:02:17 +08:00
zihluwangandClaude Opus 4.7 86e259a500 feat: add legal tabs and update header assets
Switch the legal page to tabbed EULA/privacy content with URL sync, update header styling, and migrate footer icons to Ant Design.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 07:00:05 +08:00
zihluwang b91855095b refactor: optimised slots 2026-05-10 13:52:22 +08:00
zihluwang 1f30f70f21 refactor: rename app title 2026-05-10 13:43:01 +08:00
zihluwang 13242deb6c refactor: reduce slots 2026-05-10 13:41:37 +08:00
zihluwang 3b5153f386 refactor: add a built-in slot 2026-05-10 13:32:49 +08:00
zihluwang 5502643466 refactor: change the modification display order 2026-05-10 13:30:51 +08:00
zihluwang 5790750124 feat: implemented listening port check by vite-plugin-port-checker 2026-05-10 13:30:25 +08:00
zihluwang 26bca96575 chore: add dependency vite-plugin-port-checker for checking listening ports 2026-05-10 13:29:55 +08:00
fanxingyao 381ccae5fd feat(index): 更新footer-更新header-添加header背景,footer图标 2026-05-08 18:48:38 +08:00
zihluwang 752e64f259 ci: add ci process 2026-05-08 08:23:35 +08:00
zihluwang 84a2a2ffea Merge remote-tracking branch 'origin/main' into develop 2026-05-08 08:11:08 +08:00
dependabot[bot] fd2352b6ad chore: bump the dependency-updates group with 7 updates
Bumps the dependency-updates group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [axios](https://github.com/axios/axios) | `1.15.2` | `1.16.0` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.5` | `19.2.6` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.5` | `19.2.6` |
| [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.14.2` | `7.15.0` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.14.2` | `7.15.0` |
| [globals](https://github.com/sindresorhus/globals) | `17.5.0` | `17.6.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.10` | `8.0.11` |


Updates `axios` from 1.15.2 to 1.16.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.15.2...v1.16.0)

Updates `react` from 19.2.5 to 19.2.6
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.6/packages/react)

Updates `react-dom` from 19.2.5 to 19.2.6
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.6/packages/react-dom)

Updates `react-router` from 7.14.2 to 7.15.0
- [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.15.0/packages/react-router)

Updates `react-router-dom` from 7.14.2 to 7.15.0
- [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.15.0/packages/react-router-dom)

Updates `globals` from 17.5.0 to 17.6.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.5.0...v17.6.0)

Updates `vite` from 8.0.10 to 8.0.11
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.11/packages/vite)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependency-updates
- dependency-name: react
  dependency-version: 19.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
- dependency-name: react-dom
  dependency-version: 19.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
- dependency-name: react-router
  dependency-version: 7.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependency-updates
- dependency-name: react-router-dom
  dependency-version: 7.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependency-updates
- dependency-name: globals
  dependency-version: 17.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dependency-updates
- dependency-name: vite
  dependency-version: 8.0.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-07 07:58:51 +00:00
siujamo 0efcf75221 chore: remove unused deploy scripts from package.json 2026-05-07 13:35:41 +08:00
siujamo 0479744ce5 perf: optimise packaging 2026-05-07 10:51:29 +08:00
siujamo af58edbafd feat: add custom hooks for typed useDispatch and useSelector 2026-05-07 10:46:06 +08:00
siujamo 1a16199f2f docs: update .env.example with detailed instructions and configuration notes 2026-05-07 09:21:27 +08:00
zihluwang 9b2d527576 docs: add EULA and PrivacyPolicy 2026-05-06 19:14:43 +08:00
zihluwang d663cc5d20 docs: add js docs 2026-05-06 19:01:58 +08:00
zihluwang 5d84cf0589 feat: add placeholder files for EULA and Privacy Policy
Added empty files as placeholders for future legal documentation.
2026-05-06 18:59:17 +08:00
zihluwang 84fa103555 Merge branch 'refs/heads/main' into develop 2026-05-06 18:57:05 +08:00
zihluwang ff967da485 docs: update README file 2026-05-06 18:55:28 +08:00
zihluwang 6271b22708 Merge pull request #14 from zihluwang/dependabot/npm_and_yarn/dependency-updates-9da01cc519
chore: bump antd from 6.3.6 to 6.3.7 in the dependency-updates group
2026-05-06 15:07:20 +08:00
zihluwang 6e18d2efa9 refactor: change default size of modifications to 10 2026-05-06 01:31:05 +08:00
zihluwang 5803d057fd refactor: change the order of the slots 2026-05-05 11:48:30 +08:00
dependabot[bot] 22da81a102 chore: bump antd from 6.3.6 to 6.3.7 in the dependency-updates group
Bumps the dependency-updates group with 1 update: [antd](https://github.com/ant-design/ant-design).


Updates `antd` from 6.3.6 to 6.3.7
- [Release notes](https://github.com/ant-design/ant-design/releases)
- [Changelog](https://github.com/ant-design/ant-design/blob/master/CHANGELOG.en-US.md)
- [Commits](https://github.com/ant-design/ant-design/compare/6.3.6...6.3.7)

---
updated-dependencies:
- dependency-name: antd
  dependency-version: 6.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-30 07:57:59 +00:00
zihluwang a8959c28ec Merge pull request #13 from zihluwang/develop
v1.2.2
2026-04-26 11:59:11 +08:00
zihluwang fbbef5c28b feat: enhance firearm options and tuning button styles for better usability 2026-04-26 11:55:55 +08:00
zihluwang 6d98ecef30 feat: add tuning options to modification form and update labels for clarity 2026-04-26 11:51:25 +08:00
zihluwang 0e695e4266 feat: update initial form values to clear accessories array 2026-04-26 11:48:12 +08:00
zihluwang d524b3814c feat: format code for improved readability and consistency 2026-04-26 11:40:30 +08:00
zihluwang 8e98f5b9da feat: refactor loadModifications to use async/await and improve invocation 2026-04-25 15:31:17 +08:00
zihluwang 9a65fd04c3 feat: refactor loadFirearms to use async/await and improve error handling 2026-04-25 15:30:33 +08:00
zihluwang 49fbcb221c feat: enhance review tooltip in firearm form for better user experience 2026-04-25 15:29:24 +08:00
zihluwang e4acb7fd6f Merge pull request #12 from zihluwang/develop
v1.2.1
2026-04-25 15:17:04 +08:00
zihluwang a66ed2e216 Merge branch 'main' into develop
# Conflicts:
#	package.json
#	pnpm-lock.yaml
2026-04-25 11:10:23 +08:00
zihluwang 0a2f58a91b feat: add calibres data and update firearm form to use select input for calibre 2026-04-25 11:06:49 +08:00
zihluwang 3f3ff08d25 feat: make code more strict 2026-04-24 19:47:03 +08:00
zihluwang 05b355e709 feat: add a new commonly used slot 2026-04-24 19:44:24 +08:00
zihluwang a9ddf3e3f8 Merge pull request #11
chore: bump the dependency-updates group with 11 updates
2026-04-24 19:43:20 +08:00
zihluwang e76e684b4d feat: integrate dayjs with duration plugin and update imports 2026-04-23 20:37:43 +08:00
zihluwang 2fc865ea57 feat: add create and edit modals for modifications management 2026-04-23 16:12:48 +08:00
dependabot[bot] ab9be06d5e chore: bump the dependency-updates group with 11 updates
Bumps the dependency-updates group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.2.2` | `4.2.4` |
| [@tanstack/react-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual) | `3.13.23` | `3.13.24` |
| [antd](https://github.com/ant-design/ant-design) | `6.3.5` | `6.3.6` |
| [axios](https://github.com/axios/axios) | `1.14.0` | `1.15.2` |
| [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.14.0` | `7.14.2` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.14.0` | `7.14.2` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.2.2` | `4.2.4` |
| [globals](https://github.com/sindresorhus/globals) | `17.4.0` | `17.5.0` |
| [prettier](https://github.com/prettier/prettier) | `3.8.1` | `3.8.3` |
| [typescript](https://github.com/microsoft/TypeScript) | `6.0.2` | `6.0.3` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.8` | `8.0.10` |


Updates `@tailwindcss/vite` from 4.2.2 to 4.2.4
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.4/packages/@tailwindcss-vite)

Updates `@tanstack/react-virtual` from 3.13.23 to 3.13.24
- [Release notes](https://github.com/TanStack/virtual/releases)
- [Changelog](https://github.com/TanStack/virtual/blob/main/packages/react-virtual/CHANGELOG.md)
- [Commits](https://github.com/TanStack/virtual/commits/@tanstack/react-virtual@3.13.24/packages/react-virtual)

Updates `antd` from 6.3.5 to 6.3.6
- [Release notes](https://github.com/ant-design/ant-design/releases)
- [Changelog](https://github.com/ant-design/ant-design/blob/master/CHANGELOG.en-US.md)
- [Commits](https://github.com/ant-design/ant-design/compare/6.3.5...6.3.6)

Updates `axios` from 1.14.0 to 1.15.2
- [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.2)

Updates `react-router` from 7.14.0 to 7.14.2
- [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.2/packages/react-router)

Updates `react-router-dom` from 7.14.0 to 7.14.2
- [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.2/packages/react-router-dom)

Updates `tailwindcss` from 4.2.2 to 4.2.4
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.4/packages/tailwindcss)

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)

Updates `typescript` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v6.0.2...v6.0.3)

Updates `vite` from 8.0.8 to 8.0.10
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.10/packages/vite)

---
updated-dependencies:
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
- dependency-name: "@tanstack/react-virtual"
  dependency-version: 3.13.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
- dependency-name: antd
  dependency-version: 6.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependency-updates
- dependency-name: react-router
  dependency-version: 7.14.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
- dependency-name: react-router-dom
  dependency-version: 7.14.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
- dependency-name: tailwindcss
  dependency-version: 4.2.4
  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
- dependency-name: typescript
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
- dependency-name: vite
  dependency-version: 8.0.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 07:59:11 +00:00
zihluwang abc4c68a0f feat: add CRUD operations for modifications in modification-api 2026-04-22 16:52:19 +08:00
zihluwang ff487064a2 refactor: split interfaces by module 2026-04-22 16:52:09 +08:00
zihluwang e7373d6e98 Merge pull request #10 from zihluwang/dependabot/npm_and_yarn/dependency-updates-2c57f57997
chore: bump the dependency-updates group with 5 updates
2026-04-22 14:44:01 +08:00
zihluwang d52ce8828d Merge branch 'develop' into dependabot/npm_and_yarn/dependency-updates-2c57f57997 2026-04-22 14:43:52 +08:00
zihluwang 745c98bc20 feat: add create and edit modals for firearms management 2026-04-21 23:40:00 +08:00
zihluwang 16db0eb0ee feat: add functionality to create, edit, and remove firearms 2026-04-21 14:30:13 +08:00
zihluwang a2e3676d05 feat: add logout functionality with dropdown menu for user authentication 2026-04-21 11:28:07 +08:00
dependabot[bot] 088b0e87ce 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] <support@github.com>
2026-04-16 07:57:56 +00:00
zihluwang d6b8d12b2e feat: add user-based conditional rendering for firearms and mod codes pages 2026-04-14 11:57:43 +08:00
zihluwang ac76150915 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.
2026-04-14 11:17:31 +08:00
zihluwang b000336d22 Merge pull request #9 from zihluwang/develop
Enhance firearms management with filtering, display, and UI improvements
2026-04-09 17:27:22 +08:00
zihluwang cf2b57f745 feat: reinstall packages 2026-04-09 17:25:31 +08:00
zihluwang a1984c3ecb Merge remote-tracking branch 'origin/main' into develop
# Conflicts:
#	package.json
#	pnpm-lock.yaml
2026-04-09 17:24:21 +08:00
zihluwang 3af4650d32 Merge pull request #8 from zihluwang/dependabot/npm_and_yarn/dependency-updates-1cafa9d752 2026-04-09 17:19:39 +08:00
dependabot[bot] a36c539ef1 chore: bump the dependency-updates group with 6 updates
Bumps the dependency-updates group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.4` | `19.2.5` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.4` | `19.2.5` |
| [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.13.2` | `7.14.0` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.13.2` | `7.14.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.19.15` | `22.19.17` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.3` | `8.0.8` |


Updates `react` from 19.2.4 to 19.2.5
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.5/packages/react)

Updates `react-dom` from 19.2.4 to 19.2.5
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.5/packages/react-dom)

Updates `react-router` from 7.13.2 to 7.14.0
- [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.0/packages/react-router)

Updates `react-router-dom` from 7.13.2 to 7.14.0
- [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.0/packages/react-router-dom)

Updates `@types/node` from 22.19.15 to 22.19.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `vite` from 8.0.3 to 8.0.8
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
- dependency-name: react-dom
  dependency-version: 19.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
- dependency-name: react-router
  dependency-version: 7.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependency-updates
- dependency-name: react-router-dom
  dependency-version: 7.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependency-updates
- dependency-name: "@types/node"
  dependency-version: 22.19.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 07:57:55 +00:00
zihluwang dad5c191fa feat: add tarball build script to package.json 2026-04-09 14:33:18 +08:00
zihluwang 78fa02e618 feat: add bullet calibre display to FirearmsPage 2026-04-09 14:30:21 +08:00
zihluwang 63f9885aa2 feat: add additional firearm attributes and DPS calculation to FirearmsPage 2026-04-09 14:17:38 +08:00
zihluwang 7f50fafee8 feat: add tag API and integrate tag selection in modifications page 2026-04-07 11:59:37 +08:00
zihluwang 3da706402d feat: enhance modification display with copy functionality and improved styling 2026-04-07 11:17:14 +08:00
zihluwang 9dfd52eb2d feat: integrate Ant Design components and configuration for improved UI 2026-04-07 11:06:34 +08:00
zihluwang 4294495ffa chore: add @ant-design/cssinjs 2026-04-07 11:06:21 +08:00
zihluwang a98902b08b fix: correct className syntax for consistent styling in firearms and mod codes pages 2026-04-07 01:28:34 +08:00
zihluwang bad31c6653 feat: implement modifications page with pagination and detail display 2026-04-06 21:02:16 +08:00
zihluwang ae912050b6 feat: add link to view modification codes for firearms 2026-04-06 20:53:13 +08:00
zihluwang feeb44bf6a feat: add firearm type filter to firearms page 2026-04-06 20:47:44 +08:00
zihluwang 452b807b62 feat: add firearm page for display firearms 2026-04-06 20:40:42 +08:00
zihluwang 38b25099de chore: add antd@6 for better UI and UX 2026-04-06 20:40:19 +08:00
zihluwang a0a5c835aa feat: add firearm management features
- Implemented API for fetching firearms and firearm details.
- Created a new page for displaying the list of firearms with search and filter options.
- Added Redux slice for managing firearms state.
- Integrated Redux Persist for state persistence.
- Updated routing to include firearms page.
- Removed obsolete modification codes data.
- Enhanced UI with responsive grid layout for firearms display.
- Added utility functions for handling URL query parameters.
2026-04-06 17:57:25 +08:00
zihluwang 864895d932 feat: add new weapon modifications for AWM, MCX LT, and KC17 2026-04-02 15:31:00 +08:00
zihluwang a4e5891189 feat: update README and add MIT Licence 2026-04-02 15:20:57 +08:00
zihluwang 335de44487 feat: add mode filtering to ModCodes component 2026-04-02 15:06:47 +08:00
zihluwang 3573a23acf Merge pull request #5 from zihluwang:dependabot/npm_and_yarn/globals-17.4.0
chore: bump globals from 16.5.0 to 17.4.0
2026-04-02 15:01:43 +08:00
zihluwang 6858e07603 Merge pull request #6 from zihluwang/dependabot/npm_and_yarn/typescript-6.0.2
chore: bump typescript from 5.9.3 to 6.0.2
2026-04-02 15:00:27 +08:00
dependabot[bot] aa2415a0cb chore: bump typescript from 5.9.3 to 6.0.2
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 06:54:37 +00:00
dependabot[bot] f9ff917360 chore: bump globals from 16.5.0 to 17.4.0
Bumps [globals](https://github.com/sindresorhus/globals) from 16.5.0 to 17.4.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.5.0...v17.4.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.4.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 06:54:36 +00:00
zihluwang 405c520cab Merge pull request #7 from zihluwang:dependabot/npm_and_yarn/dependency-updates-0c07818b49
chore: bump the dependency-updates group across 1 directory with 3 updates
2026-04-02 14:53:27 +08:00
dependabot[bot] 026843c6f0 chore: bump the dependency-updates group across 1 directory with 3 updates
Bumps the dependency-updates group with 3 updates in the / directory: [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router), [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `react-router` from 7.13.1 to 7.13.2
- [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.13.2/packages/react-router)

Updates `react-router-dom` from 7.13.1 to 7.13.2
- [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.13.2/packages/react-router-dom)

Updates `vite` from 8.0.1 to 8.0.3
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.0.3/packages/vite)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
- dependency-name: react-router-dom
  dependency-version: 7.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
- dependency-name: vite
  dependency-version: 8.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dependency-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 02:44:23 +00:00
zihluwang 353e8a597b feat: add @tanstack/react-virtual for improved virtual scrolling and enhance modification codes with additional weapon data and filtering options 2026-04-02 10:42:36 +08:00
zihluwang 86b743dcc0 feat: remove GitHub Pages deployment workflow 2026-04-02 10:02:33 +08:00
zihluwang 00215decc5 feat: add modification codes page and update routing; remove about and contact pages 2026-04-02 10:01:18 +08:00
zihluwang c22bc08282 Merge pull request #1 from zihluwang/copilot/remove-auth-store-redux-axios
Remove auth, store, redux, and axios from template
2026-04-02 09:29:47 +08:00
copilot-swe-agent[bot]andzihluwang b3caeb8ce5 feat: remove auth, store, redux, and axios from template
Agent-Logs-Url: https://github.com/zihluwang/delta-force-firearm-modification-codes/sessions/3477dccf-d002-4dda-ad39-4768da36bb9e

Co-authored-by: zihluwang <47817169+zihluwang@users.noreply.github.com>
2026-04-02 01:27:56 +00:00
copilot-swe-agent[bot] 5d44e24c95 Initial plan 2026-04-02 01:24:00 +00:00
zihluwang 8b3ccc6a51 Initial commit 2026-04-02 09:23:57 +08:00
102 changed files with 7930 additions and 1517 deletions

No files matched your search

+2 -4
View File
@@ -1,4 +1,2 @@
delta-force-guide-server.iml node_modules/
build/ README.md
.idea/
.gradle
+21
View File
@@ -0,0 +1,21 @@
# ENVIRONMENT CONFIGURATION TEMPLATE
#
# This file serves as a template for environment variables.
# DO NOT include any sensitive data (passwords, API keys, etc.) in this file.
#
# INSTRUCTIONS
# 1. Copy this file to `.env`, `.env.development`, or `.env.production` depending on your
# target environment.
# 2. Replace the placeholder values with your actual configuration.
# 3. Alternatively, ensure these variables are defined within your system's environment
# settings before running the application.
# The base URL fot the backend API service.
# Ensure this matches your Spring Boot server address (e.g., http://localhost:8080).
VITE_API_BASE_URL=/api
# Determines where Redux state is persisted on the client side.
# Available options:
# - `local`: Persists data in LocalStorage (remains after closing the browser).
# - `session`: Persists data in SessionStorage (cleared when the tab is closed).
VITE_REDUX_STORAGE=local
-3
View File
@@ -1,3 +0,0 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary
+27
View File
@@ -0,0 +1,27 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore: "
groups:
dependency-updates:
patterns: ["*"]
update-types: ["minor", "patch"]
ignore:
- dependency-name: "@types/node"
update-types: ["version-update:semver-major"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
+30
View File
@@ -0,0 +1,30 @@
name: Upload Release Assets
on:
release:
types: [created] # 仅当创建 Release 之后触发
jobs:
build-and-upload:
runs-on: ubuntu-latest
# 必须授予权限以允许 Action 修改 Release
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Create archive
run: |
TAG_NAME=${{ github.event.release.tag_name }}
tar -czvf "website-dist-${TAG_NAME}.tar.gz" --exclude=".git*" --exclude=".github*" .
- name: Upload Release Asset
uses: softprops/action-gh-release@v2
with:
# 上传刚才生成的 tar.gz 文件
files: dist-${{ github.event.release.tag_name }}.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+161 -23
View File
@@ -97,7 +97,30 @@ fabric.properties
# Editor-based HTTP Client # Editor-based HTTP Client
http-client.private.env.json http-client.private.env.json
### Server ### Gradle
.gradle
**/build/
!**/src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties
# Cache of project
.gradletasknamecache
# Eclipse Gradle plugin generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
### Java
# Compiled class file # Compiled class file
*.class *.class
@@ -111,40 +134,155 @@ http-client.private.env.json
.mtj.tmp/ .mtj.tmp/
# Package Files # # Package Files #
*.jar
*.war *.war
*.nar *.nar
*.ear *.ear
*.zip *.zip
*.tar.gz *.tar.gz
*.rar *.rar
*.jar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid* hs_err_pid*
replay_pid* replay_pid*
# Config ### Node.js
config/*.yml # Logs
config/*.yaml logs
config/*.properties #*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Tests # Diagnostic reports (https://nodejs.org/api/report.html)
test/ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
### Gradle # Runtime data
.gradle pids
**/build/ *.pid
!**/src/**/build/ *.seed
*.pid.lock
!gradle/ # Directory for instrumented libs generated by jscoverage/JSCover
!gradle/wrapper lib-cov
!gradle/wrapper/gradle-wrapper.jar
!gradle/wrapper/gradle-wrapper.properties
gradle-app.setting
.gradletasknamecache
# Eclipse Gradle plugin generated files # Coverage directory used by tools like istanbul
# Eclipse Core coverage
.project *.lcov
# JDT-specific (Eclipse Java Development Tools)
.classpath # nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.*
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
#.cache
# Sveltekit cache directory
.svelte-kit/
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Firebase cache directory
.firebase/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+24
View File
@@ -0,0 +1,24 @@
printWidth: 100
tabWidth: 2
useTabs: false
semi: false
singleQuote: false
quoteProps: as-needed
jsxSingleQuote: false
trailingComma: es5
bracketSpacing: true
bracketSameLine: true
arrowParens: always
#rangeStart: 0
#rangeEnd: n
#parser:
#filepath:
#requirePragma: false
#insertPragma: false
#proseWrap: preserve
htmlWhitespaceSensitivity: css
#vueIndentScriptAndStyle: false
endOfLine: lf
#embeddedLanguageFormatting: auto
# Enforce single attribute per line in HTML, Vue and JSX
#singleAttributePerLine: false
+22
View File
@@ -0,0 +1,22 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node-terminal",
"request": "launch",
"name": "pnpm dev",
"command": "pnpm dev",
"cwd": "${workspaceFolder}"
},
{
"type": "node-terminal",
"request": "launch",
"name": "build",
"command": "pnpm build",
"cwd": "${workspaceFolder}"
}
]
}
+7 -8
View File
@@ -1,10 +1,9 @@
FROM amazoncorretto:21 AS builder FROM node:20-alpine as builder
WORKDIR /app WORKDIR /app
ADD . . COPY . .
RUN chmod +x ./gradlew && \ RUN corepack enable pnpm
./gradlew build -x test RUN pnpm install
RUN pnpm build
FROM amazoncorretto:21-alpine AS runner FROM nginx:1.27 as runner
WORKDIR /app COPY --from=builder /app/dist/ /usr/share/nginx/html/
COPY --from=builder /app/build/libs/delta-force-guide-server-1.0.0.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
+21
View File
@@ -0,0 +1,21 @@
MIT Licence
Copyright (c) 2026 Zihlu Wang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+94
View File
@@ -0,0 +1,94 @@
# Delta Force Firearm Modification Codes
Delta Force Firearm Modification Codes is a lightweight web app for browsing and filtering firearm modification codes for Delta Force.
The site is built with Vite, React, TypeScript, Tailwind CSS, and React Router. It presents a searchable code library with filtering by weapon, mode, and tag, and includes quick copy support for each modification code.
## Features
- Browse a curated list of firearm modification codes.
- Filter results by weapon, mode, and tag.
- Copy modification codes directly from the interface.
- Render large lists efficiently with window virtualisation.
- Deploy as a static site.
## Tech Stack
- Vite
- React 19
- TypeScript
- Tailwind CSS 4
- React Router 7
- @tanstack/react-virtual
- Day.js
## Getting Started
### Prerequisites
- Node.js 20 or later is recommended.
- pnpm is required for dependency management and scripts.
### Install dependencies
```bash
pnpm install
```
### Start the development server
```bash
pnpm dev
```
### Build for production
```bash
pnpm build
```
### Preview the production build locally
```bash
pnpm preview
```
## Available Scripts
- `pnpm dev`: start the Vite development server.
- `pnpm build`: run TypeScript compilation and create a production build.
- `pnpm preview`: preview the production bundle locally.
- `pnpm lint`: run project linting.
- `pnpm deploy`: build and publish the site with `gh-pages`.
## Project Structure
```text
src/
components/ Shared UI components
layout/ Route layouts
page/ Route pages
router/ Router configuration
```
The current dataset is stored in `src/data/modification-codes.json`.
## Deployment
The repository is configured for static deployment. The `public/CNAME` file indicates the site is intended to be served on `onixbyte.dev`.
To deploy:
```bash
pnpm deploy
```
## Contributing
Contributions are welcome. If you want to improve the dataset, refine the filtering experience, or fix UI issues, open an issue or submit a pull request.
When contributing, please keep documentation and user-facing copy in British English.
## Licence
This project is released under the MIT Licence. See `LICENCE` for details.
-63
View File
@@ -1,63 +0,0 @@
plugins {
java
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependency.management)
}
val artefactVersion: String by project
group = "com.onixbyte.helix"
version = artefactVersion
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
dependencies {
implementation(libs.jspecify.core)
implementation(platform(libs.aws.sdk.bom))
implementation(libs.aws.sdk.s3)
implementation(libs.commons.io)
implementation(libs.commons.lang)
implementation(libs.commons.collections)
implementation(libs.hypersistence.core)
implementation(platform(libs.onixbyte.versionCatalogue))
implementation(libs.onixbyte.tuple)
implementation(libs.onixbyte.commonToolbox)
implementation(libs.onixbyte.mathToolbox)
implementation(libs.onixbyte.identityGenerator)
implementation(libs.onixbyte.captcha)
implementation(libs.onixbyte.regions)
implementation(libs.jwt.core)
implementation(libs.spring.boot.configurationProcessor)
implementation(libs.spring.boot.actuator)
implementation(libs.spring.boot.starter.web)
implementation(libs.spring.boot.starter.webFlux)
implementation(libs.spring.boot.starter.validation)
implementation(libs.spring.boot.starter.redis)
implementation(libs.spring.boot.starter.cache)
implementation(libs.spring.boot.starter.jpa)
implementation(libs.mybatis.starter.core)
implementation(libs.flyway.core)
implementation(libs.flyway.postgresql)
implementation(libs.jackson.jsr310)
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.16")
testImplementation(libs.spring.boot.starter.test)
testImplementation(libs.reactor.test)
testImplementation(libs.mybatis.starter.test)
runtimeOnly(libs.postgres.driver)
testRuntimeOnly(libs.h2.database)
testRuntimeOnly(libs.junit.launcher)
}
tasks.test {
useJUnitPlatform()
}
-1
View File
@@ -1 +0,0 @@
artefactVersion = 1.0.0
-76
View File
@@ -1,76 +0,0 @@
[versions]
jspecifyVersion = "1.0.0"
javaJwtVersion = "4.5.1"
postgresDriverVersion = "42.7.9"
h2Version = "2.2.224"
springBootVersion = "3.5.13"
springSecurityVersion = "6.5.2"
reactorVersion = "3.7.8"
junitPlatformVersion = "1.12.2"
onixbyteVersion = "3.3.0"
onixbyteCaptcha = "1.1.0"
onixbyteRegions = "2025.12.0"
awsSdkVersion = "2.25.48"
commonsIoVersion = "2.16.1"
commonsCollections = "4.5.0"
commonsLangVersion = "3.20.0"
mybatisVersion = "3.0.5"
jacksonVersion = "2.19.2"
hypersistenceVersion = "3.14.0"
springDependencyManagementVersion = "1.1.7"
[libraries]
# General Utilities
jspecify-core = { group = "org.jspecify", name = "jspecify", version.ref = "jspecifyVersion" }
commons-io = { group = "commons-io", name = "commons-io", version.ref = "commonsIoVersion" }
commons-collections = { group = "org.apache.commons", name = "commons-collections4", version.ref = "commonsCollections" }
commons-lang = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commonsLangVersion" }
jackson-jsr310 = { group = "com.fasterxml.jackson.datatype", name = "jackson-datatype-jsr310", version.ref = "jacksonVersion" }
# Onixbyte Ecosystem
onixbyte-versionCatalogue = { group = "com.onixbyte", name = "version-catalogue", version.ref = "onixbyteVersion" }
onixbyte-tuple = { group = "com.onixbyte", name = "tuple" }
onixbyte-commonToolbox = { group = "com.onixbyte", name = "common-toolbox" }
onixbyte-mathToolbox = { group = "com.onixbyte", name = "math-toolbox" }
onixbyte-identityGenerator = { group = "com.onixbyte", name = "identity-generator" }
onixbyte-captcha = { group = "com.onixbyte", name = "captcha", version.ref = "onixbyteCaptcha" }
onixbyte-regions = { group = "com.onixbyte", name = "regions4j", version.ref = "onixbyteRegions" }
# Persistence & Database
mybatis-starter-core = { group = "org.mybatis.spring.boot", name = "mybatis-spring-boot-starter", version.ref = "mybatisVersion" }
spring-boot-starter-jpa = { group = "org.springframework.boot", name = "spring-boot-starter-data-jpa" }
hypersistence-core = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-63", version.ref = "hypersistenceVersion" }
postgres-driver = { group = "org.postgresql", name = "postgresql", version.ref = "postgresDriverVersion" }
mysql-driver = { group = "com.mysql", name = "mysql-connector-j" }
h2-database = { group = "com.h2database", name = "h2", version.ref = "h2Version" }
spring-boot-starter-redis = { group = "org.springframework.boot", name = "spring-boot-starter-data-redis", version.ref = "springBootVersion" }
flyway-core = { group = "org.flywaydb", name = "flyway-core" }
flyway-postgresql = { group = "org.flywaydb", name = "flyway-database-postgresql" }
flyway-mysql = { group = "org.flywaydb", name = "flyway-mysql" }
# Spring Boot Core & Web
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
spring-boot-starter-webFlux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux" }
spring-boot-starter-validation = { group = "org.springframework.boot", name = "spring-boot-starter-validation" }
spring-boot-starter-cache = { group = "org.springframework.boot", name = "spring-boot-starter-cache" }
spring-boot-configurationProcessor = { group = "org.springframework.boot", name = "spring-boot-configuration-processor" }
spring-boot-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" }
# Security & Auth
spring-boot-starter-security = { group = "org.springframework.boot", name = "spring-boot-starter-security", version.ref = "springBootVersion" }
jwt-core = { group = "com.auth0", name = "java-jwt", version.ref = "javaJwtVersion" }
# Cloud Services
aws-sdk-bom = { group = "software.amazon.awssdk", name = "bom", version.ref = "awsSdkVersion" }
aws-sdk-s3 = { group = "software.amazon.awssdk", name = "s3" }
# Testing
spring-boot-starter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test", version.ref = "springBootVersion" }
spring-security-test = { group = "org.springframework.security", name = "spring-security-test", version.ref = "springSecurityVersion" }
reactor-test = { group = "io.projectreactor", name = "reactor-test", version.ref = "reactorVersion" }
junit-launcher = { group = "org.junit.platform", name = "junit-platform-launcher", version.ref = "junitPlatformVersion" }
mybatis-starter-test = { group = "org.mybatis.spring.boot", name = "mybatis-spring-boot-starter-test", version.ref = "mybatisVersion" }
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "springBootVersion" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "springDependencyManagementVersion" }
Binary file not shown.
-7
View File
@@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
-251
View File
@@ -1,251 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
-94
View File
@@ -1,94 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/onixbyte.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>三角洲行动改枪码库</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+2798
View File
File diff suppressed because it is too large. Load diff
+51
View File
@@ -0,0 +1,51 @@
{
"name": "delta-force-guide-web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"build:tar": "pnpm build && tar -czf dist.tar.gz dist",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/cssinjs": "^2.1.2",
"@ant-design/icons": "^6.2.2",
"@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/vite": "^4.2.4",
"@tanstack/react-virtual": "^3.13.24",
"antd": "^6.3.7",
"axios": "^1.16.0",
"dayjs": "^1.11.20",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-redux": "^9.2.0",
"react-router": "^7.15.0",
"react-router-dom": "^7.15.0",
"redux-persist": "^6.0.0",
"tailwindcss": "^4.2.4"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^22.19.17",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"globals": "^17.6.0",
"prettier": "^3.8.3",
"typescript": "~6.0.3",
"vite": "^8.0.11",
"vite-plugin-markdown": "^2.2.0",
"vite-plugin-port-checker": "^1.0.1"
},
"pnpm": {
"ignoredBuiltDependencies": [
"esbuild"
],
"onlyBuiltDependencies": [
"@tailwindcss/oxide"
]
}
}
+2290
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
esbuild: false
View File
Whitespace-only changes.
+1
View File
@@ -0,0 +1 @@
onixbyte.dev
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" zoomAndPan="magnify" viewBox="0 0 375 374.999991" height="500" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><g/></defs><rect x="-37.5" width="450" fill="#ffffff" y="-37.499999" height="449.999989" fill-opacity="1"/><rect x="-37.5" width="450" fill="#324370" y="-37.499999" height="449.999989" fill-opacity="1"/><g fill="#ffffff" fill-opacity="1"><g transform="translate(27.506677, 211.874986)"><g><path d="M 12.359375 -0.453125 C 8.003906 -1.816406 5.003906 -4.3125 3.359375 -7.9375 C 1.710938 -11.5625 1.550781 -16.253906 2.875 -22.015625 C 4.195312 -27.773438 6.519531 -32.46875 9.84375 -36.09375 C 13.175781 -39.71875 17.34375 -42.210938 22.34375 -43.578125 Z M 27.578125 -34.109375 L 30.015625 -44.796875 L 31.875 -44.796875 C 39.46875 -44.796875 44.882812 -42.851562 48.125 -38.96875 C 51.363281 -35.09375 52.128906 -29.441406 50.421875 -22.015625 C 48.722656 -14.585938 45.351562 -8.929688 40.3125 -5.046875 C 35.28125 -1.171875 28.96875 0.765625 21.375 0.765625 C 20.519531 0.765625 19.898438 0.742188 19.515625 0.703125 L 21.953125 -9.921875 C 22.722656 -9.835938 23.34375 -9.796875 23.8125 -9.796875 C 26.84375 -9.796875 29.367188 -10.710938 31.390625 -12.546875 C 33.421875 -14.378906 34.796875 -16.851562 35.515625 -19.96875 L 36.421875 -24.0625 C 37.140625 -27.175781 36.910156 -29.648438 35.734375 -31.484375 C 34.566406 -33.316406 32.46875 -34.234375 29.4375 -34.234375 C 28.96875 -34.234375 28.347656 -34.191406 27.578125 -34.109375 Z M 27.578125 -34.109375 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(80.815405, 211.874986)"><g><path d="M 31.171875 -34.5625 C 28.816406 -34.5625 26.363281 -34.003906 23.8125 -32.890625 L 21.953125 -24.890625 C 22.460938 -25.066406 23.0625 -25.15625 23.75 -25.15625 C 25.15625 -25.15625 26.125 -24.71875 26.65625 -23.84375 C 27.1875 -22.96875 27.28125 -21.804688 26.9375 -20.359375 L 22.265625 0 L 35 0 L 40.0625 -21.890625 C 41 -25.984375 40.726562 -29.117188 39.25 -31.296875 C 37.78125 -33.472656 35.085938 -34.5625 31.171875 -34.5625 Z M 17.28125 -19.96875 L 17.796875 -28.671875 L 18.171875 -33.796875 L 7.75 -33.796875 L -0.0625 0 L 12.671875 0 L 17.21875 -19.84375 Z M 17.28125 -19.96875 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(123.500788, 211.874986)"><g><path d="M 19.515625 -33.796875 L 11.703125 0 L -1.03125 0 L 6.78125 -33.796875 Z M 22.46875 -46.40625 L 20.484375 -37.765625 L 7.75 -37.765625 L 9.71875 -46.40625 Z M 22.46875 -46.40625 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(144.811483, 211.874986)"><g><path d="M 45.5625 -33.796875 L 29.953125 -17.59375 L 38.53125 0 L 23.671875 0 L 19.703125 -10.625 L 19.453125 -10.625 L 10.5625 0 L -3.578125 0 L 13.5625 -17.921875 L 5.5 -33.796875 L 20.484375 -33.796875 L 23.8125 -24.828125 L 24.0625 -24.828125 L 31.484375 -33.796875 Z M 45.5625 -33.796875 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(187.496867, 211.874986)"><g><path d="M 24 -44.03125 L 13.890625 0 L -0.25 0 L 9.859375 -44.03125 Z M 29.25 -34.375 L 31.421875 -44.03125 L 39.171875 -44.03125 C 42.578125 -44.03125 45.21875 -43.003906 47.09375 -40.953125 C 48.976562 -38.910156 49.554688 -36.332031 48.828125 -33.21875 C 47.585938 -27.800781 44.347656 -24.382812 39.109375 -22.96875 L 39.109375 -22.71875 C 44.390625 -21.4375 46.328125 -17.769531 44.921875 -11.71875 C 44.109375 -8.257812 42.21875 -5.441406 39.25 -3.265625 C 36.289062 -1.085938 33.019531 0 29.4375 0 L 21.3125 0 L 23.671875 -10.234375 L 27 -10.234375 C 27.601562 -10.234375 28.179688 -10.382812 28.734375 -10.6875 C 29.285156 -10.988281 29.765625 -11.40625 30.171875 -11.9375 C 30.578125 -12.46875 30.863281 -13.054688 31.03125 -13.703125 L 31.171875 -14.34375 C 31.421875 -15.320312 31.300781 -16.140625 30.8125 -16.796875 C 30.320312 -17.460938 29.628906 -17.796875 28.734375 -17.796875 L 25.40625 -17.796875 L 27.515625 -26.875 L 29.828125 -26.875 C 30.722656 -26.875 31.5625 -27.203125 32.34375 -27.859375 C 33.132812 -28.523438 33.640625 -29.351562 33.859375 -30.34375 L 33.984375 -30.96875 C 34.191406 -31.914062 34.0625 -32.71875 33.59375 -33.375 C 33.125 -34.039062 32.441406 -34.375 31.546875 -34.375 Z M 29.25 -34.375 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(237.285815, 211.874986)"><g><path d="M 44.609375 -33.796875 L 25.015625 -1.03125 C 24.847656 -0.769531 24.703125 -0.507812 24.578125 -0.25 L 23.671875 -16.890625 L 32.0625 -33.796875 Z M 21.25 4.609375 C 19.03125 7.460938 16.585938 9.644531 13.921875 11.15625 C 11.253906 12.675781 8.148438 13.4375 4.609375 13.4375 C 1.835938 13.4375 -0.53125 13.050781 -2.5 12.28125 L -0.765625 4.671875 L 4.34375 4.671875 C 7.550781 4.671875 9.816406 3.113281 11.140625 0 L 5.5 -33.796875 L 19.203125 -33.796875 Z M 21.25 4.609375 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(276.387414, 211.874986)"><g><pathLine truncated

After

Width:  |  Height:  |  Size: 6.6 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

-14
View File
@@ -1,14 +0,0 @@
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
rootProject.name = "delta-force-guide-server"
+13
View File
@@ -0,0 +1,13 @@
import { LoginRequest, User } from "@/types"
import { WebClient } from "@/shared/web-client"
export async function login(loginRequest: LoginRequest): Promise<User> {
const { data } = await WebClient.post<User>("/auth/login", {
...loginRequest,
})
return data
}
export async function logout() {
await WebClient.get<void>("/auth/logout")
}
+56
View File
@@ -0,0 +1,56 @@
import { AddFirearmRequest, Direction, Firearm, FirearmType, Page, PageQueryParams } from "@/types"
import { WebClient } from "@/shared/web-client"
import { asUrlSearchParam } from "@/utils/query-param-utils.ts"
interface FirearmParams extends PageQueryParams {
type?: FirearmType
}
/**
* Fetch firearm list
*
* @param params Paged query parameters
*/
export async function getFirearms(params?: FirearmParams): Promise<Page<Firearm>> {
let uri = "/firearms"
const urlSearchParam = asUrlSearchParam(params)
if (params?.type) {
urlSearchParam.append("type", params.type)
}
if (urlSearchParam.size > 0) {
uri = uri.concat("?", urlSearchParam.toString())
}
const { data } = await WebClient.get<Page<Firearm>>(uri)
return data
}
/**
* Fetch firearm by ID
*
* @param id Firearm ID
*/
export async function getFirearm(id: number): Promise<Firearm> {
const { data } = await WebClient.get<Firearm>(`/firearms/${id}`)
return data
}
/**
* Create firearm
* @param request
*/
export async function addFirearm(request: AddFirearmRequest): Promise<Firearm> {
const { data } = await WebClient.post<Firearm>("/firearms", request)
return data
}
export async function editFirearm(id: number, request: AddFirearmRequest): Promise<Firearm> {
const { data } = await WebClient.put<Firearm>(`/firearms/${id}`, request)
return data
}
export async function removeFirearm(id: number) {
await WebClient.delete<void>(`/firearms/${id}`)
}
+4
View File
@@ -0,0 +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"
+67
View File
@@ -0,0 +1,67 @@
import { Modification, ModificationRequest, Page, PageQueryParams } from "@/types"
import { WebClient } from "@/shared/web-client"
import { asUrlSearchParam } from "@/utils/query-param-utils.ts"
interface ModificationParams extends PageQueryParams {
firearmId?: string
tags?: string[]
}
export async function getModifications(params?: ModificationParams): Promise<Page<Modification>> {
let uri = "/modifications"
const urlSearchParams = asUrlSearchParam(params)
if (params?.firearmId) {
urlSearchParams.append("firearmId", "" + params.firearmId)
}
if (params?.tags) {
params.tags.forEach((tag) => urlSearchParams.append("tags", tag))
}
if (urlSearchParams.size > 0) {
uri = uri.concat("?", urlSearchParams.toString())
}
const { data } = await WebClient.get<Page<Modification>>(uri)
return data
}
export async function getModification(id: number): Promise<Modification> {
const { data } = await WebClient.get<Modification>(`/modifications/${id}`)
return data
}
export async function addModification(modification: ModificationRequest): Promise<Modification> {
const { data } = await WebClient.post<Modification>("/modifications", modification)
return data
}
export async function addModifications(
modifications: ModificationRequest[]
): Promise<Modification[]> {
const { data } = await WebClient.post<Modification[]>("/modifications/batch", {
modifications,
})
return data
}
export async function editModification(
id: number,
modification: ModificationRequest
): Promise<Modification> {
const { data } = await WebClient.put<Modification>(`/modifications/${id}`, modification)
return data
}
export async function removeModification(
id: number
): Promise<void> {
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()}`)
}
+18
View File
@@ -0,0 +1,18 @@
import { WebClient } from "@/shared/web-client"
export async function getTags(firearmId?: number): Promise<string[]> {
let uri = "/tags"
const urlSearchParam = new URLSearchParams()
if (firearmId) {
urlSearchParam.append("firearmId", "" + firearmId)
}
if (urlSearchParam.size > 0) {
uri = uri.concat("?", urlSearchParam.toString())
}
const { data } = await WebClient.get<string[]>(uri)
return data
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" zoomAndPan="magnify" viewBox="0 0 375 374.999991" height="500" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><g/></defs><rect x="-37.5" width="450" fill="#ffffff" y="-37.499999" height="449.999989" fill-opacity="1"/><rect x="-37.5" width="450" fill="#324370" y="-37.499999" height="449.999989" fill-opacity="1"/><g fill="#ffffff" fill-opacity="1"><g transform="translate(27.506677, 211.874986)"><g><path d="M 12.359375 -0.453125 C 8.003906 -1.816406 5.003906 -4.3125 3.359375 -7.9375 C 1.710938 -11.5625 1.550781 -16.253906 2.875 -22.015625 C 4.195312 -27.773438 6.519531 -32.46875 9.84375 -36.09375 C 13.175781 -39.71875 17.34375 -42.210938 22.34375 -43.578125 Z M 27.578125 -34.109375 L 30.015625 -44.796875 L 31.875 -44.796875 C 39.46875 -44.796875 44.882812 -42.851562 48.125 -38.96875 C 51.363281 -35.09375 52.128906 -29.441406 50.421875 -22.015625 C 48.722656 -14.585938 45.351562 -8.929688 40.3125 -5.046875 C 35.28125 -1.171875 28.96875 0.765625 21.375 0.765625 C 20.519531 0.765625 19.898438 0.742188 19.515625 0.703125 L 21.953125 -9.921875 C 22.722656 -9.835938 23.34375 -9.796875 23.8125 -9.796875 C 26.84375 -9.796875 29.367188 -10.710938 31.390625 -12.546875 C 33.421875 -14.378906 34.796875 -16.851562 35.515625 -19.96875 L 36.421875 -24.0625 C 37.140625 -27.175781 36.910156 -29.648438 35.734375 -31.484375 C 34.566406 -33.316406 32.46875 -34.234375 29.4375 -34.234375 C 28.96875 -34.234375 28.347656 -34.191406 27.578125 -34.109375 Z M 27.578125 -34.109375 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(80.815405, 211.874986)"><g><path d="M 31.171875 -34.5625 C 28.816406 -34.5625 26.363281 -34.003906 23.8125 -32.890625 L 21.953125 -24.890625 C 22.460938 -25.066406 23.0625 -25.15625 23.75 -25.15625 C 25.15625 -25.15625 26.125 -24.71875 26.65625 -23.84375 C 27.1875 -22.96875 27.28125 -21.804688 26.9375 -20.359375 L 22.265625 0 L 35 0 L 40.0625 -21.890625 C 41 -25.984375 40.726562 -29.117188 39.25 -31.296875 C 37.78125 -33.472656 35.085938 -34.5625 31.171875 -34.5625 Z M 17.28125 -19.96875 L 17.796875 -28.671875 L 18.171875 -33.796875 L 7.75 -33.796875 L -0.0625 0 L 12.671875 0 L 17.21875 -19.84375 Z M 17.28125 -19.96875 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(123.500788, 211.874986)"><g><path d="M 19.515625 -33.796875 L 11.703125 0 L -1.03125 0 L 6.78125 -33.796875 Z M 22.46875 -46.40625 L 20.484375 -37.765625 L 7.75 -37.765625 L 9.71875 -46.40625 Z M 22.46875 -46.40625 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(144.811483, 211.874986)"><g><path d="M 45.5625 -33.796875 L 29.953125 -17.59375 L 38.53125 0 L 23.671875 0 L 19.703125 -10.625 L 19.453125 -10.625 L 10.5625 0 L -3.578125 0 L 13.5625 -17.921875 L 5.5 -33.796875 L 20.484375 -33.796875 L 23.8125 -24.828125 L 24.0625 -24.828125 L 31.484375 -33.796875 Z M 45.5625 -33.796875 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(187.496867, 211.874986)"><g><path d="M 24 -44.03125 L 13.890625 0 L -0.25 0 L 9.859375 -44.03125 Z M 29.25 -34.375 L 31.421875 -44.03125 L 39.171875 -44.03125 C 42.578125 -44.03125 45.21875 -43.003906 47.09375 -40.953125 C 48.976562 -38.910156 49.554688 -36.332031 48.828125 -33.21875 C 47.585938 -27.800781 44.347656 -24.382812 39.109375 -22.96875 L 39.109375 -22.71875 C 44.390625 -21.4375 46.328125 -17.769531 44.921875 -11.71875 C 44.109375 -8.257812 42.21875 -5.441406 39.25 -3.265625 C 36.289062 -1.085938 33.019531 0 29.4375 0 L 21.3125 0 L 23.671875 -10.234375 L 27 -10.234375 C 27.601562 -10.234375 28.179688 -10.382812 28.734375 -10.6875 C 29.285156 -10.988281 29.765625 -11.40625 30.171875 -11.9375 C 30.578125 -12.46875 30.863281 -13.054688 31.03125 -13.703125 L 31.171875 -14.34375 C 31.421875 -15.320312 31.300781 -16.140625 30.8125 -16.796875 C 30.320312 -17.460938 29.628906 -17.796875 28.734375 -17.796875 L 25.40625 -17.796875 L 27.515625 -26.875 L 29.828125 -26.875 C 30.722656 -26.875 31.5625 -27.203125 32.34375 -27.859375 C 33.132812 -28.523438 33.640625 -29.351562 33.859375 -30.34375 L 33.984375 -30.96875 C 34.191406 -31.914062 34.0625 -32.71875 33.59375 -33.375 C 33.125 -34.039062 32.441406 -34.375 31.546875 -34.375 Z M 29.25 -34.375 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(237.285815, 211.874986)"><g><path d="M 44.609375 -33.796875 L 25.015625 -1.03125 C 24.847656 -0.769531 24.703125 -0.507812 24.578125 -0.25 L 23.671875 -16.890625 L 32.0625 -33.796875 Z M 21.25 4.609375 C 19.03125 7.460938 16.585938 9.644531 13.921875 11.15625 C 11.253906 12.675781 8.148438 13.4375 4.609375 13.4375 C 1.835938 13.4375 -0.53125 13.050781 -2.5 12.28125 L -0.765625 4.671875 L 4.34375 4.671875 C 7.550781 4.671875 9.816406 3.113281 11.140625 0 L 5.5 -33.796875 L 19.203125 -33.796875 Z M 21.25 4.609375 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(276.387414, 211.874986)"><g><pathLine truncated

After

Width:  |  Height:  |  Size: 6.6 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32"
preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228">
<path fill="#00D8FF"
d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z" />
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+69
View File
@@ -0,0 +1,69 @@
import { useRouteError, Link } from "react-router-dom"
/**
* Error page component that displays when routing errors occur.
* Uses React Router's useRouteError hook to get error information.
*/
export default function ErrorPage() {
const error = useRouteError() as Error & { statusText?: string; status?: number }
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<div className="text-center">
{/* Error Icon */}
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
<svg
className="h-6 w-6 text-red-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
{/* Error Title */}
<h1 className="mt-4 text-2xl font-bold text-gray-900">
页面出现错误
</h1>
{/* Error Message */}
<div className="mt-4">
<p className="text-sm text-gray-600">
{error?.statusText ?? error?.message ?? "发生了未预期的错误"}
</p>
{error?.status && (
<p className="text-xs text-gray-500 mt-1">
错误代码:{error.status}
</p>
)}
</div>
{/* Action Buttons */}
<div className="mt-6 flex flex-col space-y-3">
<Link
to="/"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
返回首页
</Link>
<button
onClick={() => window.history.back()}
className="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
返回上一页
</button>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -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<AddFirearmRequest>()
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 (
<Modal
title="新建武器"
open={open}
onCancel={onCancel}
onOk={() => form.submit()}
okText="创建"
cancelText="取消"
confirmLoading={loading}
destroyOnHidden
afterOpenChange={(visible) => {
if (!visible) {
form.resetFields()
}
}}>
<FirearmForm form={form} onFinish={onFinish} />
</Modal>
)
}
@@ -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<AddFirearmRequest>()
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 (
<Modal
title="编辑武器"
open={open}
onCancel={onCancel}
onOk={() => form.submit()}
okText="保存"
cancelText="取消"
confirmLoading={loading}
destroyOnHidden
>
<FirearmForm form={form} onFinish={onFinish} />
</Modal>
)
}
+101
View File
@@ -0,0 +1,101 @@
import { Form, Input, InputNumber, Select } from "antd"
import { AddFirearmRequest, FirearmType } from "@/types"
import calibres from "@/constant/calibres.json"
const firearmTypeText: Record<FirearmType, string> = {
RIFLE: "步枪",
SUB_MACHINE_GUN: "冲锋枪",
SHOTGUN: "霰弹枪",
LIGHT_MACHINE_GUN: "轻机枪",
DESIGNATED_MARKSMAN_RIFLE: "射手步枪",
SNIPER_RIFLE: "狙击步枪",
PISTOL: "手枪",
SPECIAL: "特殊",
}
const calibreOptions = calibres.map((calibre) => ({
value: calibre,
label: calibre,
}))
interface FirearmFormProps {
form: ReturnType<typeof Form.useForm<AddFirearmRequest>>[0]
onFinish: (values: AddFirearmRequest) => void
}
export default function FirearmForm({ form, onFinish }: FirearmFormProps) {
return (
<Form<AddFirearmRequest> form={form} layout="vertical" onFinish={onFinish} requiredMark={false}>
<Form.Item<AddFirearmRequest>
name="name"
label="武器名称"
rules={[{ required: true, message: "请输入武器名称" }]}
>
<Input placeholder="请输入武器名称" />
</Form.Item>
<Form.Item<AddFirearmRequest>
name="type"
label="武器类型"
rules={[{ required: true, message: "请选择武器类型" }]}
>
<Select
placeholder="请选择武器类型"
options={Object.entries(firearmTypeText).map(([value, label]) => ({
value,
label,
}))}
/>
</Form.Item>
<Form.Item<AddFirearmRequest>
name="level"
label="武器输出等级"
rules={[{ required: true, message: "请输入武器输出等级" }]}
>
<Input placeholder="例如:T0" />
</Form.Item>
<Form.Item<AddFirearmRequest>
name="calibre"
label="子弹口径"
rules={[{ required: true, message: "请选择子弹口径" }]}
>
<Select
placeholder="请选择子弹口径"
showSearch
optionFilterProp="label"
options={calibreOptions}
/>
</Form.Item>
<Form.Item<AddFirearmRequest>
name="fireRate"
label="射速(每分钟发数)"
rules={[{ required: true, message: "请输入射速" }]}
>
<InputNumber className="w-full" min={1} precision={0} placeholder="请输入射速" />
</Form.Item>
<Form.Item<AddFirearmRequest>
name="armourDamage"
label="甲伤"
rules={[{ required: true, message: "请输入甲伤" }]}
>
<InputNumber className="w-full" min={0} precision={0} placeholder="请输入甲伤" />
</Form.Item>
<Form.Item<AddFirearmRequest>
name="bodyDamage"
label="肉伤"
rules={[{ required: true, message: "请输入肉伤" }]}
>
<InputNumber className="w-full" min={0} precision={0} placeholder="请输入肉伤" />
</Form.Item>
<Form.Item<AddFirearmRequest> name="review" label="描述">
<Input.TextArea rows={4} placeholder="可选:补充武器特点或使用建议" />
</Form.Item>
</Form>
)
}
@@ -0,0 +1,17 @@
import React from "react"
interface MarkdownRendererProps {
/** HTML string processed by vite-plugin-markdown */
html: string
/** Optional custom class name */
className?: string
}
export default function MarkdownRenderer({ html, className = "" }: MarkdownRendererProps) {
return (
<article
className={`prose prose-slate max-w-none dark:prose-invert ${className}`}
dangerouslySetInnerHTML={{ __html: html }}
/>
)
}
@@ -0,0 +1,97 @@
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<ModificationRequest>()
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!open) {
return
}
form.setFieldsValue({
firearmId: lockedFirearmId ?? defaultFirearmId,
accessories: [],
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 (
<Modal
title="新建改枪"
open={open}
onCancel={onCancel}
onOk={() => form.submit()}
okText="创建"
cancelText="取消"
confirmLoading={loading}
width={820}
destroyOnHidden
afterOpenChange={(visible) => {
if (!visible) {
form.resetFields()
}
}}
>
<ModificationForm form={form} onFinish={onFinish} lockFirearmId={lockedFirearmId} />
</Modal>
)
}
@@ -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<ModificationRequest>()
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 (
<Modal
title="编辑改枪"
open={open}
onCancel={onCancel}
onOk={() => form.submit()}
okText="保存"
cancelText="取消"
confirmLoading={loading}
width={820}
destroyOnHidden
>
<ModificationForm form={form} onFinish={onFinish} lockFirearmId={lockedFirearmId} />
</Modal>
)
}
+217
View File
@@ -0,0 +1,217 @@
import { useEffect, useMemo, useState } from "react"
import { FirearmApi } from "@/api"
import slotNames from "@/constant/slots.json"
import tuningNames from "@/constant/tunings.json"
import { Firearm, ModificationRequest } from "@/types"
import { AutoComplete, Button, Card, Form, Input, InputNumber, Select, Space } from "antd"
interface ModificationFormProps {
form: ReturnType<typeof Form.useForm<ModificationRequest>>[0]
onFinish: (values: ModificationRequest) => void
lockFirearmId?: number
}
const slotOptions = slotNames.map((slotName) => ({ value: slotName }))
const tuningOptions = tuningNames.map((tuningName) => ({ value: tuningName }))
export default function ModificationForm({ form, onFinish, lockFirearmId }: ModificationFormProps) {
const [firearmOptions, setFirearmOptions] = useState<Array<{ value: number; label: string }>>([])
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<ModificationRequest>
form={form}
layout="vertical"
onFinish={onFinish}
requiredMark={false}>
<Form.Item<ModificationRequest>
name="firearmId"
label="武器"
rules={[{ required: true, message: "请输入武器" }]}>
<Select<number>
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())
},
}}
/>
</Form.Item>
<Form.Item<ModificationRequest>
name="name"
label="改装名称"
rules={[{ required: true, message: "请输入改装名称" }]}>
<Input placeholder="请输入改装名称" />
</Form.Item>
<Form.Item<ModificationRequest>
name="code"
label="改枪码"
rules={[{ required: true, message: "请输入改枪码" }]}>
<Input placeholder="请输入改枪码" />
</Form.Item>
<Form.Item<ModificationRequest> name="tags" label="标签">
<Select mode="tags" tokenSeparators={[",", " "]} placeholder="可选:输入后回车" />
</Form.Item>
<Form.Item<ModificationRequest> name="author" label="作者">
<Input placeholder="可选:请输入作者" />
</Form.Item>
<Form.Item<ModificationRequest> name="videoUrl" label="视频链接">
<Input placeholder="可选:请输入视频链接" />
</Form.Item>
<Form.Item<ModificationRequest> name="note" label="备注">
<Input.TextArea rows={3} placeholder="可选:补充说明" />
</Form.Item>
<Form.List name="accessories">
{(accessoryFields, { add: addAccessory, remove: removeAccessory }) => (
<div className="flex flex-col gap-4">
{accessoryFields.map((accessoryField) => (
<Card
key={accessoryField.key}
title={`配件 ${accessoryField.name + 1}`}
size="small"
extra={
<Button
danger
type="link"
size="small"
onClick={() => removeAccessory(accessoryField.name)}>
删除配件
</Button>
}>
<Form.Item
name={[accessoryField.name, "slotName"]}
label="槽位"
rules={[{ required: true, message: "请选择或输入槽位" }]}>
<AutoComplete options={slotOptions} placeholder="请选择或输入槽位" />
</Form.Item>
<Form.Item
name={[accessoryField.name, "accessoryName"]}
label="配件名称"
rules={[{ required: true, message: "请输入配件名称" }]}>
<Input placeholder="请输入配件名称" />
</Form.Item>
<Form.List name={[accessoryField.name, "tunings"]}>
{(tuningFields, { add: addTuning, remove: removeTuning }) => (
<div className="flex flex-col gap-3">
{tuningFields.map((tuningField) => (
<Space key={tuningField.key} align="start" className="w-full" wrap>
<Form.Item
name={[tuningField.name, "tuningName"]}
label="精校属性"
rules={[{ required: true, message: "请选择或输入精校属性" }]}>
<AutoComplete
options={tuningOptions}
placeholder="例如:后坐控制"
className="w-44"
/>
</Form.Item>
<Form.Item
name={[tuningField.name, "tuningValue"]}
label="精校值"
rules={[{ required: true, message: "请输入精校值" }]}>
<InputNumber className="w-32" placeholder="例如:0.35" />
</Form.Item>
<Button
type="link"
danger
className="mt-8"
onClick={() => removeTuning(tuningField.name)}>
删除
</Button>
</Space>
))}
<Button
type="dashed"
disabled={tuningFields.length >= 2}
onClick={() => addTuning({ tuningName: "", tuningValue: 0 })}>
添加精校
</Button>
</div>
)}
</Form.List>
</Card>
))}
<Button
variant="solid"
color="lime"
onClick={() => addAccessory({ slotName: "", accessoryName: "", tunings: [] })}>
添加配件
</Button>
</div>
)}
</Form.List>
</Form>
)
}
+23
View File
@@ -0,0 +1,23 @@
[
".338 Lap Mag",
".357 Magnum",
".45 ACP",
".50 AE",
".50 BMG",
"12 Gauge",
"12.7x55mm",
"5.45x39mm",
"5.56x45mm",
"5.7x28mm",
"5.8x42mm",
"6.8x51mm",
"7.62x39mm",
"7.62x51mm",
"7.62x54mm",
"9x19mm",
"9x39mm",
"4.6x30mm",
".300 BLK",
"箭矢",
"45-70 Govt"
]
+19
View File
@@ -0,0 +1,19 @@
[
"枪口",
"枪管",
"贴片",
"瞄准镜",
"战术设备",
"增高座瞄具",
"侧瞄具",
"枪托",
"托腮板",
"枪托套件",
"导轨脚架",
"前握把",
"后握把",
"后握贴片",
"握把座",
"弹匣",
"弹匣座"
]
+1
View File
@@ -0,0 +1 @@
["安装位置", "厚度", "缩放倍率", "长度", "瞳距", "配重", "托腮板安装位置"]
+29
View File
@@ -0,0 +1,29 @@
# 《三角洲行动游戏指南》最终用户许可协议
> 最近更新日期: 2026年5月6日
感谢您访问《三角洲行动游戏指南》(以下简称“本站”)。在访问或使用本站前,请您务必仔细阅读本协议。
## 第一条:项目性质与声明
本站是由《三角洲行动》游戏玩家开发的兴趣爱好项目,与游戏官方(包括但不限于腾讯、琳恩工作室等)不存在任何形式的关联、授权或代理关系。
本站仅旨在为玩家提供游戏资讯、攻略及技术性指南,不保证资讯的绝对实时性与准确性。
## 第二条:内容版权说明
本站部分指南、攻略及图片素材提取、整理自哔哩哔哩、抖音等视频平台的公开视频,本站均会标注原作者信息或来源。相关内容的知识产权归原作者所有。
由本站开发者原创或邀请的高手提供的特约稿件,其版权归本站或原提供者所有。
严禁任何个人或组织在未经本站或原作者授权的情况下,将本站内容用于商业牟利。
## 第三条:免责声明
游戏机制可能随版本更新而变动,因参考本站指南而产生的任何游戏结果(如战绩波动、游戏内损失等),本站概不负责。
来源标注中可能包含指向第三方平台的链接,本站不对第三方平台内容的合法性或安全性负责。
## 第四条:协议变更
本站保留随时修改本协议的权利。重大变更将通过站点公告形式发布。
+33
View File
@@ -0,0 +1,33 @@
# 《三角洲行动游戏指南》隐私政策
本隐私政策旨在向您说明在无需登录的情况下,本站如何处理与您相关的信息。
## 第一条:信息收集情况
1. **个人信息:** 由于本站采用无需登录的模式,我们不会收集您的姓名、手机号、身份证号或精确地理位置等个人敏感数据。
2. **服务器日志:** 本站未集成任何第三方流量统计工具(如 Google Analytics 或百度统计),不会主动记录您的 IP 地址或用户代理(User-agent)数据。
## 第二条:Cookie 的使用
1. **必要性 Cookie:** 本站仅使用必要的 Cookie 用于保存与后端交互所需的功能性数据(如临时会话标识或您的本地设置偏好)。
2. **非追踪性:** 这些 Cookie 不用于追踪您的个人跨站行为,也不会用于构建您的个人画像。
## 第三条:第三方广告与赞助
1. **现状说明:** 本站目前不包含任何商业广告,亦未开通打赏或赞助入口。
2. **未来规划:** 考虑到运营成本,本站保留未来接入广告服务或赞助链接的权利。届时,广告商可能会根据其自身的隐私政策使用 Cookie。一旦此类功能上线,我们将同步更新本政策。
## 第四条:数据安全
我们致力于保护站点的运行安全,防止现有数据被非法篡改或泄露。
## 第五条:联系我们
若您对本协议或内容版权有任何疑问,请通过以下方式联系:
开发者: Zihlu Wang、Xingyao Fan
联系渠道: [GitHub](https://github.com/zihluwang/delta-force-guide-web)
+66
View File
@@ -0,0 +1,66 @@
@layer theme, base, antd, components, utilities;
@import 'tailwindcss';
@plugin "@tailwindcss/typography";
html, body {
margin: 0;
padding: 0;
width: 100%;
}
#root {
width: 100%;
}
.mod-codes-grid {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@media (min-width: 640px) {
.mod-codes-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.mod-codes-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (min-width: 1280px) {
.mod-codes-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.nav-item {
position: relative;
overflow: hidden;
}
.nav-item::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 0;
background: linear-gradient(to top, #22ffa7, transparent);
transition: height 0.2s ease-in-out;
opacity: 0.35;
pointer-events: none;
}
.nav-item:hover::after,
.nav-item.active::after {
height: 80%; /* Height of the upward glow; adjustable. */
}
.nav-item:hover,
.nav-item.active {
color: white;
}
+13
View File
@@ -0,0 +1,13 @@
import { Outlet } from "react-router-dom"
/**
* Empty layout component that provides minimal structure.
* Useful for pages that need full control over their layout.
*/
export default function EmptyLayout() {
return (
<div className="min-h-screen">
<Outlet />
</div>
)
}
+170
View File
@@ -0,0 +1,170 @@
import { Outlet, Link, NavLink } from "react-router-dom"
import { useMemo } from "react"
import dayjs from "dayjs"
import { Dropdown } from "antd"
import {
FileTextOutlined,
GithubOutlined,
LockOutlined,
LoginOutlined,
} from "@ant-design/icons"
import { AuthApi } from "@/api"
import { useAppDispatch, useAppSelector } from "@/store/hooks"
import { clearCurrentUser } from "@/store/auth-slice"
import { useState } from "react"
/**
* Main application component that serves as the root layout.
* Uses React Router's Outlet to render child routes.
*/
export default function HeroLayout() {
const today = useMemo(() => dayjs(), [])
const user = useAppSelector((state) => state.auth.user)
const dispatch = useAppDispatch()
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
async function handleLogout() {
try {
await AuthApi.logout()
} finally {
dispatch(clearCurrentUser())
}
}
return (
<div className="bg-gray-50 ">
{/* Navigation Header */}
<header className="bg-[#0b0f14] shadow-sm border-b">
<div className="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-10 h-full">
<div className="flex justify-between items-center h-20">
<div className="flex items-center">
<h1 className="text-xl font-semibold text-white">《三角洲》指南</h1>
</div>
<nav className="flex h-full">
<NavLink
to="/firearms"
className={({ isActive }) =>
`nav-item inline-flex items-center px-10 h-full text-base font-medium transition-all duration-200 ${
isActive ? "active" : ""
} text-gray-500 hover:text-white`
}>
武器列表
</NavLink>
<NavLink
to="/mod-codes"
className={({ isActive }) =>
`nav-item inline-flex items-center px-10 h-full text-base font-medium transition-all duration-200 ${
isActive ? "active" : ""
} text-gray-500 hover:text-white`
}>
改枪码
</NavLink>
</nav>
</div>
</div>
</header>
{/* Main Content Area */}
<main className="max-w-screen-2xl mx-auto py-6 sm:px-6 lg:px-10">
<div className="px-4 py-6 sm:px-0">
<Outlet />
</div>
</main>
{/* Footer */}
<footer className="bg-black border-t border-gray-800">
<div className="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-10 py-8">
<div className="flex flex-wrap justify-center items-center gap-x-6 gap-y-10 text-sm">
<div
className="relative"
onMouseEnter={() => setIsDropdownOpen(true)}
onMouseLeave={() => setIsDropdownOpen(false)}>
<button
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200 focus:outline-none"
aria-label="GitHub 仓库">
<GithubOutlined className="text-base opacity-80" />
<span>GitHub</span>
<svg
className={`w-3 h-3 transition-transform duration-200 ${isDropdownOpen ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{isDropdownOpen && (
<div className="absolute top-full left-2/3 -translate-x-1/2 w-18 bg-gray-950 border border-gray-700 rounded-lg shadow-xl py-1 z-20 opacity-60">
<a
href="https://github.com/zihluwang/frontend-repo"
target="_blank"
rel="noopener noreferrer"
className="block px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white transition-colors">
前端
</a>
<a
href="https://github.com/zihluwang/backend-repo"
target="_blank"
rel="noopener noreferrer"
className="block px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white transition-colors">
后端
</a>
</div>
)}
</div>
<span className="text-gray-700 select-none">•</span>
<Link
to="/legal?tab=eula"
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200">
<FileTextOutlined className="text-lg opacity-80" />
EULA
</Link>
<span className="text-gray-700 select-none">•</span>
<Link
to="/legal?tab=privacy"
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200">
<LockOutlined className="text-lg opacity-80" />
隐私政策
</Link>
<span className="text-gray-700 select-none">•</span>
{user ? (
<Dropdown
trigger={["hover"]}
menu={{
items: [
{
key: "logout",
label: "退出登录",
danger: true,
onClick: handleLogout,
},
],
}}>
<span className="nav-item inline-flex items-center px-10 h-full text-base font-medium text-gray-500 hover:text-white cursor-pointer">
{user.username}
</span>
</Dropdown>
) : (
<Link
to="/login"
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200">
<LoginOutlined className="text-lg opacity-80" />
登录
</Link>
)}
</div>
<div className="border-t border-gray-800 my-6" />
<div className="text-center text-xs text-gray-500">
<p>© 2024-{today.year()} Zihlu Wang 和 OnixByte。使用 React 与 TypeScript 构建。</p>
</div>
</div>
</footer>
</div>
)
}
+35
View File
@@ -0,0 +1,35 @@
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { RouterProvider } from "react-router-dom"
import { Provider } from "react-redux"
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 router from "@/router"
import store, { persistor } from "@/store"
import "./index.css"
/**
* Main application entry point.
* Sets up the React app with React Router.
*/
createRoot(document.getElementById("root")!).render(
<StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<AntStyleProvider layer>
<AntConfigProvider
locale={AntSimplifiedChinese}
button={{
autoInsertSpace: false,
}}>
<AntApp className="h-full w-full">
<RouterProvider router={router} />
</AntApp>
</AntConfigProvider>
</AntStyleProvider>
</PersistGate>
</Provider>
</StrictMode>
)
@@ -1,13 +0,0 @@
package com.onixbyte.deltaforceguide;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DeltaForceGuideApplication {
public static void main(String[] args) {
SpringApplication.run(DeltaForceGuideApplication.class, args);
}
}
@@ -1,102 +0,0 @@
package com.onixbyte.deltaforceguide.config;
import com.onixbyte.deltaforceguide.shared.JacksonRedisSerialiser;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.time.Duration;
/**
* Configuration class for Redis-based caching components.
* <p>
* This configuration class provides beans for Redis cache management and template operations
* within the Helix application. It configures custom serialisation strategies using
* {@link GenericJackson2JsonRedisSerializer} for values and string serialisation for keys,
* ensuring optimal performance and compatibility with JSON-based data structures.
* <p>
* The configuration includes:
* <ul>
* <li>Custom {@link RedisCacheManager} with JSON serialisation support</li>
* <li>Configured {@link RedisTemplate} for direct Redis operations</li>
* </ul>
*
* @author zihluwang
* @see RedisCacheManager
* @see RedisTemplate
* @see GenericJackson2JsonRedisSerializer
* @since 1.0.0
*/
@Configuration
@EnableCaching
public class CacheConfig {
/**
* Creates a custom Redis cache manager with JSON serialisation support.
* <p>
* This method configures a {@link RedisCacheManager} that uses string serialisation for cache
* keys and {@link GenericJackson2JsonRedisSerializer} for cache values. This setup ensures that
* complex objects can be stored and retrieved from Redis cache whilst maintaining readability
* and compatibility with JSON-based systems.
*
* @param connectionFactory the Redis connection factory used to establish connections
* @return a configured {@link RedisCacheManager} with custom serialisation settings
* @see RedisCacheManager
* @see GenericJackson2JsonRedisSerializer
* @see RedisSerializationContext
*/
@Bean
public RedisCacheManager cacheManager(
RedisConnectionFactory connectionFactory
) {
var _keySerializer = RedisSerializer.string();
var cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(_keySerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(JacksonRedisSerialiser.INSTANCE))
.entryTtl(Duration.ofMinutes(90L));
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(connectionFactory)
.cacheDefaults(cacheConfiguration)
.build();
}
/**
* Creates a Redis template for direct Redis operations with custom serialisation.
* <p>
* This method configures a {@link RedisTemplate} that uses string serialisation for keys
* and {@link GenericJackson2JsonRedisSerializer} for values. This template provides low-level
* access to Redis operations whilst ensuring consistent serialisation strategies across
* the application.
* <p>
* The template is fully configured and ready for use after bean creation.
*
* @param connectionFactory the Redis connection factory used to establish connections
* @return a fully configured {@link RedisTemplate} for Redis operations
* @see RedisTemplate
* @see GenericJackson2JsonRedisSerializer
* @see RedisSerializer
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory
) {
var redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(JacksonRedisSerialiser.INSTANCE);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
@@ -1,47 +0,0 @@
package com.onixbyte.deltaforceguide.config;
import com.onixbyte.deltaforceguide.properties.CorsProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Optional;
import java.util.stream.Stream;
@Configuration
@EnableConfigurationProperties({CorsProperties.class})
public class CorsConfig implements WebMvcConfigurer {
private final CorsProperties properties;
public CorsConfig(CorsProperties properties) {
this.properties = properties;
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(toSafeArray(properties.allowedOrigins()))
.allowedHeaders(toSafeArray(properties.allowedHeaders()))
.allowedMethods(toHttpMethodNames(properties.allowedMethods()))
.allowCredentials(properties.allowCredentials())
.maxAge(properties.maxAge().toSeconds())
.exposedHeaders(toSafeArray(properties.exposedHeaders()));
}
private static String[] toSafeArray(String[] values) {
return values == null ? new String[0] : values;
}
private static String[] toHttpMethodNames(HttpMethod[] methods) {
return Optional.ofNullable(methods)
.stream()
.flatMap(Stream::of)
.map(HttpMethod::name)
.toList()
.toArray(String[]::new);
}
}
@@ -1,20 +0,0 @@
package com.onixbyte.deltaforceguide.config;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.onixbyte.deltaforceguide.shared.JacksonModules;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomiser() {
return (builder) -> {
builder.modules(JacksonModules.DATE_TIME_MODULE);
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
};
}
}
@@ -1,10 +0,0 @@
package com.onixbyte.deltaforceguide.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan(basePackages = {"com.onixbyte.deltaforceguide.mapper"})
public class MyBatisConfig {
}
@@ -1,10 +0,0 @@
package com.onixbyte.deltaforceguide.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@Configuration
@EnableJpaRepositories(basePackages = {"com.onixbyte.deltaforceguide.repository"})
public class SpringDataConfig {
}
@@ -1,45 +0,0 @@
package com.onixbyte.deltaforceguide.controller;
import com.onixbyte.deltaforceguide.domain.dto.FirearmResponse;
import com.onixbyte.deltaforceguide.domain.dto.PageResponse;
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
import com.onixbyte.deltaforceguide.service.FirearmService;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Validated
@RestController
@RequestMapping("/firearms")
public class FirearmController {
private final FirearmService firearmService;
public FirearmController(FirearmService firearmService) {
this.firearmService = firearmService;
}
@GetMapping
public PageResponse<FirearmResponse> pageQuery(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "DESC") Sort.Direction direction,
@RequestParam(required = false) FirearmType type
) {
return firearmService.pageQuery(type, PageRequest.of(page, size, Sort.by(direction, sortBy)));
}
@GetMapping("/{id}")
public FirearmResponse queryById(@PathVariable Long id) {
return firearmService.queryById(id);
}
}
@@ -1,45 +0,0 @@
package com.onixbyte.deltaforceguide.controller;
import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse;
import com.onixbyte.deltaforceguide.domain.dto.PageResponse;
import com.onixbyte.deltaforceguide.service.ModificationService;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Validated
@RestController
@RequestMapping("/modifications")
public class ModificationController {
private final ModificationService modificationService;
public ModificationController(ModificationService modificationService) {
this.modificationService = modificationService;
}
@GetMapping
public PageResponse<ModificationResponse> pageQuery(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size,
@RequestParam(required = false) @Positive Long firearmId,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "DESC") Sort.Direction direction
) {
return modificationService.pageQuery(firearmId, PageRequest.of(page, size, Sort.by(direction, sortBy)));
}
@GetMapping("/{id}")
public ModificationResponse queryById(@PathVariable Long id) {
return modificationService.queryById(id);
}
}
@@ -1,21 +0,0 @@
package com.onixbyte.deltaforceguide.domain.converter;
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
@Converter
public class FirearmTypeConverter implements AttributeConverter<FirearmType, Integer> {
@Override
public Integer convertToDatabaseColumn(FirearmType attribute) {
return attribute == null ? null : attribute.getCode();
}
@Override
public FirearmType convertToEntityAttribute(Integer dbData) {
return FirearmType.fromCode(dbData);
}
}
@@ -1,23 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import com.onixbyte.deltaforceguide.domain.entity.Firearm;
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
public record FirearmResponse(
Long id,
String name,
FirearmType type,
String level,
String review
) {
public static FirearmResponse from(Firearm firearm) {
return new FirearmResponse(
firearm.getId(),
firearm.getName(),
firearm.getType(),
firearm.getLevel(),
firearm.getReview()
);
}
}
@@ -1,30 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import com.onixbyte.deltaforceguide.domain.entity.Modification;
import java.util.List;
public record ModificationResponse(
Long id,
Long firearmId,
String name,
String code,
List<String> tags,
String note,
String author,
String videoUrl
) {
public static ModificationResponse from(Modification modification) {
return new ModificationResponse(
modification.getId(),
modification.getFirearm().getId(),
modification.getName(),
modification.getCode(),
modification.getTags(),
modification.getNote(),
modification.getAuthor(),
modification.getVideoUrl()
);
}
}
@@ -1,24 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import org.springframework.data.domain.Page;
import java.util.List;
public record PageResponse<T>(
List<T> items,
int page,
int size,
long totalElements,
int totalPages
) {
public static <T> PageResponse<T> from(Page<T> source) {
return new PageResponse<>(
source.getContent(),
source.getNumber(),
source.getSize(),
source.getTotalElements(),
source.getTotalPages()
);
}
}
@@ -1,100 +0,0 @@
package com.onixbyte.deltaforceguide.domain.entity;
import com.onixbyte.deltaforceguide.domain.converter.FirearmTypeConverter;
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "firearm")
public class Firearm {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false, length = 64)
private String name;
@Column(name = "type", nullable = false)
@Convert(converter = FirearmTypeConverter.class)
private FirearmType type;
@Column(name = "level", nullable = false)
private String level;
@Column(name = "review", columnDefinition = "TEXT")
private String review;
@OneToMany(mappedBy = "firearm", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Modification> modifications = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public FirearmType getType() {
return type;
}
public void setType(FirearmType type) {
this.type = type;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
public String getReview() {
return review;
}
public void setReview(String review) {
this.review = review;
}
public List<Modification> getModifications() {
return modifications;
}
public void setModifications(List<Modification> modifications) {
this.modifications = modifications;
}
public void addModification(Modification modification) {
this.modifications.add(modification);
modification.setFirearm(this);
}
public void removeModification(Modification modification) {
this.modifications.remove(modification);
modification.setFirearm(null);
}
}
@@ -1,120 +0,0 @@
package com.onixbyte.deltaforceguide.domain.entity;
import io.hypersistence.utils.hibernate.type.json.JsonType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import org.hibernate.annotations.Type;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(
name = "modification",
indexes = {
@Index(name = "idx_modification_firearm_id", columnList = "firearm_id")
}
)
public class Modification {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "firearm_id", nullable = false, foreignKey = @ForeignKey(name = "fk_modification_firearm"))
private Firearm firearm;
@Column(name = "name", nullable = false, length = 64)
private String name;
@Column(name = "code", nullable = false, length = 64)
private String code;
@Type(JsonType.class)
@Column(name = "tags", columnDefinition = "json")
private List<String> tags = new ArrayList<>();
@Column(name = "note", columnDefinition = "TEXT")
private String note;
@Column(name = "author", length = 64)
private String author;
@Column(name = "video_url", length = 512)
private String videoUrl;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Firearm getFirearm() {
return firearm;
}
public void setFirearm(Firearm firearm) {
this.firearm = firearm;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public List<String> getTags() {
return tags;
}
public void setTags(List<String> tags) {
this.tags = tags;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getVideoUrl() {
return videoUrl;
}
public void setVideoUrl(String videoUrl) {
this.videoUrl = videoUrl;
}
}
@@ -1,37 +0,0 @@
package com.onixbyte.deltaforceguide.enumeration;
public enum FirearmType {
RIFLE(0),
SUB_MACHINE_GUN(1),
SHOTGUN(2),
LIGHT_MACHINE_GUN(3),
DESIGNATED_MARKSMAN_RIFLE(4),
SNIPER_RIFLE(5),
PISTOL(6),
SPECIAL(7);
private final int code;
FirearmType(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static FirearmType fromCode(Integer code) {
if (code == null) {
return null;
}
for (FirearmType type : values()) {
if (type.code == code) {
return type;
}
}
throw new IllegalArgumentException("Unknown FirearmType code: " + code);
}
}
@@ -1,24 +0,0 @@
package com.onixbyte.deltaforceguide.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.http.HttpMethod;
import java.time.Duration;
@ConfigurationProperties(prefix = "app.cors")
public record CorsProperties(
@DefaultValue({"Content-Type", "Authorization"})
String[] allowedHeaders,
@DefaultValue({"GET", "POST", "PUT", "PATCH", "DELETE"})
HttpMethod[] allowedMethods,
String[] allowedOrigins,
boolean allowCredentials,
boolean allowPrivateNetwork,
@DefaultValue("PT2H")
Duration maxAge,
@DefaultValue({"Content-Type", "Authorization"})
String[] exposedHeaders
) {
}
@@ -1,15 +0,0 @@
package com.onixbyte.deltaforceguide.repository;
import com.onixbyte.deltaforceguide.domain.entity.Firearm;
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface FirearmRepository extends JpaRepository<Firearm, Long> {
Page<Firearm> findAllByType(FirearmType type, Pageable pageable);
}
@@ -1,26 +0,0 @@
package com.onixbyte.deltaforceguide.repository;
import com.onixbyte.deltaforceguide.domain.entity.Modification;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface ModificationRepository extends JpaRepository<Modification, Long> {
@EntityGraph(attributePaths = {"firearm"})
Page<Modification> findAllBy(Pageable pageable);
@EntityGraph(attributePaths = {"firearm"})
Page<Modification> findAllByFirearm_Id(Long firearmId, Pageable pageable);
@Override
@EntityGraph(attributePaths = {"firearm"})
Optional<Modification> findById(Long id);
}
@@ -1,40 +0,0 @@
package com.onixbyte.deltaforceguide.service;
import com.onixbyte.deltaforceguide.domain.dto.FirearmResponse;
import com.onixbyte.deltaforceguide.domain.dto.PageResponse;
import com.onixbyte.deltaforceguide.domain.entity.Firearm;
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
import com.onixbyte.deltaforceguide.repository.FirearmRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
@Service
public class FirearmService {
private final FirearmRepository firearmRepository;
public FirearmService(FirearmRepository firearmRepository) {
this.firearmRepository = firearmRepository;
}
@Transactional(readOnly = true)
public PageResponse<FirearmResponse> pageQuery(FirearmType type, Pageable pageable) {
Page<Firearm> page = type == null
? firearmRepository.findAll(pageable)
: firearmRepository.findAllByType(type, pageable);
return PageResponse.from(page.map(FirearmResponse::from));
}
@Transactional(readOnly = true)
public FirearmResponse queryById(Long id) {
return firearmRepository.findById(id)
.map(FirearmResponse::from)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Firearm not found: " + id));
}
}
@@ -1,39 +0,0 @@
package com.onixbyte.deltaforceguide.service;
import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse;
import com.onixbyte.deltaforceguide.domain.dto.PageResponse;
import com.onixbyte.deltaforceguide.domain.entity.Modification;
import com.onixbyte.deltaforceguide.repository.ModificationRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
@Service
public class ModificationService {
private final ModificationRepository modificationRepository;
public ModificationService(ModificationRepository modificationRepository) {
this.modificationRepository = modificationRepository;
}
@Transactional(readOnly = true)
public PageResponse<ModificationResponse> pageQuery(Long firearmId, Pageable pageable) {
Page<Modification> page = firearmId == null
? modificationRepository.findAllBy(pageable)
: modificationRepository.findAllByFirearm_Id(firearmId, pageable);
return PageResponse.from(page.map(ModificationResponse::from));
}
@Transactional(readOnly = true)
public ModificationResponse queryById(Long id) {
return modificationRepository.findById(id)
.map(ModificationResponse::from)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Modification not found: " + id));
}
}
@@ -1,46 +0,0 @@
package com.onixbyte.deltaforceguide.shared;
import java.time.format.DateTimeFormatter;
/**
* Utility class providing predefined {@link DateTimeFormatter} instances for common date and
* time patterns. These formatters can be used to parse and format {@code java.time} objects
* consistently throughout an application.
*
* @author zihluwang
*/
public class DateTimeFormatters {
/**
* A {@link DateTimeFormatter} for formatting and parsing full date and time with seconds, using
* the pattern "yyyy-MM-dd HH:mm:ss".
* <p>
* Example: "{@code 2023-10-27 15:30:45}"
*/
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* A {@link DateTimeFormatter} for formatting and parsing dates only, using the
* pattern "yyyy-MM-dd".
* <p>
* Example: "{@code 2023-10-27}"
*/
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/**
* A {@link DateTimeFormatter} for formatting and parsing times only, using the
* pattern "HH:mm:ss".
* <p>
* Example: "{@code 15:30:45}"
*/
public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
/**
* A {@link DateTimeFormatter} for formatting and parsing year and month only, using the
* pattern "yyyy-MM".
* <p>
* Example: "{@code 2023-10}"
*/
public static final DateTimeFormatter YEAR_MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
}
@@ -1,34 +0,0 @@
package com.onixbyte.deltaforceguide.shared;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
public class JacksonModules {
public static final SimpleModule DATE_TIME_MODULE = initialiseDateTimeModule();
private static SimpleModule initialiseDateTimeModule() {
var javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatters.DATE_TIME_FORMATTER));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatters.DATE_TIME_FORMATTER));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatters.DATE_FORMATTER));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatters.DATE_FORMATTER));
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatters.TIME_FORMATTER));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatters.TIME_FORMATTER));
return javaTimeModule;
}
}
@@ -1,22 +0,0 @@
package com.onixbyte.deltaforceguide.shared;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
public class JacksonRedisSerialiser {
public static final GenericJackson2JsonRedisSerializer INSTANCE = initialiseSerializer();
private static GenericJackson2JsonRedisSerializer initialiseSerializer() {
var serializer = new GenericJackson2JsonRedisSerializer();
serializer.configure((configurer) -> {
configurer.registerModule(new JavaTimeModule());
configurer.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
});
return serializer;
}
}
-46
View File
@@ -1,46 +0,0 @@
spring:
application:
name: delta-force-guide
cache:
type: redis
redis:
time-to-live: PT2H
data:
redis:
repositories:
# Disable redis repositories
enabled: false
jta:
# Disable JTA support
enabled: false
jpa:
properties:
hibernate:
transaction:
jta:
# No need to use distributed transaction manager for 1 datasource.
platform: org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform
hibernate:
ddl-auto: none
open-in-view: false
datasource:
hikari:
minimum-idle: 1
maximum-pool-size: 10
flyway:
enabled: true
baseline-on-migrate: true
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
map-underscore-to-camel-case: true
type-aliases-package: com.onixbyte.deltaforceguide.domain.entity
type-handlers-package: com.onixbyte.deltaforceguide.mapper.handler
mapper-locations: classpath:/mapper/*.xml
logging:
level:
org.hibernate:
orm.connections.pooling: off
@@ -1,34 +0,0 @@
DROP TABLE IF EXISTS firearm CASCADE;
DROP TABLE IF EXISTS modification CASCADE;
CREATE TABLE IF NOT EXISTS firearm
(
id BIGSERIAL NOT NULL PRIMARY KEY,
name VARCHAR(64) NOT NULL,
type INT NOT NULL,
level VARCHAR(10) NOT NULL,
review TEXT NULL
);
CREATE TABLE IF NOT EXISTS modification
(
id BIGSERIAL NOT NULL PRIMARY KEY,
firearm_id BIGINT NOT NULL,
name VARCHAR(64) NOT NULL,
code VARCHAR(64) NOT NULL,
tags JSONB NULL,
note TEXT NULL,
author VARCHAR(64) NULL,
video_url VARCHAR(512) NULL,
CONSTRAINT fk_modification_firearm
FOREIGN KEY (firearm_id)
REFERENCES firearm (id)
ON DELETE CASCADE
ON UPDATE RESTRICT
);
CREATE INDEX idx_modification_firearm_id ON modification (firearm_id);
+6
View File
@@ -0,0 +1,6 @@
declare module "*.md" {
const attributes: Record<string, any>
const html: string
const toc: { level: string; content: string; slug: string }[]
export { attributes, html, toc }
}
+214
View File
@@ -0,0 +1,214 @@
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/hooks"
import { Firearm, FirearmType } from "@/types"
import { Button, Card, Col, Pagination, Popconfirm, Row, Select, Tag, Typography, App } from "antd"
const firearmTypeText: Record<FirearmType, string> = {
RIFLE: "步枪",
SUB_MACHINE_GUN: "冲锋枪",
SHOTGUN: "霰弹枪",
LIGHT_MACHINE_GUN: "轻机枪",
DESIGNATED_MARKSMAN_RIFLE: "射手步枪",
SNIPER_RIFLE: "狙击步枪",
PISTOL: "手枪",
SPECIAL: "特殊",
}
const allTypeValue = "ALL"
type FirearmTypeFilter = FirearmType | typeof allTypeValue
function asDps(fireRate: number, damage: number) {
return ((fireRate / 60) * damage).toFixed(2)
}
export default function FirearmsPage() {
const user = useAppSelector((state) => state.auth.user)
const { message } = App.useApp()
const [page, setPage] = useState<number>(1)
const [typeFilter, setTypeFilter] = useState<FirearmTypeFilter>(allTypeValue)
const [firearms, setFirearms] = useState<Firearm[]>([])
const [total, setTotal] = useState<number>(0)
const [createModalOpen, setCreateModalOpen] = useState(false)
const [editingFirearm, setEditingFirearm] = useState<Firearm | null>(null)
const [deletingId, setDeletingId] = useState<number | null>(null)
const loadFirearms = useCallback(async () => {
const pagedData = await FirearmApi.getFirearms({
page: page - 1,
size: 12,
sortBy: "id",
direction: "ASC",
type: typeFilter === allTypeValue ? undefined : typeFilter,
})
setFirearms(pagedData.items)
setTotal(pagedData.totalElements)
}, [page, typeFilter])
useEffect(() => {
void 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 {
void loadFirearms()
}
} catch {
message.error("武器删除失败,请稍后重试")
} finally {
setDeletingId(null)
}
}
return (
<>
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
{user && (
<Button type="primary" onClick={() => setCreateModalOpen(true)}>
新建武器
</Button>
)}
</div>
<Select<FirearmTypeFilter>
className="w-full sm:w-64"
value={typeFilter}
options={[
{ value: allTypeValue, label: "全部类型" },
...Object.entries(firearmTypeText).map(([value, label]) => ({
value,
label,
})),
]}
onChange={(nextType) => {
setPage(1)
setTypeFilter(nextType)
}}
/>
</div>
<div className="mb-6">
<Row gutter={[16, 16]}>
{firearms.map((firearm) => (
<Col key={firearm.id} xs={24} md={12} lg={8}>
<Card
title={firearm.name}
extra={
user ? (
<div className="flex items-center gap-1">
<Button type="link" size="small" onClick={() => setEditingFirearm(firearm)}>
编辑
</Button>
<Popconfirm
title="确认删除武器"
description={`确定要删除 ${firearm.name} 吗?该操作不可撤销。`}
okText="删除"
cancelText="取消"
okButtonProps={{ danger: true, loading: deletingId === firearm.id }}
onConfirm={() => handleDelete(firearm)}>
<Button type="link" danger size="small" loading={deletingId === firearm.id}>
删除
</Button>
</Popconfirm>
</div>
) : null
}
variant="outlined"
styles={{
header: { minHeight: 56 },
}}
actions={[
<Link key={`mod-codes-${firearm.id}`} to={`/mod-codes?firearmId=${firearm.id}`}>
<Button type="link">查看改枪码</Button>
</Link>,
]}>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<Tag color="blue">{firearmTypeText[firearm.type]}</Tag>
</div>
<Typography.Text>
<strong>武器输出等级:</strong>
{firearm.level}
</Typography.Text>
<Typography.Text>
<strong>子弹口径:</strong>
{firearm.calibre}
</Typography.Text>
<Typography.Text>
<strong>每秒甲伤:</strong>
{asDps(firearm.fireRate, firearm.armourDamage)}
</Typography.Text>
<Typography.Text>
<strong>每秒肉伤:</strong>
{asDps(firearm.fireRate, firearm.bodyDamage)}
</Typography.Text>
<Typography.Paragraph
style={{ marginBottom: 0 }}
type="secondary"
ellipsis={{
rows: 3,
tooltip: firearm.review
? {
title: <div style={{ whiteSpace: "pre-line" }}>{firearm.review}</div>,
placement: "topLeft",
}
: false,
}}
className="whitespace-pre-line">
{firearm.review || "暂无描述"}
</Typography.Paragraph>
</div>
</Card>
</Col>
))}
{firearms.length === 0 && (
<Col span={24}>
<Card>
<Typography.Text type="secondary">暂无武器数据</Typography.Text>
</Card>
</Col>
)}
</Row>
</div>
<div className="flex justify-end">
<Pagination
align="end"
current={page}
pageSize={12}
total={total}
onChange={(nextPage) => {
setPage(nextPage)
}}
showSizeChanger={false}
/>
</div>
<FirearmCreateModal
open={createModalOpen}
onCancel={() => setCreateModalOpen(false)}
onSuccess={() => {
setCreateModalOpen(false)
void loadFirearms()
}}
/>
<FirearmEditModal
open={!!editingFirearm}
firearm={editingFirearm}
onCancel={() => setEditingFirearm(null)}
onSuccess={() => {
setEditingFirearm(null)
void loadFirearms()
}}
/>
</>
)
}
+34
View File
@@ -0,0 +1,34 @@
import { Tabs } from "antd"
import { useSearchParams } from "react-router-dom"
import MarkdownRenderer from "@/components/markdown-renderer"
import { html as EulaHtml } from "@/docs/EULA.md"
import { html as PrivacyHtml } from "@/docs/PrivacyPolicy.md"
const tabKeys = new Set(["eula", "privacy"])
export default function LegalPage() {
const [searchParams, setSearchParams] = useSearchParams()
const rawTab = searchParams.get("tab")
const activeTab = rawTab && tabKeys.has(rawTab) ? rawTab : "eula"
return (
<div className="mx-auto max-w-4xl">
<Tabs
activeKey={activeTab}
onChange={(key) => setSearchParams({ tab: key })}
items={[
{
key: "eula",
label: "最终用户许可协议",
children: <MarkdownRenderer html={EulaHtml} />,
},
{
key: "privacy",
label: "隐私政策",
children: <MarkdownRenderer html={PrivacyHtml} />,
},
]}
/>
</div>
)
}
+67
View File
@@ -0,0 +1,67 @@
import { useState } from "react"
import { useNavigate } from "react-router-dom"
import { App, Button, Card, Form, Input, Typography } from "antd"
import { AuthApi } from "@/api"
import { useAppDispatch } from "@/store/hooks"
import { setCurrentUser } from "@/store/auth-slice"
import { LoginRequest } from "@/types"
export default function LoginPage() {
const navigate = useNavigate()
const { message } = App.useApp()
const dispatch = useAppDispatch()
const [loading, setLoading] = useState(false)
async function onFinish(values: LoginRequest) {
setLoading(true)
try {
const user = await AuthApi.login(values)
dispatch(setCurrentUser(user))
message.success(`欢迎回来,${user.username}`)
navigate("/firearms")
} catch {
message.error("登录失败,请检查帐号或密码")
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-gray-100 px-4 py-10 sm:py-16">
<div className="mx-auto max-w-md">
<Card bordered={false} className="shadow-sm">
<Typography.Title level={3} className="!mb-2 text-center">
登录
</Typography.Title>
<Typography.Paragraph className="!mb-6 text-center !text-gray-500">
使用你的帐号登录后即可继续操作
</Typography.Paragraph>
<Form<LoginRequest> layout="vertical" onFinish={onFinish} requiredMark={false}>
<Form.Item<LoginRequest>
name="principle"
label="帐号"
rules={[{ required: true, message: "请输入帐号" }]}
>
<Input autoComplete="username" placeholder="请输入帐号" />
</Form.Item>
<Form.Item<LoginRequest>
name="credential"
label="密码"
rules={[{ required: true, message: "请输入密码" }]}
>
<Input.Password autoComplete="current-password" placeholder="请输入密码" />
</Form.Item>
<Form.Item className="!mb-0">
<Button type="primary" htmlType="submit" loading={loading} block>
登录
</Button>
</Form.Item>
</Form>
</Card>
</div>
</div>
)
}
+313
View File
@@ -0,0 +1,313 @@
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 { ModificationApi, TagApi } from "@/api"
import ModificationCreateModal from "@/components/modification-create-modal"
import ModificationEditModal from "@/components/modification-edit-modal"
import { useAppSelector } from "@/store/hooks"
import { Modification } from "@/types"
const pageSize = 10
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<number>(1)
const [modifications, setModifications] = useState<Modification[]>([])
const [tagOptions, setTagOptions] = useState<string[]>([])
const [selectedTags, setSelectedTags] = useState<string[]>([])
const [total, setTotal] = useState<number>(0)
const [deletingId, setDeletingId] = useState<number | null>(null)
const [createModalOpen, setCreateModalOpen] = useState(false)
const [editingModification, setEditingModification] = useState<Modification | null>(null)
useEffect(() => {
const _firearmId = firearmId ? +firearmId : void 0
TagApi.getTags(_firearmId).then((tags) => {
setTagOptions(tags)
})
}, [firearmId])
const loadModifications = useCallback(async () => {
return ModificationApi.getModifications({
page: page - 1,
size: pageSize,
sortBy: "id",
direction: "DESC",
firearmId,
tags: selectedTags,
}).then((pagedData) => {
setModifications(pagedData.items)
setTotal(pagedData.totalElements)
})
}, [page, firearmId, selectedTags])
useEffect(() => {
void 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 {
void loadModifications()
}
} catch {
message.error("改枪码删除失败,请稍后重试")
} finally {
setDeletingId(null)
}
}
useEffect(() => {
setPage(1)
}, [firearmId])
useEffect(() => {
setPage(1)
}, [selectedTags])
return (
<>
<div className="mb-4 flex items-start justify-between gap-4">
<Typography.Title level={4} className="mb-0!">
改枪码列表
</Typography.Title>
<div className="flex flex-wrap items-center justify-end gap-3">
<Space wrap>
<span>标签:</span>
<Select<string[]>
mode="multiple"
allowClear
placeholder="请选择标签"
className="w-64"
value={selectedTags}
options={tagOptions.map((tag) => ({ value: tag, label: tag }))}
onChange={(values) => {
setSelectedTags(values)
}}
/>
{firearmId && <Tag color="geekblue">武器 ID: {firearmId}</Tag>}
{(firearmId || selectedTags.length > 0) && (
<Link to="/mod-codes">
<Button
type="link"
onClick={() => {
setSelectedTags([])
setPage(1)
}}>
清除筛选
</Button>
</Link>
)}
</Space>
{user && (
<Button type="primary" onClick={() => setCreateModalOpen(true)}>
添加改装
</Button>
)}
</div>
</div>
<div className="mb-6">
<Row gutter={[16, 16]}>
{modifications.map((modification) => (
<Col key={modification.id} span={24}>
<Card
title={modification.name}
extra={
user ? (
<div className="flex items-center gap-1">
<Button
type="link"
size="small"
onClick={() => setEditingModification(modification)}>
编辑
</Button>
<Popconfirm
title="确认删除改枪码"
description={`确定要删除 ${modification.name} 吗?该操作不可撤销。`}
okText="删除"
cancelText="取消"
okButtonProps={{ danger: true, loading: deletingId === modification.id }}
onConfirm={() => handleDelete(modification)}>
<Button
type="link"
danger
size="small"
loading={deletingId === modification.id}>
删除
</Button>
</Popconfirm>
</div>
) : null
}
variant="outlined"
styles={{
header: { minHeight: 56 },
}}>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-2">
<span>
<strong>改枪码:</strong>
<code className="bg-gray-400 px-2 py-1 rounded text-sm text-white">
{modification.code}
</code>
</span>
<Button
type="text"
size="small"
onClick={() => navigator.clipboard.writeText(modification.code)}>
复制
</Button>
</div>
<Typography.Text>
<strong>作者:</strong>
{modification.author || "未知"}
</Typography.Text>
{(modification.tags?.length || 0) > 0 && (
<div className="flex flex-wrap gap-2">
{(modification.tags || []).map((tag) => (
<Tag key={`${modification.id}-${tag}`}>{tag}</Tag>
))}
</div>
)}
<div>
<Typography.Text strong>配件配置:</Typography.Text>
{(modification.accessories?.length || 0) > 0 ? (
<div className="mt-2 overflow-x-auto">
<div className="grid min-w-275 grid-cols-5 gap-2">
{(modification.accessories || []).map((accessory, accessoryIndex) => (
<div
key={`${modification.id}-accessory-${accessoryIndex}`}
className="rounded border border-gray-100 p-2">
<div className="flex items-center justify-between gap-2 rounded bg-gray-50 px-2 py-1">
<Typography color="blue" className="mr-0">
{accessory.slotName || "未填写槽位"}
</Typography>
<Typography className="mr-0 text-[#4C1D95]">
{accessory.accessoryName || "未填写配件"}
</Typography>
</div>
{(accessory.tunings?.length || 0) > 0 ? (
<div className="mt-2 flex flex-wrap gap-1">
{accessory.tunings.map((tuning, tuningIndex) => (
<Tag
key={`${modification.id}-${accessoryIndex}-tuning-${tuningIndex}`}
color="geekblue">
{tuning.tuningName || "未命名"}: {tuning.tuningValue ?? "-"}
</Tag>
))}
</div>
) : null}
</div>
))}
</div>
</div>
) : (
<Typography.Text type="secondary" className="block mt-1">
暂无配件信息
</Typography.Text>
)}
</div>
<Typography.Paragraph
style={{ marginBottom: 0 }}
type="secondary"
ellipsis={{ rows: 3 }}>
{modification.note || "暂无备注"}
</Typography.Paragraph>
{modification.videoUrl && (
<div>
<a href={modification.videoUrl} target="_blank" rel="noopener noreferrer">
查看视频
</a>
</div>
)}
</div>
</Card>
</Col>
))}
{modifications.length === 0 && (
<Col span={24}>
<Card>
<Typography.Text type="secondary">暂无改枪码数据</Typography.Text>
</Card>
</Col>
)}
</Row>
</div>
<div className="flex justify-end">
<Pagination
align="end"
current={page}
pageSize={pageSize}
total={total}
onChange={(nextPage) => {
setPage(nextPage)
}}
showSizeChanger={false}
/>
</div>
<ModificationCreateModal
open={createModalOpen}
defaultFirearmId={parsedFirearmId}
lockedFirearmId={parsedFirearmId}
onCancel={() => setCreateModalOpen(false)}
onSuccess={() => {
setCreateModalOpen(false)
void loadModifications()
}}
/>
<ModificationEditModal
open={!!editingModification}
modification={editingModification}
lockedFirearmId={parsedFirearmId}
onCancel={() => setEditingModification(null)}
onSuccess={() => {
setEditingModification(null)
void loadModifications()
}}
/>
</>
)
}
+66
View File
@@ -0,0 +1,66 @@
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<T extends { default: ComponentType<unknown> }>(importer: () => Promise<T>) {
return async () => {
const module = await importer()
return {
Component: module.default,
}
}
}
const hydrateFallbackElement = <div className="px-4 py-6 text-gray-500">页面加载中...</div>
/**
* Main application router configuration using React Router v6.
* Defines all routes and their corresponding components.
*/
const router = createBrowserRouter(
[
{
path: "/",
element: <HeroLayout />,
hydrateFallbackElement,
errorElement: <ErrorPage />,
children: [
{
index: true,
lazy: lazy(() => import("@/page/firearms")),
},
{
path: "firearms",
lazy: lazy(() => import("@/page/firearms")),
},
{
path: "mod-codes",
lazy: lazy(() => import("@/page/mod-codes")),
},
{
path: "legal",
lazy: lazy(() => import("@/page/legal"))
}
],
},
{
element: <EmptyLayout />,
hydrateFallbackElement,
errorElement: <ErrorPage />,
children: [
{
path: "login",
lazy: lazy(() => import("@/page/login")),
},
],
},
],
{
basename: "/",
}
)
export default router
+6
View File
@@ -0,0 +1,6 @@
import dayjs from "dayjs"
import duration from "dayjs/plugin/duration"
dayjs.extend(duration)
export default dayjs
+10
View File
@@ -0,0 +1,10 @@
import axios from "axios"
import dayjs from "@/shared/dayjs"
const WebClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: dayjs.duration({ seconds: 10 }).asMilliseconds(),
withCredentials: true
})
export { WebClient }
+26
View File
@@ -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<User>) {
state.user = action.payload
},
clearCurrentUser(state) {
state.user = null
},
},
})
export const { setCurrentUser, clearCurrentUser } = authSlice.actions
export const authReducer = authSlice.reducer
+42
View File
@@ -0,0 +1,42 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
import { Firearm, Page } from "@/types"
interface FirearmsState {
items: Firearm[]
page: number
size: number
totalPages: number
totalElements: number
}
const initialState: FirearmsState = {
items: [],
page: 0,
size: 12,
totalPages: 0,
totalElements: 0,
}
const firearmsSlice = createSlice({
name: "firearms",
initialState,
reducers: {
setFirearmsPage(state, action: PayloadAction<Page<Firearm>>) {
state.items = action.payload.items
state.page = action.payload.page
state.size = action.payload.size
state.totalPages = action.payload.totalPages
state.totalElements = action.payload.totalElements
},
clearFirearms(state) {
state.items = []
state.page = 0
state.size = 12
state.totalPages = 0
state.totalElements = 0
},
},
})
export const { setFirearmsPage, clearFirearms } = firearmsSlice.actions
export const firearmsReducer = firearmsSlice.reducer
+6
View File
@@ -0,0 +1,6 @@
import { useDispatch, useSelector } from "react-redux"
import type { AppDispatch, RootState } from "./index"
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
+47
View File
@@ -0,0 +1,47 @@
import { configureStore, combineReducers } from "@reduxjs/toolkit"
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
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")
const persistConfig = {
key: "root",
storage,
whitelist: ["auth", "firearms"],
}
const rootReducer = combineReducers({
auth: authReducer,
firearms: firearmsReducer
})
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
})
export const persistor = persistStore(store)
export default store
export type RootState = ReturnType<typeof rootReducer>
export type AppDispatch = typeof store.dispatch
export type AppStore = typeof store
+11
View File
@@ -0,0 +1,11 @@
export interface LoginRequest {
principle: string
credential: string
}
export interface User {
id: number
username: string
email: string
}
+17
View File
@@ -0,0 +1,17 @@
export type Direction = "ASC" | "DESC"
export interface Page<T> {
items: T[]
page: number
size: number
totalElements: number
totalPages: number
}
export interface PageQueryParams {
page?: number
size?: number
sortBy?: string
direction?: Direction
}
+24
View File
@@ -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<Firearm, "id"> {}
+4
View File
@@ -0,0 +1,4 @@
export * from "./common"
export * from "./firearm"
export * from "./modification"
export * from "./auth"
+24
View File
@@ -0,0 +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,
accessories: Accessory[]
}
export interface ModificationRequest extends Omit<Modification, "id"> {}
+1
View File
@@ -0,0 +1 @@
export * as QueryParamUtils from "./query-param-utils"
+23
View File
@@ -0,0 +1,23 @@
import { PageQueryParams } from "@/types"
export function asUrlSearchParam(pageQueryParams?: PageQueryParams) {
const urlSearchParams = new URLSearchParams()
if (pageQueryParams?.page) {
urlSearchParams.append("page", "" + pageQueryParams.page)
}
if (pageQueryParams?.size) {
urlSearchParams.append("size", "" + pageQueryParams.size)
}
if (pageQueryParams?.sortBy) {
urlSearchParams.append("sortBy", pageQueryParams.sortBy)
}
if (pageQueryParams?.direction) {
urlSearchParams.append("direction", pageQueryParams.direction)
}
return urlSearchParams
}
+17
View File
@@ -0,0 +1,17 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
/**
* Redux persistent storage location, use `local` for local storage and `session` for
* session storage
*/
readonly VITE_REDUX_STORAGE: "local" | "session"
/**
* Backend API Base URL
*/
readonly VITE_API_BASE_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
+10
View File
@@ -0,0 +1,10 @@
import type { Config } from "tailwindcss"
import typography from "@tailwindcss/typography"
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [typography],
} satisfies Config
+31
View File
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more