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>
This commit is contained in:
2026-05-11 07:00:05 +08:00
parent b91855095b
commit 86e259a500
12 changed files with 324 additions and 84 deletions
+3
View File
@@ -12,6 +12,7 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs": "^2.1.2",
"@ant-design/icons": "^6.2.2",
"@reduxjs/toolkit": "^2.11.2", "@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/vite": "^4.2.4", "@tailwindcss/vite": "^4.2.4",
"@tanstack/react-virtual": "^3.13.24", "@tanstack/react-virtual": "^3.13.24",
@@ -27,6 +28,7 @@
"tailwindcss": "^4.2.4" "tailwindcss": "^4.2.4"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^22.19.17", "@types/node": "^22.19.17",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
@@ -35,6 +37,7 @@
"prettier": "^3.8.3", "prettier": "^3.8.3",
"typescript": "~6.0.3", "typescript": "~6.0.3",
"vite": "^8.0.11", "vite": "^8.0.11",
"vite-plugin-markdown": "^2.2.0",
"vite-plugin-port-checker": "^1.0.1" "vite-plugin-port-checker": "^1.0.1"
}, },
"pnpm": { "pnpm": {
+173
View File
@@ -11,6 +11,9 @@ importers:
'@ant-design/cssinjs': '@ant-design/cssinjs':
specifier: ^2.1.2 specifier: ^2.1.2
version: 2.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) version: 2.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@ant-design/icons':
specifier: ^6.2.2
version: 6.2.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@reduxjs/toolkit': '@reduxjs/toolkit':
specifier: ^2.11.2 specifier: ^2.11.2
version: 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6) version: 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6)
@@ -51,6 +54,9 @@ importers:
specifier: ^4.2.4 specifier: ^4.2.4
version: 4.2.4 version: 4.2.4
devDependencies: devDependencies:
'@tailwindcss/typography':
specifier: ^0.5.19
version: 0.5.19(tailwindcss@4.2.4)
'@types/node': '@types/node':
specifier: ^22.19.17 specifier: ^22.19.17
version: 22.19.17 version: 22.19.17
@@ -75,6 +81,9 @@ importers:
vite: vite:
specifier: ^8.0.11 specifier: ^8.0.11
version: 8.0.11(@types/node@22.19.17)(jiti@2.6.1) version: 8.0.11(@types/node@22.19.17)(jiti@2.6.1)
vite-plugin-markdown:
specifier: ^2.2.0
version: 2.2.0(vite@8.0.11(@types/node@22.19.17)(jiti@2.6.1))
vite-plugin-port-checker: vite-plugin-port-checker:
specifier: ^1.0.1 specifier: ^1.0.1
version: 1.0.1(vite@8.0.11(@types/node@22.19.17)(jiti@2.6.1)) version: 1.0.1(vite@8.0.11(@types/node@22.19.17)(jiti@2.6.1))
@@ -656,6 +665,11 @@ packages:
resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
'@tailwindcss/typography@0.5.19':
resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
'@tailwindcss/vite@4.2.4': '@tailwindcss/vite@4.2.4':
resolution: {integrity: sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==} resolution: {integrity: sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==}
peerDependencies: peerDependencies:
@@ -706,6 +720,12 @@ packages:
react: '>=18.0.0' react: '>=18.0.0'
react-dom: '>=18.0.0' react-dom: '>=18.0.0'
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
asynckit@0.4.0: asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -731,6 +751,11 @@ packages:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
csstype@3.2.3: csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -745,6 +770,19 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
dom-serializer@1.4.1:
resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
domhandler@4.3.1:
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
engines: {node: '>= 4'}
domutils@2.8.0:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
dunder-proto@1.0.1: dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -753,6 +791,12 @@ packages:
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
entities@2.1.0:
resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==}
entities@2.2.0:
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
es-define-property@1.0.1: es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -769,6 +813,11 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
fdir@6.5.0: fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -791,6 +840,9 @@ packages:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
front-matter@4.0.2:
resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==}
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -830,6 +882,9 @@ packages:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
htmlparser2@6.1.0:
resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==}
immer@11.1.4: immer@11.1.4:
resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==}
@@ -840,6 +895,10 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true hasBin: true
js-yaml@3.14.2:
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
hasBin: true
json2mq@0.2.0: json2mq@0.2.0:
resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==} resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==}
@@ -917,13 +976,23 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
linkify-it@3.0.3:
resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==}
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
markdown-it@12.3.2:
resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==}
hasBin: true
math-intrinsics@1.1.0: math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
mdurl@1.0.1:
resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
mime-db@1.52.0: mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -944,6 +1013,10 @@ packages:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'} engines: {node: '>=12'}
postcss-selector-parser@6.0.10:
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
engines: {node: '>=4'}
postcss@8.5.14: postcss@8.5.14:
resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@@ -1036,6 +1109,9 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
string-convert@0.2.1: string-convert@0.2.1:
resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==}
@@ -1065,6 +1141,9 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
uc.micro@1.0.6:
resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
undici-types@6.21.0: undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@@ -1073,6 +1152,14 @@ packages:
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vite-plugin-markdown@2.2.0:
resolution: {integrity: sha512-eH2tXMZcx3EHb5okd+/0VIyoR8Gp9pGe24UXitOOcGkzObbJ1vl48aGOAbakoT88FBdzC8MXNkMfBIB9VK0Ndg==}
peerDependencies:
vite: '>= 2.0.0'
vite-plugin-port-checker@1.0.1: vite-plugin-port-checker@1.0.1:
resolution: {integrity: sha512-Mx/Pj5zyu4oak+SKGwQyRLGAvnp9zx0nqjCLhaJoGJeZk2t4EarZojTcHGG0gPMJWj9X6c0MIfzaZDslg80/GA==} resolution: {integrity: sha512-Mx/Pj5zyu4oak+SKGwQyRLGAvnp9zx0nqjCLhaJoGJeZk2t4EarZojTcHGG0gPMJWj9X6c0MIfzaZDslg80/GA==}
peerDependencies: peerDependencies:
@@ -1702,6 +1789,11 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4
'@tailwindcss/oxide-win32-x64-msvc': 4.2.4 '@tailwindcss/oxide-win32-x64-msvc': 4.2.4
'@tailwindcss/typography@0.5.19(tailwindcss@4.2.4)':
dependencies:
postcss-selector-parser: 6.0.10
tailwindcss: 4.2.4
'@tailwindcss/vite@4.2.4(vite@8.0.11(@types/node@22.19.17)(jiti@2.6.1))': '@tailwindcss/vite@4.2.4(vite@8.0.11(@types/node@22.19.17)(jiti@2.6.1))':
dependencies: dependencies:
'@tailwindcss/node': 4.2.4 '@tailwindcss/node': 4.2.4
@@ -1798,6 +1890,12 @@ snapshots:
- luxon - luxon
- moment - moment
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3
argparse@2.0.1: {}
asynckit@0.4.0: {} asynckit@0.4.0: {}
axios@1.16.0: axios@1.16.0:
@@ -1823,6 +1921,8 @@ snapshots:
cookie@1.1.1: {} cookie@1.1.1: {}
cssesc@3.0.0: {}
csstype@3.2.3: {} csstype@3.2.3: {}
dayjs@1.11.20: {} dayjs@1.11.20: {}
@@ -1831,6 +1931,24 @@ snapshots:
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
dom-serializer@1.4.1:
dependencies:
domelementtype: 2.3.0
domhandler: 4.3.1
entities: 2.2.0
domelementtype@2.3.0: {}
domhandler@4.3.1:
dependencies:
domelementtype: 2.3.0
domutils@2.8.0:
dependencies:
dom-serializer: 1.4.1
domelementtype: 2.3.0
domhandler: 4.3.1
dunder-proto@1.0.1: dunder-proto@1.0.1:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@@ -1842,6 +1960,10 @@ snapshots:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
tapable: 2.3.3 tapable: 2.3.3
entities@2.1.0: {}
entities@2.2.0: {}
es-define-property@1.0.1: {} es-define-property@1.0.1: {}
es-errors@1.3.0: {} es-errors@1.3.0: {}
@@ -1857,6 +1979,8 @@ snapshots:
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
hasown: 2.0.3 hasown: 2.0.3
esprima@4.0.1: {}
fdir@6.5.0(picomatch@4.0.4): fdir@6.5.0(picomatch@4.0.4):
optionalDependencies: optionalDependencies:
picomatch: 4.0.4 picomatch: 4.0.4
@@ -1871,6 +1995,10 @@ snapshots:
hasown: 2.0.3 hasown: 2.0.3
mime-types: 2.1.35 mime-types: 2.1.35
front-matter@4.0.2:
dependencies:
js-yaml: 3.14.2
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
@@ -1910,12 +2038,24 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
htmlparser2@6.1.0:
dependencies:
domelementtype: 2.3.0
domhandler: 4.3.1
domutils: 2.8.0
entities: 2.2.0
immer@11.1.4: {} immer@11.1.4: {}
is-mobile@5.0.0: {} is-mobile@5.0.0: {}
jiti@2.6.1: {} jiti@2.6.1: {}
js-yaml@3.14.2:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
json2mq@0.2.0: json2mq@0.2.0:
dependencies: dependencies:
string-convert: 0.2.1 string-convert: 0.2.1
@@ -1969,12 +2109,26 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0
linkify-it@3.0.3:
dependencies:
uc.micro: 1.0.6
magic-string@0.30.21: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
markdown-it@12.3.2:
dependencies:
argparse: 2.0.1
entities: 2.1.0
linkify-it: 3.0.3
mdurl: 1.0.1
uc.micro: 1.0.6
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
mdurl@1.0.1: {}
mime-db@1.52.0: {} mime-db@1.52.0: {}
mime-types@2.1.35: mime-types@2.1.35:
@@ -1987,6 +2141,11 @@ snapshots:
picomatch@4.0.4: {} picomatch@4.0.4: {}
postcss-selector-parser@6.0.10:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss@8.5.14: postcss@8.5.14:
dependencies: dependencies:
nanoid: 3.3.12 nanoid: 3.3.12
@@ -2074,6 +2233,8 @@ snapshots:
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
sprintf-js@1.0.3: {}
string-convert@0.2.1: {} string-convert@0.2.1: {}
stylis@4.3.6: {} stylis@4.3.6: {}
@@ -2094,12 +2255,24 @@ snapshots:
typescript@6.0.3: {} typescript@6.0.3: {}
uc.micro@1.0.6: {}
undici-types@6.21.0: {} undici-types@6.21.0: {}
use-sync-external-store@1.6.0(react@19.2.6): use-sync-external-store@1.6.0(react@19.2.6):
dependencies: dependencies:
react: 19.2.6 react: 19.2.6
util-deprecate@1.0.2: {}
vite-plugin-markdown@2.2.0(vite@8.0.11(@types/node@22.19.17)(jiti@2.6.1)):
dependencies:
domhandler: 4.3.1
front-matter: 4.0.2
htmlparser2: 6.1.0
markdown-it: 12.3.2
vite: 8.0.11(@types/node@22.19.17)(jiti@2.6.1)
vite-plugin-port-checker@1.0.1(vite@8.0.11(@types/node@22.19.17)(jiti@2.6.1)): vite-plugin-port-checker@1.0.1(vite@8.0.11(@types/node@22.19.17)(jiti@2.6.1)):
dependencies: dependencies:
vite: 8.0.11(@types/node@22.19.17)(jiti@2.6.1) vite: 8.0.11(@types/node@22.19.17)(jiti@2.6.1)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

+5 -5
View File
@@ -7,9 +7,9 @@ interface FirearmParams extends PageQueryParams {
} }
/** /**
* 查询武器列表 * Fetch firearm list
* *
* @param params 分页查询参数¬ * @param params Paged query parameters
*/ */
export async function getFirearms(params?: FirearmParams): Promise<Page<Firearm>> { export async function getFirearms(params?: FirearmParams): Promise<Page<Firearm>> {
let uri = "/firearms" let uri = "/firearms"
@@ -28,9 +28,9 @@ export async function getFirearms(params?: FirearmParams): Promise<Page<Firearm>
} }
/** /**
* 根据 ID 查询武器 * Fetch firearm by ID
* *
* @param id 武器 ID * @param id Firearm ID
*/ */
export async function getFirearm(id: number): Promise<Firearm> { export async function getFirearm(id: number): Promise<Firearm> {
const { data } = await WebClient.get<Firearm>(`/firearms/${id}`) const { data } = await WebClient.get<Firearm>(`/firearms/${id}`)
@@ -38,7 +38,7 @@ export async function getFirearm(id: number): Promise<Firearm> {
} }
/** /**
* 新建武器 * Create firearm
* @param request * @param request
*/ */
export async function addFirearm(request: AddFirearmRequest): Promise<Firearm> { export async function addFirearm(request: AddFirearmRequest): Promise<Firearm> {
@@ -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 }}
/>
)
}
+2 -1
View File
@@ -1,6 +1,7 @@
@layer theme, base, antd, components, utilities; @layer theme, base, antd, components, utilities;
@import 'tailwindcss'; @import 'tailwindcss';
@plugin "@tailwindcss/typography";
html, body { html, body {
margin: 0; margin: 0;
@@ -56,7 +57,7 @@ html, body {
.nav-item:hover::after, .nav-item:hover::after,
.nav-item.active::after { .nav-item.active::after {
height: 80%; /* 向上打光的高度,可以调整 */ height: 80%; /* Height of the upward glow; adjustable. */
} }
.nav-item:hover, .nav-item:hover,
+68 -77
View File
@@ -2,10 +2,16 @@ import { Outlet, Link, NavLink } from "react-router-dom"
import { useMemo } from "react" import { useMemo } from "react"
import dayjs from "dayjs" import dayjs from "dayjs"
import { Dropdown } from "antd" import { Dropdown } from "antd"
import {
FileTextOutlined,
GithubOutlined,
LockOutlined,
LoginOutlined,
} from "@ant-design/icons"
import { AuthApi } from "@/api" import { AuthApi } from "@/api"
import { useAppDispatch, useAppSelector } from "@/store/hooks" import { useAppDispatch, useAppSelector } from "@/store/hooks"
import { clearCurrentUser } from "@/store/auth-slice" import { clearCurrentUser } from "@/store/auth-slice"
import { useState } from 'react'; import { useState } from "react"
/** /**
* Main application component that serves as the root layout. * Main application component that serves as the root layout.
@@ -15,7 +21,7 @@ export default function HeroLayout() {
const today = useMemo(() => dayjs(), []) const today = useMemo(() => dayjs(), [])
const user = useAppSelector((state) => state.auth.user) const user = useAppSelector((state) => state.auth.user)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false)
async function handleLogout() { async function handleLogout() {
try { try {
@@ -28,29 +34,29 @@ export default function HeroLayout() {
return ( return (
<div className="bg-gray-50 "> <div className="bg-gray-50 ">
{/* Navigation Header */} {/* Navigation Header */}
<header className="bg-white shadow-sm border-b bg-[url('/nav_bg.png')] bg-cover bg-center"> <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="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 justify-between items-center h-20">
<div className="flex items-center"> <div className="flex items-center">
<h1 className="text-xl font-semibold text-white"></h1> <h1 className="text-xl font-semibold text-white"></h1>
</div> </div>
<nav className="flex h-full"> <nav className="flex h-full">
<NavLink <NavLink
to="/firearms" to="/firearms"
className={({ isActive }) => className={({ isActive }) =>
`nav-item inline-flex items-center px-10 h-full text-base font-medium transition-all duration-200 ${isActive ? 'active' : '' `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` } text-gray-500 hover:text-white`
} }>
>
</NavLink> </NavLink>
<NavLink <NavLink
to="/mod-codes" to="/mod-codes"
className={({ isActive }) => className={({ isActive }) =>
`nav-item inline-flex items-center px-10 h-full text-base font-medium transition-all duration-200 ${isActive ? 'active' : '' `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` } text-gray-500 hover:text-white`
} }>
>
</NavLink> </NavLink>
{/* {user ? ( {/* {user ? (
@@ -102,71 +108,61 @@ export default function HeroLayout() {
{/* Footer */} {/* Footer */}
<footer className="bg-black border-t border-gray-800"> <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="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="flex flex-wrap justify-center items-center gap-x-6 gap-y-10 text-sm">
<div <div
className="relative" className="relative"
onMouseEnter={() => setIsDropdownOpen(true)} onMouseEnter={() => setIsDropdownOpen(true)}
onMouseLeave={() => setIsDropdownOpen(false)} onMouseLeave={() => setIsDropdownOpen(false)}>
> <button
<button className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200 focus:outline-none"
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200 focus:outline-none" aria-label="GitHub 仓库">
aria-label="GitHub 仓库" <GithubOutlined className="text-base opacity-80" />
> <span>GitHub</span>
<img <svg
src="/github-logo.png" className={`w-3 h-3 transition-transform duration-200 ${isDropdownOpen ? "rotate-180" : ""}`}
alt="" fill="none"
className="w-5 h-5 opacity-80" stroke="currentColor"
/> viewBox="0 0 24 24">
<span>GitHub</span> <path
<svg strokeLinecap="round"
className={`w-3 h-3 transition-transform duration-200 ${isDropdownOpen ? 'rotate-180' : ''}`} strokeLinejoin="round"
fill="none" strokeWidth={2}
stroke="currentColor" d="M19 9l-7 7-7-7"
viewBox="0 0 24 24" />
> </svg>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> </button>
</svg> {isDropdownOpen && (
</button> <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">
{isDropdownOpen && ( <a
<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"> href="https://github.com/zihluwang/frontend-repo"
<a target="_blank"
href="https://github.com/zihluwang/frontend-repo" rel="noopener noreferrer"
target="_blank" className="block px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white transition-colors">
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"
</a> target="_blank"
<a rel="noopener noreferrer"
href="https://github.com/zihluwang/backend-repo" className="block px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white transition-colors">
target="_blank"
rel="noopener noreferrer" </a>
className="block px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white transition-colors" </div>
> )}
</div>
</a>
</div>
)}
</div>
<span className="text-gray-700 select-none"></span> <span className="text-gray-700 select-none"></span>
<Link <Link
to="/" to="/legal?tab=eula"
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200" className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200">
> <FileTextOutlined className="text-lg opacity-80" />
<img src="/footer-1.png" alt="" className="w-8 w-8 opacity-80" />
EULA EULA
</Link> </Link>
<span className="text-gray-700 select-none"></span> <span className="text-gray-700 select-none"></span>
<Link <Link
to="/" to="/legal?tab=privacy"
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200" className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200">
> <LockOutlined className="text-lg opacity-80" />
<img src="/footer-2.png" alt="" className="w-8 w-8 opacity-80" />
</Link> </Link>
<span className="text-gray-700 select-none"></span> <span className="text-gray-700 select-none"></span>
@@ -182,8 +178,7 @@ export default function HeroLayout() {
onClick: handleLogout, 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"> <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} {user.username}
</span> </span>
@@ -191,9 +186,8 @@ export default function HeroLayout() {
) : ( ) : (
<Link <Link
to="/login" to="/login"
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200" className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200">
> <LoginOutlined className="text-lg opacity-80" />
<img src="../../public/footer-3.png" alt="" className="w-8 w-8 opacity-80" />
</Link> </Link>
)} )}
@@ -201,11 +195,8 @@ export default function HeroLayout() {
<div className="border-t border-gray-800 my-6" /> <div className="border-t border-gray-800 my-6" />
<div className="text-center text-xs text-gray-500"> <div className="text-center text-xs text-gray-500">
<p> <p>© 2024-{today.year()} Zihlu Wang OnixByte使 React TypeScript </p>
© 2024-{today.year()} Zihlu Wang OnixByte使 React TypeScript
</p>
</div> </div>
</div> </div>
</footer> </footer>
</div> </div>
+6
View File
@@ -0,0 +1,6 @@
declare module "*.md" {
const attributes: Record<string, any>
const html: string
const toc: { level: string; content: string; slug: string }[]
export { attributes, html, toc }
}
+34
View File
@@ -0,0 +1,34 @@
import { Tabs } from "antd"
import { useSearchParams } from "react-router-dom"
import MarkdownRenderer from "@/components/markdown-renderer"
import { html as EulaHtml } from "@/docs/EULA.md"
import { html as PrivacyHtml } from "@/docs/PrivacyPolicy.md"
const tabKeys = new Set(["eula", "privacy"])
export default function LegalPage() {
const [searchParams, setSearchParams] = useSearchParams()
const rawTab = searchParams.get("tab")
const activeTab = rawTab && tabKeys.has(rawTab) ? rawTab : "eula"
return (
<div className="mx-auto max-w-4xl">
<Tabs
activeKey={activeTab}
onChange={(key) => setSearchParams({ tab: key })}
items={[
{
key: "eula",
label: "最终用户许可协议",
children: <MarkdownRenderer html={EulaHtml} />,
},
{
key: "privacy",
label: "隐私政策",
children: <MarkdownRenderer html={PrivacyHtml} />,
},
]}
/>
</div>
)
}
+4
View File
@@ -40,6 +40,10 @@ const router = createBrowserRouter(
path: "mod-codes", path: "mod-codes",
lazy: lazy(() => import("@/page/mod-codes")), lazy: lazy(() => import("@/page/mod-codes")),
}, },
{
path: "legal",
lazy: lazy(() => import("@/page/legal"))
}
], ],
}, },
{ {
+10
View File
@@ -0,0 +1,10 @@
import type { Config } from "tailwindcss"
import typography from "@tailwindcss/typography"
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [typography],
} satisfies Config
+2 -1
View File
@@ -3,10 +3,11 @@ import { defineConfig } from "vite"
import react from "@vitejs/plugin-react" import react from "@vitejs/plugin-react"
import tailwindcss from "@tailwindcss/vite" import tailwindcss from "@tailwindcss/vite"
import portChecker from "vite-plugin-port-checker" import portChecker from "vite-plugin-port-checker"
import { Mode, plugin as markdown } from "vite-plugin-markdown"
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss(), portChecker()], plugins: [react(), tailwindcss(), portChecker(), markdown({ mode: [Mode.HTML, Mode.TOC] })],
base: "/", base: "/",
build: { build: {
rolldownOptions: { rolldownOptions: {