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
161 changed files with 7870 additions and 4424 deletions

No files matched your search

+2 -6
View File
@@ -1,6 +1,2 @@
delta-force-guide-server.iml
build/
!.gitlab-ci.yml
!build/libs/*.jar
.idea/
.gradle
node_modules/
README.md
+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"
-86
View File
@@ -1,86 +0,0 @@
name: Build and Deploy
on:
release:
types: [published]
env:
APP_NAME: delta-force-guide-server
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ================================================================
# Single Job: Build, Upload JAR to Release, and Push to GHCR
# ================================================================
build-and-release:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21 (Corretto)
uses: actions/setup-java@v4
with:
java-version: 21
distribution: corretto
cache: gradle
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
# 使用 Release Tag 做为 Gradle 属性传入
- name: Build with Gradle
run: ./gradlew bootJar -x test -PartefactVersion="${{ github.event.release.tag_name }}"
- name: Resolve JAR file path
id: jar
run: |
JAR_PATH=$(find build/libs -name '*.jar' | head -1)
echo "file=$JAR_PATH" >> "$GITHUB_OUTPUT"
# 上传 JAR 包到 GitHub Release 中
- name: Upload JAR to GitHub Release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ steps.jar.outputs.file }}
asset_name: ${{ github.event.repository.name }}-${{ github.event.release.tag_name }}.jar
tag: ${{ github.event.release.tag_name }}
overwrite: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# 登录到 GitHub Container Registry (GHCR)
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 镜像打标签准备
- name: Generate image tags
id: meta
run: |
OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
REPO_LC=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]')
echo "tag_version=ghcr.io/$OWNER_LC/$REPO_LC:${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
echo "tag_latest=ghcr.io/$OWNER_LC/$REPO_LC:latest" >> "$GITHUB_OUTPUT"
# 构建并上传镜像到 GHCR
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.ci
build-args: JAR_FILE=${{ steps.jar.outputs.file }}
push: true
tags: |
${{ steps.meta.outputs.tag_version }}
${{ steps.meta.outputs.tag_latest }}
cache-from: type=gha
cache-to: type=gha,mode=max
+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 }}
+159 -30
View File
@@ -97,7 +97,30 @@ fabric.properties
# Editor-based HTTP Client
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
*.class
@@ -111,49 +134,155 @@ http-client.private.env.json
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
*.jar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
# Config
config/*.yml
config/*.yaml
config/*.properties
### Node.js
# Logs
logs
#*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Tests
test/
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
### Gradle
.gradle
**/build/
!**/src/**/build/
# Runtime data
pids
*.pid
*.seed
*.pid.lock
!gradle/
!gradle/wrapper
!gradle/wrapper/gradle-wrapper.jar
!gradle/wrapper/gradle-wrapper.properties
gradle-app.setting
.gradletasknamecache
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
### Claude Code
.claude/settings.local.json
.claude/memory/
.claude/plans/
.claude/worktrees/
.claude/scheduled_tasks.json
# Coverage directory used by tools like istanbul
coverage
*.lcov
# Eclipse Gradle plugin generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
# nyc test coverage
.nyc_output
gradle.properties
# 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-*
-34
View File
@@ -1,34 +0,0 @@
variables:
GRADLE_OPTS: -Dorg.gradle.daemon=false
DOCKER_HOST: unix:///var/run/docker.sock
stages:
- release
release:
stage: release
image: amazoncorretto:21-alpine
cache:
key: gradle
paths:
- .gradle/wrapper
- .gradle/caches
before_script:
- chmod +x gradlew
- apk add --no-cache docker-cli
script:
- ./gradlew bootJar -x test -PartefactVersion="$CI_COMMIT_TAG"
- JAR_FILE=$(find build/libs -name '*.jar' | head -1)
- echo "Building Docker image for tag $CI_COMMIT_TAG with JAR $JAR_FILE"
- docker build
-f Dockerfile.ci
--build-arg JAR_FILE="$JAR_FILE"
-t "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
.
- docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG" "$CI_REGISTRY_IMAGE:latest"
- echo "Pushing image $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
- docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
- docker push "$CI_REGISTRY_IMAGE:latest"
rules:
- if: $CI_COMMIT_TAG
+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}"
}
]
}
-109
View File
@@ -1,109 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Coding Standards
- **Style**: Follow the Google Java Coding Style as the foundation.
- **Indentation**: Use 4 spaces — no tabs.
- **Line length**: Maximum 100 characters per line.
- **Comments**: All code comments must use British English spelling (e.g. "colour" not "color", "behaviour" not "behavior", "serialise" not "serialize", "analyse" not "analyze", "traveller" not "traveler").
## Build & Test Commands
```bash
# Build the project (skip tests)
./gradlew build -x test
# Run all tests
./gradlew test
# Run a single test class
./gradlew test --tests "com.onixbyte.deltaforceguide.service.PasswordEncoderTest"
# Run a specific test method
./gradlew test --tests "com.onixbyte.deltaforceguide.service.PasswordEncoderTest.generatePassword"
# Build the full JAR
./gradlew bootJar
```
The project uses Gradle with Java 21 (Amazon Corretto). Tests use JUnit 5 with the Spring Boot test framework, H2 in-memory database for test runtime, and Spring Security test support. Tests require an active `dev` profile.
## Code Architecture
**Delta Force Guide Server** — A REST API backend for managing Delta Force game firearm builds/modifications.
### Package structure
```
com.onixbyte.deltaforceguide
├── client/ # External service clients (TokenClient for JWT)
├── config/ # Spring beans: Security, CORS, Cache/Redis, Jackson, MyBatis, Spring Data
├── controller/ # REST controllers (Firearm, Modification, Tag, Auth)
├── domain/
│ ├── converter/ # JPA attribute converters (FirearmTypeConverter)
│ ├── dto/ # Request/response records (FirearmRequest, ModificationResponse, etc.)
│ └── entity/ # JPA entities (Firearm, Modification, User, Accessory, Tuning)
├── enumeration/ # Enums (FirearmType)
├── exeption/ # BizException (custom runtime exception with HTTP status)
├── filter/ # TokenAuthenticationFilter (JWT auth via OncePerRequestFilter)
├── manager/ # Thin @Transactional wrappers around repositories
├── mapper/ # MyBatis mappers (configured but currently unused)
├── properties/ # @ConfigurationProperties records (Cors, Token, Cookie)
├── repository/ # Spring Data JPA repositories
├── security/
│ ├── authentication/ # Custom UsernamePasswordAuthentication impl
│ └── provider/ # UsernamePasswordAuthenticationProvider
├── service/ # Business logic layer (FirearmService, ModificationService, AuthService, etc.)
├── shared/ # Constants and utility classes (CookieName, CredentialProvider, JacksonModules)
└── utils/ # Helpers (DateTimeUtil)
```
### Key design decisions
- **JPA + native queries**: Most CRUD uses Spring Data JPA. Native queries (in `ModificationRepository`) handle JSONB tag filtering with Postgres `@>` operator.
- **Custom auth flow**: JWT tokens in httpOnly cookies (`AccessToken`). Spring Security with a custom `UsernamePasswordAuthenticationProvider` and `TokenAuthenticationFilter`. Tokens are auto-renewed within 5 min of expiry.
- **JSONB storage**: `Modification.tags` and `Modification.accessories` (including nested `Tuning` objects) are stored as JSONB columns using Hypersistence Utils `JsonType`.
- **Strict layering**: The call chain must follow `Controller → Service → Manager → Repository/Mapper`. Skipping layers (e.g. Controller calling Manager directly, Service calling Repository directly) is not permitted. Each layer has a distinct responsibility: Controller handles HTTP concerns, Service contains business logic, Manager manages `@Transactional` boundaries and data access coordination, Repository/Mapper handles raw data access.
- **DTOs as Java records**: All request/response objects are immutable records with static `from()` factory methods for entity→DTO conversion.
- **Flyway migrations**: SQL migrations in `src/main/resources/db/migration/` — V2 (init), V3 (bullet/damage fields), V4 (user), V5 (accessories JSONB column).
### Data model
- `firearm` table: id, name, type (int→FirearmType enum), level, calibre, fire_rate, armour_damage, body_damage, review
- `modification` table: id, firearm_id (FK→firearm), name, code, tags (jsonb), accessories (jsonb), note, author, video_url
- `app_user` table: id, username, email
- `user_credential` table: user_id, provider, credential (hashed)
### API endpoints
| Path | Methods | Auth |
|-------------------------------|------------------|--------------------------------------|
| `/firearms` | GET, POST | GET public, POST requires auth |
| `/firearms/{id}` | GET, PUT, DELETE | GET public, PUT/DELETE requires auth |
| `/modifications` | GET, POST | GET public, POST requires auth |
| `/modifications/{id}` | GET, PUT, DELETE | GET public, PUT/DELETE requires auth |
| `/modifications/batch` | POST | Requires auth |
| `/modifications/batch-delete` | DELETE | Requires auth |
| `/tags` | GET | Public |
| `/auth/login` | POST | Public |
| `/auth/logout` | POST | Authenticated |
### Commit convention
Conventional commits: `feat:`, `chore:`, `fix:`. Messages are in English, present tense imperative style.
### External dependencies
- **DB**: PostgreSQL (via Flyway migrations), H2 in test
- **Cache**: Redis (via Spring Cache + RedisTemplate with GenericJackson2JsonRedisSerializer)
- **Auth**: java-jwt (auth0), BCrypt
- **Docs**: springdoc-openapi (Swagger UI) on dev profile
- **Onixbyte internal libs**: version-catalogue, tuple, common-toolbox, math-toolbox, identity-generator, captcha, regions
- **AWS**: S3 SDK
### Profiles
- `dev`: Enables Swagger UI, connects to dev DB/Redis at `dfguide.onixbyte.cn`. Config in `config/application-dev.yaml`.
- Default profile: Used for production, no Swagger, connects to production datasource.
+7 -8
View File
@@ -1,10 +1,9 @@
FROM amazoncorretto:21 AS builder
FROM node:20-alpine as builder
WORKDIR /app
ADD . .
RUN chmod +x ./gradlew && \
./gradlew build -x test
COPY . .
RUN corepack enable pnpm
RUN pnpm install
RUN pnpm build
FROM amazoncorretto:21-alpine AS runner
WORKDIR /app
COPY --from=builder /app/build/libs/delta-force-guide-server-1.0.0.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
FROM nginx:1.27 as runner
COPY --from=builder /app/dist/ /usr/share/nginx/html/
-7
View File
@@ -1,7 +0,0 @@
FROM amazoncorretto:21-alpine
WORKDIR /app
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT Licence
Copyright (c) 2026 OnixByte
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
+56 -112
View File
@@ -1,150 +1,94 @@
# Delta Force Guide Server
# Delta Force Firearm Modification Codes
REST API backend for managing **Delta Force** game firearm builds and modifications. Provides endpoints to browse, create, and share weapon configurations including attachments, tuning setups, tags, and build reviews.
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
| Layer | Technology |
|-----------|------------------------------------------------------------|
| Language | Java 21 (Amazon Corretto) |
| Framework | Spring Boot 3.x (Web, Security, Data JPA, Cache, Actuator) |
| Build | Gradle (Kotlin DSL) |
| Database | PostgreSQL (Flyway migrations, JSONB columns) |
| Cache | Redis (2-hour TTL) |
| Auth | Custom JWT via httpOnly cookies + BCrypt |
| API Docs | springdoc-openapi (Swagger UI, dev profile only) |
| Container | Multi-stage Docker image (Amazon Corretto 21 → Alpine) |
| CI/CD | GitHub Actions — builds on release publish |
- Vite
- React 19
- TypeScript
- Tailwind CSS 4
- React Router 7
- @tanstack/react-virtual
- Day.js
## Quick Start
## Getting Started
### Prerequisites
- JDK 21 (Amazon Corretto recommended)
- PostgreSQL
- Redis
- Node.js 20 or later is recommended.
- pnpm is required for dependency management and scripts.
### Configure
Copy and customise the development config:
### Install dependencies
```bash
cp config/application-prod.yaml.example \
config/application-dev.yaml
pnpm install
```
Fill in your datasource and Redis connection details, then place the production config at `config/`, then enable it by environment variable `SPRING_PROFILES_ACTIVE`.
### Build
### Start the development server
```bash
# Compile (skip tests)
./gradlew build -x test
# Run all tests
./gradlew test
# Build executable JAR
./gradlew bootJar
pnpm dev
```
### Run
### Build for production
```bash
SPRING_PROFILES_ACTIVE=prod java -jar build/libs/delta-force-guide-server-$version.jar
pnpm build
```
Swagger UI is available at `http://localhost:8080/swagger-ui.html` when the `dev` profile is active.
## Data Model
- **Firearm** — weapon base stats (name, type, level, calibre, fire rate, armour/body damage, review)
- **Modification** — a build attached to a firearm, with name, code, tags (JSONB), accessories including nested tuning objects (JSONB), author notes, and video links
- **App User** — registered user (username, email) with hashed credentials via BCrypt
Tags and accessories are stored as PostgreSQL JSONB columns using [Hypersistence Utils](https://github.com/vladmihalcea/hypersistence-utils), enabling flexible per-build metadata and filtering with the `@>` operator.
## Architecture
```
Controller → Service → Manager → Repository / Mapper
(HTTP) (logic) (@Transactional) (data access)
```
The call chain is strictly enforced — skipping layers is not permitted. All request/response objects are Java records with static `from()` factory methods for entity-to-DTO conversion.
## Docker
### Preview the production build locally
```bash
# Build image
docker pull registry.onixbyte.cn/onixbyte/delta-force-guide-server:latest
# Run container
docker run -p 8080:8080 \
-v /path/to/config:/app/config \
-e SPRING_PROFILES_ACTIVE=$your_active_profiles
delta-force-guide-server
pnpm preview
```
Pre-built images are published to **Self-hosted GitLab Container Registry** (`registry.onixbyte.cn/onixbyte/delta-force-guide-server`) on every release.
## CI/CD
GitLab CI triggers on **tags**. The pipeline:
1. Builds the boot JAR with the release tag as the version
2. Uploads the JAR as a release asset
3. Builds and pushes a multi-arch Docker image to GHCR tagged with both `:latest` and `:<version>`
No tests are run in CI by design — tests are expected to pass locally before a release is cut.
## Profiles
| Profile | Purpose |
|---------|-----------------------------------------------------------------------|
| `dev` | Enables Swagger UI, connects to dev DB/Redis at `dfguide.onixbyte.cn` |
| default | Production mode, no Swagger, uses production datasource |
## 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
```
src/main/java/com/onixbyte/deltaforceguide/
├── client/ External service HTTP clients
├── config/ Spring bean definitions (Security, CORS, Cache, Jackson, MyBatis)
├── controller/ REST controllers
├── domain/
│ ├── converter/ JPA attribute converters
│ ├── dto/ Request/response records
│ └── entity/ JPA entities
├── enumeration/ Enums (FirearmType)
├── exception/ Custom BizException with HTTP status mapping
├── filter/ TokenAuthenticationFilter (JWT through OncePerRequestFilter)
├── manager/ @Transactional wrappers around repositories
├── mapper/ MyBatis mapper interfaces (reserved for future use)
├── properties/ @ConfigurationProperties records
├── repository/ Spring Data JPA repositories
├── security/
│ ├── authentication/ Custom UsernamePasswordAuthentication
│ └── provider/ UsernamePasswordAuthenticationProvider
├── service/ Business logic
├── shared/ Constants and utility classes
└── utils/ General-purpose helpers
```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`.
## Versioning
## Deployment
The application exposes its version at `/versions` (GET).
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 licensed under the [MIT Licence](LICENCE). Copyright &copy; 2026 OnixByte.
This project is released under the MIT Licence. See `LICENCE` for details.
-65
View File
@@ -1,65 +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(libs.spring.boot.starter.doc)
implementation(libs.spring.boot.starter.security)
testImplementation(libs.spring.boot.starter.test)
testImplementation(libs.reactor.test)
testImplementation(libs.mybatis.starter.test)
testImplementation(libs.spring.security.test)
runtimeOnly(libs.postgres.driver)
testRuntimeOnly(libs.h2.database)
testRuntimeOnly(libs.junit.launcher)
}
tasks.test {
useJUnitPlatform()
}
-55
View File
@@ -1,55 +0,0 @@
spring:
datasource:
url: jdbc:postgresql://localhost:5432/dfguide_dev
username: postgres
password: 123456
driver-class-name: org.postgresql.Driver
data:
redis:
host: localhost
port: 6379
database: 0
# password: 6hLFVqfGPviTYukn # Uncomment if password is necessary
logging:
pattern:
# dateformat: dd MMM yyyy HH:mm:ss.SSS # Modify this for custom date format.
app:
common:
version: 1.3.0.8-dev # Application version, you can change to any version you like, used for communication with frontend.
cors:
allowed-origins: # Cross-origin allowed origins
- "http://localhost:5173" # Dev server for vite.
- "http://localhost:4173" # Preview server for vite.
allow-credentials: true # Must be set to `true` since we are using cookie. You can change it only when you have modified how this application executing authentication.
allow-private-network: false
allowed-headers:
- "Content-Type"
- "Authorization"
allowed-methods:
- GET
- POST
- PUT
- PATCH
- DELETE
exposed-headers:
- "Content-Type"
- "Authorization"
max-age: PT2H
jwt:
issuer: dfguide.local # Issuer host
secret: qwertyuiopasdfghjklzxcvbnm123456 # JWT singing secret, a 32-byte long or longer string is recommended
valid-time: PT2H # JWT valid duration
cookie: # Cookie settings.
http-only: true
secure: false
same-site: lax
path: '/'
max-age: PT2H
springdoc:
api-docs:
enabled: true # Set to `false` if you do not need api docs (recommended in production mode).
swagger-ui:
enabled: true # Set to `false` if you do not need swagger ui (recommended in production mode).
-80
View File
@@ -1,80 +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"
springDocVersion = "2.8.16"
[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" }
jwt-core = { group = "com.auth0", name = "java-jwt", version.ref = "javaJwtVersion" }
# Spring Doc
spring-boot-starter-doc = { group = "org.springdoc", name = "springdoc-openapi-starter-webmvc-ui", version.ref = "springDocVersion" }
# 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" }
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,70 +0,0 @@
package com.onixbyte.deltaforceguide.client;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.onixbyte.deltaforceguide.domain.entity.User;
import com.onixbyte.deltaforceguide.properties.TokenProperties;
import com.onixbyte.deltaforceguide.utils.DateTimeUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class TokenClient {
private final Algorithm algorithm;
private final TokenProperties tokenProperties;
private final JWTVerifier verifier;
/**
* Constructs a new TokenClient with the necessary algorithm and token properties.
*
* @param algorithm the signing algorithm used to secure the JWT
* @param tokenProperties the configuration properties for the token, such as issuer and
* validity period
*/
@Autowired
public TokenClient(
Algorithm algorithm,
TokenProperties tokenProperties,
JWTVerifier verifier
) {
this.algorithm = algorithm;
this.tokenProperties = tokenProperties;
this.verifier = verifier;
}
/**
* Generate a JSON Web Token to the current user.
*
* @param user the current user for whom the token is being generated
* @return a JWT string
*/
public String generateToken(User user) {
var issuedAt = LocalDateTime.now();
var expiresAt = issuedAt.plus(tokenProperties.validTime());
return JWT.create()
.withSubject(user.getUsername())
.withIssuer(tokenProperties.issuer())
.withIssuedAt(DateTimeUtil.asInstant(issuedAt))
.withExpiresAt(DateTimeUtil.asInstant(expiresAt))
.sign(algorithm);
}
/**
* Verify and decode token.
*
* @param token a JWT token
* @return information included in the given token
* @throws com.auth0.jwt.exceptions.JWTVerificationException if the token is invalid, such as
* expired, or not signed by
* specific server
*/
public DecodedJWT verifyToken(String token) {
return verifier.verify(token);
}
}
@@ -1,10 +0,0 @@
package com.onixbyte.deltaforceguide.config;
import com.onixbyte.deltaforceguide.properties.AppProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(AppProperties.class)
public class AppConfig {
}
@@ -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,38 +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.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
import java.util.stream.Stream;
@Configuration
@EnableConfigurationProperties({CorsProperties.class})
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource(
CorsProperties properties
) {
var corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(properties.allowCredentials());
corsConfiguration.setAllowedOrigins(List.of(properties.allowedOrigins()));
corsConfiguration.setAllowedHeaders(List.of(properties.allowedHeaders()));
corsConfiguration.setAllowedMethods(Stream.of(properties.allowedMethods())
.map(HttpMethod::name)
.toList());
corsConfiguration.setMaxAge(properties.maxAge());
corsConfiguration.setAllowPrivateNetwork(properties.allowPrivateNetwork());
corsConfiguration.setExposedHeaders(List.of(properties.exposedHeaders()));
var corsConfigurationSource = new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return corsConfigurationSource;
}
}
@@ -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,26 +0,0 @@
package com.onixbyte.deltaforceguide.config;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
import org.springframework.context.annotation.Configuration;
@OpenAPIDefinition(
info = @Info(
title = "Delta Force Guide Server",
description = "API for managing Delta Force game firearm builds",
version = "1.3.4",
contact = @Contact(
name = "Zihlu Wang",
email = "zihlu.wang@onixbyte.com"
),
license = @License(
name = "MIT",
url = "https://git.onixbyte.cn/onixbyte/delta-force-guide-server/-/raw/main/LICENCE"
)
)
)
@Configuration
public class OpenApiConfiguration {
}
@@ -1,76 +0,0 @@
package com.onixbyte.deltaforceguide.config;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.onixbyte.deltaforceguide.filter.TokenAuthenticationFilter;
import com.onixbyte.deltaforceguide.properties.CookieProperties;
import com.onixbyte.deltaforceguide.properties.TokenProperties;
import com.onixbyte.deltaforceguide.security.provider.UsernamePasswordAuthenticationProvider;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.web.cors.CorsConfigurationSource;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@EnableConfigurationProperties({TokenProperties.class, CookieProperties.class})
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity httpSecurity,
CorsConfigurationSource corsConfigurationSource,
TokenAuthenticationFilter tokenAuthenticationFilter
) throws Exception {
return httpSecurity
.cors((cors) -> cors
.configurationSource(corsConfigurationSource))
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement((customiser) -> customiser
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests((customiser) -> customiser
.anyRequest().permitAll()
)
.addFilterAfter(tokenAuthenticationFilter, ExceptionTranslationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider
) {
return new ProviderManager(
usernamePasswordAuthenticationProvider
);
}
@Bean
public Algorithm algorithm(TokenProperties properties) {
return Algorithm.HMAC256(properties.secret());
}
@Bean
public JWTVerifier verifier(Algorithm algorithm, TokenProperties tokenProperties) {
return JWT.require(algorithm)
.withIssuer(tokenProperties.issuer())
.build();
}
}
@@ -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,58 +0,0 @@
package com.onixbyte.deltaforceguide.controller;
import com.onixbyte.deltaforceguide.domain.dto.LoginRequest;
import com.onixbyte.deltaforceguide.domain.dto.UserResponse;
import com.onixbyte.deltaforceguide.client.TokenClient;
import com.onixbyte.deltaforceguide.security.annotation.RequiresAuth;
import com.onixbyte.deltaforceguide.service.AuthService;
import com.onixbyte.deltaforceguide.service.CookieService;
import com.onixbyte.deltaforceguide.shared.CookieName;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
@Tag(name = "用户鉴权", description = "处理用户登录与退出功能")
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
private final TokenClient tokenClient;
private final CookieService cookieService;
public AuthController(AuthService authService, TokenClient tokenClient, CookieService cookieService) {
this.authService = authService;
this.tokenClient = tokenClient;
this.cookieService = cookieService;
}
@Operation(description = "用户登录")
@PostMapping("/login")
public ResponseEntity<UserResponse> login(@Validated @RequestBody LoginRequest request) {
var user = authService.login(request);
var accessToken = tokenClient.generateToken(user);
var accessTokenCookie = cookieService.buildCookie(CookieName.ACCESS_TOKEN, accessToken);
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString())
.body(UserResponse.from(user));
}
@RequiresAuth
@Operation(description = "退出登录")
@PostMapping("/logout")
public ResponseEntity<Void> logout() {
var expiredCookie = cookieService.buildCookie(CookieName.ACCESS_TOKEN, "", Duration.ZERO);
return ResponseEntity.noContent()
.header(HttpHeaders.SET_COOKIE, expiredCookie.toString())
.build();
}
}
@@ -1,27 +0,0 @@
package com.onixbyte.deltaforceguide.controller;
import com.onixbyte.deltaforceguide.domain.dto.DailyPasswordResponse;
import com.onixbyte.deltaforceguide.service.DailyPasswordService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "每日密码", description = "获取每日密码信息")
@RestController
@RequestMapping("/daily-passwords")
public class DailyPasswordController {
private final DailyPasswordService dailyPasswordService;
public DailyPasswordController(DailyPasswordService dailyPasswordService) {
this.dailyPasswordService = dailyPasswordService;
}
@Operation(description = "获取当日的每日密码数据,该数据将被缓存一天")
@GetMapping
public DailyPasswordResponse getDailyPassword() {
return dailyPasswordService.getDailyPassword();
}
}
@@ -1,67 +0,0 @@
package com.onixbyte.deltaforceguide.controller;
import com.onixbyte.deltaforceguide.domain.dto.FirearmRequest;
import com.onixbyte.deltaforceguide.domain.dto.FirearmResponse;
import com.onixbyte.deltaforceguide.domain.dto.PageResponse;
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
import com.onixbyte.deltaforceguide.security.annotation.RequiresAuth;
import com.onixbyte.deltaforceguide.service.FirearmService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
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.*;
@Tag(name = "武器管理", description = "与武器有关的操作")
@RestController
@RequestMapping("/firearms")
public class FirearmController {
private final FirearmService firearmService;
public FirearmController(FirearmService firearmService) {
this.firearmService = firearmService;
}
@Operation(description = "获取分页武器数据")
@Validated
@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)));
}
@Operation(description = "获取指定武器的数据")
@GetMapping("/{id}")
public FirearmResponse queryById(@PathVariable Long id) {
return firearmService.queryById(id);
}
@RequiresAuth
@PostMapping
public FirearmResponse addFirearm(@Validated @RequestBody FirearmRequest request) {
return firearmService.addFirearm(request);
}
@RequiresAuth
@Operation(description = "更新指定武器的数据")
@PutMapping("/{id}")
public FirearmResponse updateFirearm(@PathVariable Long id, @Validated @RequestBody FirearmRequest request) {
return firearmService.updateFirearm(id, request);
}
@RequiresAuth
@Operation(description = "删除指定武器的数据")
@DeleteMapping("/{id}")
public void deleteFirearm(@PathVariable Long id) {
firearmService.deleteFirearm(id);
}
}
@@ -1,19 +0,0 @@
package com.onixbyte.deltaforceguide.controller;
import com.onixbyte.deltaforceguide.domain.dto.ErrorResponse;
import com.onixbyte.deltaforceguide.exeption.BizException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public ResponseEntity<ErrorResponse> handleBizException(BizException exception) {
var status = exception.getStatus();
return ResponseEntity.status(status)
.body(new ErrorResponse(exception.getMessage()));
}
}
@@ -1,96 +0,0 @@
package com.onixbyte.deltaforceguide.controller;
import com.onixbyte.deltaforceguide.domain.dto.ModificationBatchCreateRequest;
import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest;
import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse;
import com.onixbyte.deltaforceguide.domain.dto.PageResponse;
import com.onixbyte.deltaforceguide.security.annotation.RequiresAuth;
import com.onixbyte.deltaforceguide.service.ModificationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "改装管理", description = "对枪械改装的管理")
@RestController
@RequestMapping("/modifications")
public class ModificationController {
private final ModificationService modificationService;
public ModificationController(ModificationService modificationService) {
this.modificationService = modificationService;
}
@Operation(description = "分页查询改装信息")
@Validated
@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,
@RequestParam(required = false) List<String> tags
) {
return modificationService.pageQuery(firearmId, tags, PageRequest.of(page, size, Sort.by(direction, sortBy)));
}
@Operation(description = "查询指定改装的信息")
@GetMapping("/{id}")
public ModificationResponse queryById(@PathVariable Long id) {
return modificationService.queryById(id);
}
@RequiresAuth
@Operation(description = "创建改装")
@PostMapping
public ModificationResponse create(@Valid @RequestBody ModificationRequest request) {
return modificationService.create(request);
}
@RequiresAuth
@Operation(description = "批量创建改装")
@PostMapping("/batch")
public List<ModificationResponse> batchCreate(@Valid @RequestBody ModificationBatchCreateRequest request) {
return modificationService.batchCreate(request.modifications());
}
@RequiresAuth
@Operation(description = "修改指定改装")
@PutMapping("/{id}")
public ModificationResponse update(@PathVariable Long id, @Valid @RequestBody ModificationRequest request) {
return modificationService.update(id, request);
}
@RequiresAuth
@Operation(description = "删除指定改装")
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
modificationService.delete(id);
}
@RequiresAuth
@Operation(description = "批量删除改装")
@DeleteMapping("/batch-delete")
@Validated
public void batchDelete(@RequestParam List<@Positive Long> ids) {
modificationService.batchDelete(ids);
}
}
@@ -1,29 +0,0 @@
package com.onixbyte.deltaforceguide.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.onixbyte.deltaforceguide.service.ModificationService;
import java.util.List;
@Tag(name = "标签管理", description = "管理标签信息")
@RestController
@RequestMapping("/tags")
public class TagController {
private final ModificationService modificationService;
public TagController(ModificationService modificationService) {
this.modificationService = modificationService;
}
@Operation(description = "查询指定武器或所有武器的标签")
@GetMapping
public List<String> getTags(@RequestParam(required = false) Long firearmId) {
return modificationService.findAllTags(firearmId);
}
}
@@ -1,26 +0,0 @@
package com.onixbyte.deltaforceguide.controller;
import com.onixbyte.deltaforceguide.service.AppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "版本信息")
@RestController
@RequestMapping("/versions")
public class VersionController {
private final AppService appService;
public VersionController(AppService appService) {
this.appService = appService;
}
@Operation(description = "获取当前应用版本号")
@GetMapping
public String getVersion() {
return appService.getVersion();
}
}
@@ -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,20 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import java.util.ArrayList;
import java.util.List;
public record AccessoryRequest(
@NotBlank(message = "插槽名称不能为空")
String slotName,
@NotBlank(message = "配件名称不能为空")
String accessoryName,
List<@Valid TuningRequest> tunings
) {
public List<TuningRequest> tunings() {
return tunings == null ? new ArrayList<>() : tunings;
}
}
@@ -1,22 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import com.onixbyte.deltaforceguide.domain.entity.Accessory;
import java.util.List;
public record AccessoryResponse(
String slotName,
String accessoryName,
List<TuningResponse> tunings
) {
public static AccessoryResponse from(Accessory accessory) {
return new AccessoryResponse(
accessory.getSlotName(),
accessory.getAccessoryName(),
accessory.getTunings() == null
? List.of()
: accessory.getTunings().stream().map(TuningResponse::from).toList()
);
}
}
@@ -1,7 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
public record DailyPassword(
String mapName,
String password
) {
}
@@ -1,14 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import java.time.LocalDateTime;
import java.util.List;
public record DailyPasswordData(
String updateDate,
Integer totalCount,
List<DailyPassword> passwords,
String source,
LocalDateTime lastUpdated,
Long timestamp
) {
}
@@ -1,7 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
public record DailyPasswordMetadata(
String version,
String author
) {
}
@@ -1,9 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
public record DailyPasswordResponse(
String status,
String message,
DailyPasswordData data,
DailyPasswordMetadata metadata
) {
}
@@ -1,7 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
public record ErrorResponse(
String message
) {
}
@@ -1,15 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
public record FirearmRequest(
String name,
FirearmType type,
String level,
String calibre,
Integer fireRate,
Integer armourDamage,
Integer bodyDamage,
String review
) {
}
@@ -1,31 +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 calibre,
Integer fireRate,
Integer armourDamage,
Integer bodyDamage,
String review
) {
public static FirearmResponse from(Firearm firearm) {
return new FirearmResponse(
firearm.getId(),
firearm.getName(),
firearm.getType(),
firearm.getLevel(),
firearm.getCalibre(),
firearm.getFireRate(),
firearm.getArmourDamage(),
firearm.getBodyDamage(),
firearm.getReview()
);
}
}
@@ -1,15 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "登录请求")
public record LoginRequest(
@NotBlank(message = "登录名称不能为空")
@Schema(description = "用户名或电子邮箱", requiredMode = Schema.RequiredMode.REQUIRED)
String principle,
@NotBlank(message = "登录口令不能为空")
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED)
String credential
) {
}
@@ -1,13 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
public record ModificationBatchCreateRequest(
@NotEmpty(message = "批量创建列表不能为空")
List<@Valid ModificationRequest> modifications
) {
}
@@ -1,13 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Positive;
import java.util.List;
public record ModificationBatchDeleteRequest(
@NotEmpty(message = "批量删除ID列表不能为空")
List<@Positive(message = "ID必须为正数") Long> ids
) {
}
@@ -1,33 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import java.util.ArrayList;
import java.util.List;
public record ModificationRequest(
@NotNull(message = "武器ID不能为空")
@Positive(message = "武器ID必须为正数")
Long firearmId,
@NotBlank(message = "改装名称不能为空")
String name,
@NotBlank(message = "改装代码不能为空")
String code,
List<@NotBlank(message = "标签不能为空") String> tags,
List<@Valid AccessoryRequest> accessories,
String note,
String author,
String videoUrl
) {
public List<String> tags() {
return tags == null ? new ArrayList<>() : tags;
}
public List<AccessoryRequest> accessories() {
return accessories == null ? new ArrayList<>() : accessories;
}
}
@@ -1,34 +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,
List<AccessoryResponse> accessories,
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.getAccessories() == null
? List.of()
: modification.getAccessories().stream().map(AccessoryResponse::from).toList(),
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,13 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public record TuningRequest(
@NotBlank(message = "调校项名称不能为空")
String tuningName,
@NotNull(message = "调校值不能为空")
Double tuningValue
) {
}
@@ -1,16 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import com.onixbyte.deltaforceguide.domain.entity.Tuning;
public record TuningResponse(
String tuningName,
Double tuningValue
) {
public static TuningResponse from(Tuning tuning) {
return new TuningResponse(
tuning.getTuningName(),
tuning.getTuningValue()
);
}
}
@@ -1,17 +0,0 @@
package com.onixbyte.deltaforceguide.domain.dto;
import com.onixbyte.deltaforceguide.domain.entity.User;
public record UserResponse(
Long id,
String username,
String email
) {
public static UserResponse from(User user) {
return new UserResponse(
user.getId(),
user.getUsername(),
user.getEmail()
);
}
}
@@ -1,48 +0,0 @@
package com.onixbyte.deltaforceguide.domain.entity;
import java.util.ArrayList;
import java.util.List;
public class Accessory {
private String slotName;
private String accessoryName;
private List<Tuning> tunings = new ArrayList<>();
public Accessory() {
}
public String getSlotName() {
return slotName;
}
public void setSlotName(String slotName) {
this.slotName = slotName;
}
public String getAccessoryName() {
return accessoryName;
}
public void setAccessoryName(String accessoryName) {
this.accessoryName = accessoryName;
}
public List<Tuning> getTunings() {
return tunings;
}
public void setTunings(List<Tuning> tunings) {
this.tunings = tunings;
}
public void addTuning(Tuning tuning) {
this.tunings.add(tuning);
}
public void removeTuning(Tuning tuning) {
this.tunings.remove(tuning);
}
}
@@ -1,227 +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;
@Column(name = "calibre")
private String calibre;
@Column(name = "fire_rate")
private Integer fireRate;
@Column(name = "armour_damage")
private Integer armourDamage;
@Column(name = "body_damage")
private Integer bodyDamage;
@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 String getCalibre() {
return calibre;
}
public void setCalibre(String calibre) {
this.calibre = calibre;
}
public Integer getFireRate() {
return fireRate;
}
public void setFireRate(Integer fireRate) {
this.fireRate = fireRate;
}
public Integer getArmourDamage() {
return armourDamage;
}
public void setArmourDamage(Integer armourDamage) {
this.armourDamage = armourDamage;
}
public Integer getBodyDamage() {
return bodyDamage;
}
public void setBodyDamage(Integer bodyDamage) {
this.bodyDamage = bodyDamage;
}
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);
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Long id;
private String name;
private FirearmType type;
private String level;
private String review;
private String calibre;
private Integer fireRate;
private Integer armourDamage;
private Integer bodyDamage;
private List<Modification> modifications;
public Builder id(Long id) {
this.id = id;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder type(FirearmType type) {
this.type = type;
return this;
}
public Builder level(String level) {
this.level = level;
return this;
}
public Builder review(String review) {
this.review = review;
return this;
}
public Builder calibre(String calibre) {
this.calibre = calibre;
return this;
}
public Builder fireRate(Integer fireRate) {
this.fireRate = fireRate;
return this;
}
public Builder armourDamage(Integer armourDamage) {
this.armourDamage = armourDamage;
return this;
}
public Builder bodyDamage(Integer bodyDamage) {
this.bodyDamage = bodyDamage;
return this;
}
public Builder modifications(List<Modification> modifications) {
this.modifications = modifications;
return this;
}
public Firearm build() {
Firearm firearm = new Firearm();
firearm.id = this.id;
firearm.name = this.name;
firearm.type = this.type;
firearm.level = this.level;
firearm.review = this.review;
firearm.calibre = this.calibre;
firearm.fireRate = this.fireRate;
firearm.armourDamage = this.armourDamage;
firearm.bodyDamage = this.bodyDamage;
firearm.modifications = this.modifications == null ? new ArrayList<>() : this.modifications;
return firearm;
}
}
}
@@ -1,206 +0,0 @@
package com.onixbyte.deltaforceguide.domain.entity;
import io.hypersistence.utils.hibernate.type.json.JsonType;
import jakarta.persistence.*;
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 = "jsonb")
private List<String> tags = new ArrayList<>();
@Type(JsonType.class)
@Column(name = "accessories", columnDefinition = "jsonb")
private List<Accessory> accessories = 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 List<Accessory> getAccessories() {
return accessories;
}
public void setAccessories(List<Accessory> accessories) {
this.accessories = accessories;
}
public void addAccessory(Accessory modificationAccessory) {
this.accessories.add(modificationAccessory);
}
public void removeAccessory(Accessory modificationAccessory) {
this.accessories.remove(modificationAccessory);
}
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;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Long id;
private Firearm firearm;
private String name;
private String code;
private List<String> tags;
private List<Accessory> accessories;
private String note;
private String author;
private String videoUrl;
public Builder id(Long id) {
this.id = id;
return this;
}
public Builder firearm(Firearm firearm) {
this.firearm = firearm;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder code(String code) {
this.code = code;
return this;
}
public Builder tags(List<String> tags) {
this.tags = tags;
return this;
}
public Builder accessories(List<Accessory> accessories) {
this.accessories = accessories;
return this;
}
public Builder note(String note) {
this.note = note;
return this;
}
public Builder author(String author) {
this.author = author;
return this;
}
public Builder videoUrl(String videoUrl) {
this.videoUrl = videoUrl;
return this;
}
public Modification build() {
Modification modification = new Modification();
modification.id = this.id;
modification.firearm = this.firearm;
modification.name = this.name;
modification.code = this.code;
modification.tags = this.tags == null ? new ArrayList<>() : this.tags;
modification.accessories = this.accessories == null ? new ArrayList<>() : this.accessories;
modification.note = this.note;
modification.author = this.author;
modification.videoUrl = this.videoUrl;
return modification;
}
}
}
@@ -1,26 +0,0 @@
package com.onixbyte.deltaforceguide.domain.entity;
public class Tuning {
private String tuningName;
private Double tuningValue;
public Tuning() {
}
public String getTuningName() {
return tuningName;
}
public void setTuningName(String tuningName) {
this.tuningName = tuningName;
}
public Double getTuningValue() {
return tuningValue;
}
public void setTuningValue(Double tuningValue) {
this.tuningValue = tuningValue;
}
}
@@ -1,114 +0,0 @@
package com.onixbyte.deltaforceguide.domain.entity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
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 = "app_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false)
private String username;
@Column(name = "email", nullable = false)
private String email;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserCredential> credentials = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public List<UserCredential> getCredentials() {
return credentials;
}
public void setCredentials(List<UserCredential> credentials) {
this.credentials = credentials;
}
public void addCredential(UserCredential credential) {
this.credentials.add(credential);
credential.setUser(this);
}
public void removeCredential(UserCredential credential) {
this.credentials.remove(credential);
credential.setUser(null);
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Long id;
private String username;
private String email;
private List<UserCredential> credentials;
public Builder id(Long id) {
this.id = id;
return this;
}
public Builder username(String username) {
this.username = username;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder credentials(List<UserCredential> credentials) {
this.credentials = credentials;
return this;
}
public User build() {
User user = new User();
user.id = this.id;
user.username = this.username;
user.email = this.email;
user.credentials = this.credentials == null ? new ArrayList<>() : this.credentials;
return user;
}
}
}
@@ -1,138 +0,0 @@
package com.onixbyte.deltaforceguide.domain.entity;
import jakarta.persistence.Column;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.MapsId;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "app_user_credential")
public class UserCredential {
@EmbeddedId
@AttributeOverrides({
@AttributeOverride(name = "userId", column = @Column(name = "user_id")),
@AttributeOverride(name = "provider", column = @Column(name = "provider"))
})
private UserCredentialId id = new UserCredentialId();
@MapsId("userId")
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_user_credential_user"))
private User user;
@Column(name = "credential", nullable = false, length = 255)
private String credential;
public UserCredentialId getId() {
return id;
}
public void setId(UserCredentialId id) {
this.id = id;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
if (this.id == null) {
this.id = new UserCredentialId();
}
this.id.setUserId(user == null ? null : user.getId());
}
public Long getUserId() {
return id == null ? null : id.getUserId();
}
public void setUserId(Long userId) {
if (this.id == null) {
this.id = new UserCredentialId();
}
this.id.setUserId(userId);
}
public String getProvider() {
return id == null ? null : id.getProvider();
}
public void setProvider(String provider) {
if (this.id == null) {
this.id = new UserCredentialId();
}
this.id.setProvider(provider);
}
public String getCredential() {
return credential;
}
public void setCredential(String credential) {
this.credential = credential;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private UserCredentialId id;
private User user;
private Long userId;
private String provider;
private String credential;
public Builder id(UserCredentialId id) {
this.id = id;
return this;
}
public Builder user(User user) {
this.user = user;
return this;
}
public Builder userId(Long userId) {
this.userId = userId;
return this;
}
public Builder provider(String provider) {
this.provider = provider;
return this;
}
public Builder credential(String credential) {
this.credential = credential;
return this;
}
public UserCredential build() {
UserCredential userCredential = new UserCredential();
userCredential.id = this.id == null ? new UserCredentialId() : this.id;
userCredential.user = this.user;
if (this.user != null) {
userCredential.id.setUserId(this.user.getId());
}
if (this.userId != null) {
userCredential.id.setUserId(this.userId);
}
if (this.provider != null) {
userCredential.id.setProvider(this.provider);
}
userCredential.credential = this.credential;
return userCredential;
}
}
}
@@ -1,76 +0,0 @@
package com.onixbyte.deltaforceguide.domain.entity;
import jakarta.persistence.Embeddable;
import java.io.Serializable;
import java.util.Objects;
@Embeddable
public class UserCredentialId implements Serializable {
private Long userId;
private String provider;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getProvider() {
return provider;
}
public void setProvider(String provider) {
this.provider = provider;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
UserCredentialId that = (UserCredentialId) o;
return Objects.equals(userId, that.userId) && Objects.equals(provider, that.provider);
}
@Override
public int hashCode() {
return Objects.hash(userId, provider);
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Long userId;
private String provider;
public Builder userId(Long userId) {
this.userId = userId;
return this;
}
public Builder provider(String provider) {
this.provider = provider;
return this;
}
public UserCredentialId build() {
UserCredentialId id = new UserCredentialId();
id.userId = this.userId;
id.provider = this.provider;
return id;
}
}
}
@@ -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);
}
}
Loaded 100 of 161 files, more files were not shown because too many files have changed in this diff. Show more