diff --git a/.githooks/pre-push b/.githooks/pre-push
new file mode 100755
index 0000000..5f95b0f
--- /dev/null
+++ b/.githooks/pre-push
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [[ "${SKIP_CHECKS:-0}" == "1" ]]; then
+ echo "[pre-push] SKIP_CHECKS=1,跳过检查"
+ exit 0
+fi
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+exec bash "${ROOT_DIR}/scripts/check.sh"
diff --git a/frontend/.prettierignore b/frontend/.prettierignore
new file mode 100644
index 0000000..e848f85
--- /dev/null
+++ b/frontend/.prettierignore
@@ -0,0 +1,4 @@
+node_modules/
+dist/
+coverage/
+package-lock.json
diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json
new file mode 100644
index 0000000..fa3dbf1
--- /dev/null
+++ b/frontend/.prettierrc.json
@@ -0,0 +1,8 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "trailingComma": "es5",
+ "printWidth": 100,
+ "tabWidth": 2,
+ "arrowParens": "avoid"
+}
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 0000000..886283b
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,32 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import tseslint from 'typescript-eslint'
+
+export default tseslint.config(
+ {
+ ignores: ['dist/**', 'coverage/**', 'node_modules/**'],
+ },
+ js.configs.recommended,
+ ...tseslint.configs.recommended,
+ {
+ files: ['**/*.{ts,tsx}'],
+ languageOptions: {
+ ecmaVersion: 'latest',
+ globals: globals.browser,
+ parserOptions: {
+ ecmaFeatures: { jsx: true },
+ },
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ },
+ rules: {
+ 'react-hooks/rules-of-hooks': 'error',
+ 'react-hooks/exhaustive-deps': 'warn',
+ '@typescript-eslint/no-explicit-any': 'error',
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
+ 'no-console': ['error', { allow: ['warn', 'error'] }],
+ },
+ }
+)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index c53c175..eefaf1e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -21,12 +21,21 @@
"zustand": "^5.0.8"
},
"devDependencies": {
+ "@eslint/js": "^10.0.1",
"@types/node": "^24.10.1",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4",
+ "@vitest/coverage-v8": "^5.0.5",
+ "eslint": "^10.14.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.7",
+ "globals": "^17.14.0",
+ "prettier": "^3.9.10",
"typescript": "~5.9.3",
- "vite": "^7.2.4"
+ "typescript-eslint": "^8.72.1",
+ "vite": "^7.2.4",
+ "vitest": "^5.0.5"
}
},
"node_modules/@ant-design/colors": {
@@ -413,6 +422,40 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@cacheable/memory": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
+ "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cacheable/utils": "^2.5.0",
+ "@keyv/bigmap": "^1.3.1",
+ "hookified": "^1.15.1",
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@cacheable/utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz",
+ "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hashery": "^1.5.1",
+ "keyv": "^5.6.0"
+ }
+ },
"node_modules/@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
@@ -867,6 +910,134 @@
"node": ">=18"
}
},
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
+ "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
+ "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz",
+ "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
"node_modules/@floating-ui/core": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
@@ -892,6 +1063,72 @@
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
"license": "MIT"
},
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -942,6 +1179,30 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@keyv/bigmap": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
+ "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hashery": "^1.4.0",
+ "hookified": "^1.15.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@keyv/serialize": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
+ "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
@@ -1620,6 +1881,31 @@
"@babel/types": "^7.28.2"
}
},
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -1627,6 +1913,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "24.19.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz",
@@ -1657,6 +1950,249 @@
"@types/react": "^19.3.0"
}
},
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz",
+ "integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.72.1",
+ "@typescript-eslint/type-utils": "8.72.1",
+ "@typescript-eslint/utils": "8.72.1",
+ "@typescript-eslint/visitor-keys": "8.72.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.72.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz",
+ "integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.72.1",
+ "@typescript-eslint/types": "8.72.1",
+ "@typescript-eslint/typescript-estree": "8.72.1",
+ "@typescript-eslint/visitor-keys": "8.72.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz",
+ "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.72.1",
+ "@typescript-eslint/types": "^8.72.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz",
+ "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.72.1",
+ "@typescript-eslint/visitor-keys": "8.72.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz",
+ "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz",
+ "integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.72.1",
+ "@typescript-eslint/typescript-estree": "8.72.1",
+ "@typescript-eslint/utils": "8.72.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz",
+ "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz",
+ "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.72.1",
+ "@typescript-eslint/tsconfig-utils": "8.72.1",
+ "@typescript-eslint/types": "8.72.1",
+ "@typescript-eslint/visitor-keys": "8.72.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz",
+ "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.72.1",
+ "@typescript-eslint/types": "8.72.1",
+ "@typescript-eslint/typescript-estree": "8.72.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz",
+ "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.72.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
"node_modules/@use-gesture/core": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.0.tgz",
@@ -1696,6 +2232,119 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/@vitest/coverage-v8": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.5.tgz",
+ "integrity": "sha512-4KpkGdIrSqBihx/JyP/e+S+IKB98etg1XDQiLAxksLfcShnwi1VReGeOT9NI43hLHbknosKp/1CCLn9Zgcs8mg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^1.0.2",
+ "@vitest/istanbul-lib-coverage": "^1.0.2",
+ "@vitest/istanbul-lib-report": "^1.0.2",
+ "ast-v8-to-istanbul": "^1.0.7",
+ "magicast": "^0.5.5",
+ "obug": "^3.0.0",
+ "std-env": "^4.3.0",
+ "tinyrainbow": "^3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "5.0.5",
+ "vitest": "5.0.5"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/istanbul-lib-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.2.tgz",
+ "integrity": "sha512-9J/JMwOf9AoJhAywhrn7ScKTL38hsWQP/qPG60OtaAFcQ5OXPwKsxZFlbnuCKmZ61m8/lGgHYnFpdyQZUvG/iA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@vitest/istanbul-lib-report": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-1.0.2.tgz",
+ "integrity": "sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/istanbul-lib-coverage": "1.0.2"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz",
+ "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.31",
+ "@vitest/spy": "5.0.5",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^1.4.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9 || ^3.0.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz",
+ "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz",
+ "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -1728,6 +2377,23 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/antd": {
"version": "5.29.3",
"resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz",
@@ -1988,6 +2654,35 @@
"react-dom": ">=16.0.0"
}
},
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.7.tgz",
+ "integrity": "sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^10.0.0"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
+ "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
@@ -2012,6 +2707,16 @@
"proxy-from-env": "^2.1.0"
}
},
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
"node_modules/baseline-browser-mapping": {
"version": "2.11.34",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.34.tgz",
@@ -2025,6 +2730,19 @@
"node": ">=6.0.0"
}
},
+ "node_modules/brace-expansion": {
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
+ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/browserslist": {
"version": "4.29.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz",
@@ -2059,6 +2777,20 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/cacheable": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
+ "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cacheable/memory": "^2.2.0",
+ "@cacheable/utils": "^2.5.0",
+ "hookified": "^1.15.0",
+ "keyv": "^5.6.0",
+ "qified": "^0.10.1"
+ }
+ },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -2093,6 +2825,16 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/chai": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz",
+ "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
@@ -2155,6 +2897,21 @@
"toggle-selection": "^1.0.6"
}
},
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -2184,6 +2941,13 @@
}
}
},
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@@ -2241,6 +3005,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-module-lexer": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+ "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/es-object-atoms": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
@@ -2320,6 +3091,245 @@
"node": ">=6"
}
},
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.14.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.14.0.tgz",
+ "integrity": "sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "packages/*"
+ ],
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.7.0",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.3",
+ "@humanfs/node": "^0.16.8",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "11.1.5 || >11.1.6 <12",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.6",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.7.tgz",
+ "integrity": "sha512-XhJSzLljuYD4UjNuFGJu2v7aD3sqNVK12w5/DCUfrNfHWegZo2+TsavNx8MuxMwEquuFAzNbNfXKU0WoZ+YUIg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -2338,6 +3348,52 @@
}
}
},
+ "node_modules/file-entry-cache": {
+ "version": "11.1.5",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz",
+ "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^6.1.23"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "6.1.23",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz",
+ "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cacheable": "^2.5.0",
+ "flatted": "^3.4.2",
+ "hookified": "^1.15.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
+ "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/follow-redirects": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.2.tgz",
@@ -2445,6 +3501,32 @@
"node": ">= 0.4"
}
},
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.14.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.14.0.tgz",
+ "integrity": "sha512-UtdkTVyAJePQv0J2M9Xe4L0ruNSDV314XiIJsADI2Zf40bcdAonE/D/OhAU8AT99oij9mhLByb+NvA3QWZYgCg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -2484,6 +3566,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/hashery": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
+ "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^1.15.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
@@ -2496,6 +3591,30 @@
"node": ">= 0.4"
}
},
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/hookified": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
+ "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -2509,12 +3628,62 @@
"node": ">= 6"
}
},
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-mobile": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz",
"integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==",
"license": "MIT"
},
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/js-cookie": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
@@ -2541,6 +3710,20 @@
"node": ">=6"
}
},
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json2mq": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
@@ -2563,6 +3746,46 @@
"node": ">=6"
}
},
+ "node_modules/keyv": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
+ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@keyv/serialize": "^1.1.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
@@ -2579,6 +3802,28 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/magic-string": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz",
+ "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.6.0"
+ }
+ },
+ "node_modules/magicast": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz",
+ "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "source-map-js": "^1.2.1"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -2609,6 +3854,22 @@
"node": ">= 0.6"
}
},
+ "node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2640,6 +3901,13 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/node-releases": {
"version": "2.0.61",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.61.tgz",
@@ -2650,6 +3918,90 @@
"node": ">=18"
}
},
+ "node_modules/obug": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz",
+ "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -2699,6 +4051,32 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.9.10",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz",
+ "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
@@ -2708,6 +4086,36 @@
"node": ">=10"
}
},
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qified": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz",
+ "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/qified/node_modules/hookified": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz",
+ "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/rc-cascader": {
"version": "3.34.0",
"resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz",
@@ -3502,6 +4910,29 @@
"integrity": "sha512-irZHtTqurLna57KQG/s+fzOwv/J6XSH4cs8M/vsrY3uCaQ3S6+QJguAH0WdN47SXlk82voMZ4naWMQf8fdEoIg==",
"license": "MIT"
},
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
@@ -3512,6 +4943,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/std-env": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz",
+ "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/string-convert": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
@@ -3533,6 +4971,26 @@
"node": ">=12.22"
}
},
+ "node_modules/tinybench": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz",
+ "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/tinyexec": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
+ "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -3550,18 +5008,54 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tinyrainbow": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz",
+ "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
"license": "MIT"
},
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -3576,6 +5070,30 @@
"node": ">=14.17"
}
},
+ "node_modules/typescript-eslint": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz",
+ "integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.72.1",
+ "@typescript-eslint/parser": "8.72.1",
+ "@typescript-eslint/typescript-estree": "8.72.1",
+ "@typescript-eslint/utils": "8.72.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
"node_modules/undici-types": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
@@ -3614,6 +5132,16 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz",
@@ -3698,6 +5226,128 @@
}
}
},
+ "node_modules/vitest": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz",
+ "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/mocker": "5.0.5",
+ "chai": "^6.3.0",
+ "es-module-lexer": "^2.3.2",
+ "expect-type": "^1.4.0",
+ "magic-string": "^1.4.3",
+ "obug": "^3.0.0",
+ "picomatch": "^4.0.7",
+ "std-env": "^4.3.0",
+ "tinybench": "^6.2.2",
+ "tinyexec": "^1.3.1",
+ "tinyglobby": "^0.2.17",
+ "why-is-node-running": "3.2.1"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "5.0.5",
+ "@vitest/browser-preview": "5.0.5",
+ "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
+ "@vitest/coverage-istanbul": "5.0.5",
+ "@vitest/coverage-v8": "5.0.5",
+ "@vitest/ui": "5.0.5",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz",
+ "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=20.11"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -3705,6 +5355,42 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.6.7",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz",
+ "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
"node_modules/zustand": {
"version": "5.0.16",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.16.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 7f57a9c..0bf7c33 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -7,7 +7,13 @@
"dev": "vite --host 0.0.0.0",
"build": "tsc --noEmit && vite build",
"preview": "vite preview --host 0.0.0.0",
- "typecheck": "tsc --noEmit"
+ "typecheck": "tsc --noEmit",
+ "format": "prettier --write \"src/**/*.{ts,tsx,css}\" \"*.{js,ts,json}\"",
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,css}\" \"*.{js,ts,json}\"",
+ "lint": "eslint . --max-warnings=0",
+ "test": "vitest run",
+ "test:coverage": "vitest run --coverage",
+ "check": "npm run format:check && npm run lint && npm run typecheck"
},
"dependencies": {
"@ant-design/icons": "^6.0.0",
@@ -23,11 +29,20 @@
"zustand": "^5.0.8"
},
"devDependencies": {
+ "@eslint/js": "^10.0.1",
"@types/node": "^24.10.1",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4",
+ "@vitest/coverage-v8": "^5.0.5",
+ "eslint": "^10.14.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.7",
+ "globals": "^17.14.0",
+ "prettier": "^3.9.10",
"typescript": "~5.9.3",
- "vite": "^7.2.4"
+ "typescript-eslint": "^8.72.1",
+ "vite": "^7.2.4",
+ "vitest": "^5.0.5"
}
}
diff --git a/frontend/src/components/EvidenceUpload.tsx b/frontend/src/components/EvidenceUpload.tsx
index e68a018..3339a9a 100644
--- a/frontend/src/components/EvidenceUpload.tsx
+++ b/frontend/src/components/EvidenceUpload.tsx
@@ -75,7 +75,13 @@ export function EvidenceUpload({
{value.map(url => (
{isImageFileURL(url) ? (
-
+
) : (
diff --git a/frontend/src/features/admin/AdminAnnouncementsPage.tsx b/frontend/src/features/admin/AdminAnnouncementsPage.tsx
index 524ca7c..dc63118 100644
--- a/frontend/src/features/admin/AdminAnnouncementsPage.tsx
+++ b/frontend/src/features/admin/AdminAnnouncementsPage.tsx
@@ -1,4 +1,4 @@
-import { useMemo, useState } from 'react'
+import { useState } from 'react'
import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query'
import {
Alert,
@@ -151,139 +151,136 @@ export function AdminAnnouncementsPage() {
})
}
- const columns = useMemo>(
- () => [
- {
- title: '公告',
- dataIndex: 'title',
- width: 320,
- fixed: 'left',
- render: (_, row) => (
-
- {row.title}
-
- {row.is_pinned ? 置顶 : null}
- {row.is_important ? 重要 : null}
- {row.is_popup ? 弹窗 : null}
- {row.priority !== 0 ? 优先级 {row.priority} : null}
-
- #{row.id}
-
- ),
- },
- {
- title: '分类',
- dataIndex: 'category',
- width: 110,
- render: value => announcementCategoryMap[value] ?? value,
- },
- {
- title: '站点',
- key: 'site',
- width: 150,
- render: (_, row) =>
- row.distribution_site_id > 0
- ? row.distribution_site_name || `分站 #${row.distribution_site_id}`
- : '主站',
- },
- {
- title: '状态',
- dataIndex: 'status',
- width: 110,
- render: value => ,
- },
- {
- title: '浏览',
- dataIndex: 'view_count',
- width: 90,
- align: 'right',
- },
- {
- title: '发布时间',
- dataIndex: 'published_at',
- width: 170,
- render: value => formatDateTime(value),
- },
- {
- title: '更新时间',
- dataIndex: 'updated_at',
- width: 170,
- render: value => formatDateTime(value),
- },
- {
- title: '操作',
- key: 'actions',
- width: 260,
- fixed: 'right',
- render: (_, row) => (
-
- {canManage ? (
- } onClick={() => openEdit(row)}>
- 编辑
-
- ) : null}
- {row.status === 'draft' && canManage ? (
-
- ) : null}
- {row.status === 'published' && canManage ? (
-
- ) : null}
- {row.status === 'archived' && canManage ? (
-
- ) : null}
- {canManage ? (
- }
- onClick={() =>
- runAction(
- '删除公告?',
- `删除后不可恢复:「${row.title}」。如果只是下架,建议使用归档。`,
- () => deleteAdminAnnouncement(row.id)
- )
- }
- >
- 删除
-
- ) : null}
+ const columns: TableColumnsType = [
+ {
+ title: '公告',
+ dataIndex: 'title',
+ width: 320,
+ fixed: 'left',
+ render: (_, row) => (
+
+ {row.title}
+
+ {row.is_pinned ? 置顶 : null}
+ {row.is_important ? 重要 : null}
+ {row.is_popup ? 弹窗 : null}
+ {row.priority !== 0 ? 优先级 {row.priority} : null}
- ),
- },
- ],
- [canManage]
- )
+ #{row.id}
+
+ ),
+ },
+ {
+ title: '分类',
+ dataIndex: 'category',
+ width: 110,
+ render: value => announcementCategoryMap[value] ?? value,
+ },
+ {
+ title: '站点',
+ key: 'site',
+ width: 150,
+ render: (_, row) =>
+ row.distribution_site_id > 0
+ ? row.distribution_site_name || `分站 #${row.distribution_site_id}`
+ : '主站',
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ width: 110,
+ render: value => ,
+ },
+ {
+ title: '浏览',
+ dataIndex: 'view_count',
+ width: 90,
+ align: 'right',
+ },
+ {
+ title: '发布时间',
+ dataIndex: 'published_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+ {
+ title: '更新时间',
+ dataIndex: 'updated_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+ {
+ title: '操作',
+ key: 'actions',
+ width: 260,
+ fixed: 'right',
+ render: (_, row) => (
+
+ {canManage ? (
+ } onClick={() => openEdit(row)}>
+ 编辑
+
+ ) : null}
+ {row.status === 'draft' && canManage ? (
+
+ ) : null}
+ {row.status === 'published' && canManage ? (
+
+ ) : null}
+ {row.status === 'archived' && canManage ? (
+
+ ) : null}
+ {canManage ? (
+ }
+ onClick={() =>
+ runAction(
+ '删除公告?',
+ `删除后不可恢复:「${row.title}」。如果只是下架,建议使用归档。`,
+ () => deleteAdminAnnouncement(row.id)
+ )
+ }
+ >
+ 删除
+
+ ) : null}
+
+ ),
+ },
+ ]
const patchFilter = (patch: Partial) => {
setDraft(current => ({ ...current, ...patch }))
diff --git a/frontend/src/features/admin/AdminAuditLogsPage.tsx b/frontend/src/features/admin/AdminAuditLogsPage.tsx
index 09a6964..963fb53 100644
--- a/frontend/src/features/admin/AdminAuditLogsPage.tsx
+++ b/frontend/src/features/admin/AdminAuditLogsPage.tsx
@@ -258,7 +258,11 @@ function buildSections(row: AdminAuditLog): DetailSection[] {
key: 'json',
label: '原始 JSON',
span: 2,
- value: {normalizeDetail(row.detail)},
+ value: (
+
+ {normalizeDetail(row.detail)}
+
+ ),
},
],
},
@@ -273,9 +277,7 @@ export function AdminAuditLogsPage() {
const [selected, setSelected] = useState(null)
const [detailOpen, setDetailOpen] = useState(false)
const permissions = useAdminSessionStore(state => state.admin?.permissions)
- const canView = Boolean(
- permissions?.includes('*') || permissions?.includes('audit_log:view')
- )
+ const canView = Boolean(permissions?.includes('*') || permissions?.includes('audit_log:view'))
const { data, isPending, isFetching, isError, error, refetch } = useQuery({
queryKey: ['admin-audit-logs', applied, page, pageSize],
@@ -434,7 +436,7 @@ export function AdminAuditLogsPage() {
setDetailOpen(false)}
- title={selected ? auditActionMap[selected.action] ?? selected.action : '审计详情'}
+ title={selected ? (auditActionMap[selected.action] ?? selected.action) : '审计详情'}
subtitle={
selected
? `#${selected.id} · ${formatDateTime(selected.created_at)} · ${actorName(selected)}`
diff --git a/frontend/src/features/admin/AdminBackupsPage.tsx b/frontend/src/features/admin/AdminBackupsPage.tsx
index 5a3256e..31ebead 100644
--- a/frontend/src/features/admin/AdminBackupsPage.tsx
+++ b/frontend/src/features/admin/AdminBackupsPage.tsx
@@ -71,7 +71,10 @@ export function AdminBackupsPage() {
const statusUnavailable =
statusQuery.isError &&
/尚未生成|不可用/.test(
- String((statusQuery.error as { response?: { data?: { message?: string } } })?.response?.data?.message ?? '')
+ String(
+ (statusQuery.error as { response?: { data?: { message?: string } } })?.response?.data
+ ?.message ?? ''
+ )
)
return (
@@ -112,12 +115,19 @@ export function AdminBackupsPage() {
description="备份监控脚本尚未在服务器写入状态文件;配置好定时任务后会出现在这里。"
/>
) : statusQuery.isError ? (
-
+
) : null}
- {formatText(status?.generated_at)}
+
+ {formatText(status?.generated_at)}
+
{formatText(status?.health)}
{formatText(status?.last_full?.local_complete)}
@@ -179,7 +189,8 @@ export function AdminBackupsPage() {
- 全量备份每天在指定时间执行;binlog 按间隔增量归档到远端。修改后需等待备份脚本下次读取配置。
+ 全量备份每天在指定时间执行;binlog
+ 按间隔增量归档到远端。修改后需等待备份脚本下次读取配置。
diff --git a/frontend/src/features/admin/AdminChatWelcomePage.tsx b/frontend/src/features/admin/AdminChatWelcomePage.tsx
index 31a7ebd..992147b 100644
--- a/frontend/src/features/admin/AdminChatWelcomePage.tsx
+++ b/frontend/src/features/admin/AdminChatWelcomePage.tsx
@@ -88,12 +88,7 @@ export function AdminChatWelcomePage() {
>
-
-
- 模板留空将回退到后端默认模板
-
+ 模板留空将回退到后端默认模板
diff --git a/frontend/src/features/admin/AdminCrashOrdersPage.tsx b/frontend/src/features/admin/AdminCrashOrdersPage.tsx
index 6bcef44..86f7bad 100644
--- a/frontend/src/features/admin/AdminCrashOrdersPage.tsx
+++ b/frontend/src/features/admin/AdminCrashOrdersPage.tsx
@@ -1,6 +1,16 @@
import { useState } from 'react'
import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query'
-import { Alert, App, Button, Input, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
+import {
+ Alert,
+ App,
+ Button,
+ Input,
+ Select,
+ Space,
+ Tag,
+ Typography,
+ type TableColumnsType,
+} from 'antd'
import { ReloadOutlined } from '@ant-design/icons'
import {
@@ -40,7 +50,9 @@ export function AdminCrashOrdersPage() {
const [pageSize, setPageSize] = useState(20)
const permissions = useAdminSessionStore(state => state.admin?.permissions)
const canView = Boolean(permissions?.includes('*') || permissions?.includes('mohong:order_view'))
- const canManage = Boolean(permissions?.includes('*') || permissions?.includes('mohong:order_manage'))
+ const canManage = Boolean(
+ permissions?.includes('*') || permissions?.includes('mohong:order_manage')
+ )
const query = {
keyword: applied.keyword.trim() || undefined,
@@ -174,7 +186,9 @@ export function AdminCrashOrdersPage() {
okButtonProps: { danger: true },
cancelText: '返回',
onOk: async () => {
- const el = document.getElementById(`cancel-reason-${row.id}`) as HTMLInputElement | null
+ const el = document.getElementById(
+ `cancel-reason-${row.id}`
+ ) as HTMLInputElement | null
await act(() => cancelCrashOrder(row.id, el?.value || '后台取消'), '订单已取消')
},
})
@@ -195,7 +209,8 @@ export function AdminCrashOrdersPage() {
okText: '确认退款',
okButtonProps: { danger: true },
cancelText: '返回',
- onOk: async () => act(() => refundCrashOrder(row.id, { reason: '后台退款' }), '退款已提交'),
+ onOk: async () =>
+ act(() => refundCrashOrder(row.id, { reason: '后台退款' }), '退款已提交'),
})
}
>
@@ -260,8 +275,13 @@ export function AdminCrashOrdersPage() {
allowClear
value={filters.status}
placeholder="全部状态"
- options={Object.entries(statusMeta).map(([value, meta]) => ({ value, label: meta.label }))}
- onChange={(value: string | undefined) => setFilters(current => ({ ...current, status: value }))}
+ options={Object.entries(statusMeta).map(([value, meta]) => ({
+ value,
+ label: meta.label,
+ }))}
+ onChange={(value: string | undefined) =>
+ setFilters(current => ({ ...current, status: value }))
+ }
/>
diff --git a/frontend/src/features/admin/AdminCrashProductsPage.tsx b/frontend/src/features/admin/AdminCrashProductsPage.tsx
index ebdee23..6e87b80 100644
--- a/frontend/src/features/admin/AdminCrashProductsPage.tsx
+++ b/frontend/src/features/admin/AdminCrashProductsPage.tsx
@@ -56,7 +56,11 @@ export function AdminCrashProductsPage() {
const { message, modal } = App.useApp()
const queryClient = useQueryClient()
const [form] = Form.useForm()
- const [filters, setFilters] = useState<{ keyword: string; status?: string; category_id?: number }>({
+ const [filters, setFilters] = useState<{
+ keyword: string
+ status?: string
+ category_id?: number
+ }>({
keyword: '',
})
const [applied, setApplied] = useState(filters)
@@ -67,8 +71,12 @@ export function AdminCrashProductsPage() {
const [uploading, setUploading] = useState(false)
const [submitting, setSubmitting] = useState(false)
const permissions = useAdminSessionStore(state => state.admin?.permissions)
- const canView = Boolean(permissions?.includes('*') || permissions?.includes('mohong:product_view'))
- const canManage = Boolean(permissions?.includes('*') || permissions?.includes('mohong:product_manage'))
+ const canView = Boolean(
+ permissions?.includes('*') || permissions?.includes('mohong:product_view')
+ )
+ const canManage = Boolean(
+ permissions?.includes('*') || permissions?.includes('mohong:product_manage')
+ )
const query = {
keyword: applied.keyword.trim() || undefined,
@@ -90,8 +98,7 @@ export function AdminCrashProductsPage() {
enabled: canView,
})
- const invalidate = () =>
- queryClient.invalidateQueries({ queryKey: ['admin-crash-products'] })
+ const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin-crash-products'] })
const save = async () => {
const values = await form.validateFields()
@@ -391,7 +398,12 @@ export function AdminCrashProductsPage() {
-
+
@@ -429,7 +441,11 @@ export function AdminCrashProductsPage() {
上传后自动填入封面 URL
-
+
diff --git a/frontend/src/features/admin/AdminDashboardPage.tsx b/frontend/src/features/admin/AdminDashboardPage.tsx
index 668e3b1..1a95147 100644
--- a/frontend/src/features/admin/AdminDashboardPage.tsx
+++ b/frontend/src/features/admin/AdminDashboardPage.tsx
@@ -363,9 +363,7 @@ export function AdminDashboardPage() {
const uploaders = (listingDaily?.today_uploaders ?? []).filter(row => row.upload_count > 0)
const manualEntries = (data?.manual_entry_today ?? []).filter(row => row.count > 0)
const channelTrend = [...(listingDaily?.channel_trend ?? [])]
- .filter(
- row => row.published_count + row.active_offline_count + row.trade_leave_count > 0
- )
+ .filter(row => row.published_count + row.active_offline_count + row.trade_leave_count > 0)
.sort(
(a, b) =>
b.date.localeCompare(a.date) ||
@@ -477,11 +475,7 @@ export function AdminDashboardPage() {
- }
- loading={isFetching}
- onClick={() => void refetch()}
- >
+ } loading={isFetching} onClick={() => void refetch()}>
刷新数据
navigate('orders')}>
@@ -759,9 +753,7 @@ export function AdminDashboardPage() {
{item.label}
{item.desc}
- 0 ? ' is-urgent' : ''}`}
- >
+ 0 ? ' is-urgent' : ''}`}>
{item.count}
diff --git a/frontend/src/features/admin/AdminDisputesPage.tsx b/frontend/src/features/admin/AdminDisputesPage.tsx
index 7140e0e..f40453a 100644
--- a/frontend/src/features/admin/AdminDisputesPage.tsx
+++ b/frontend/src/features/admin/AdminDisputesPage.tsx
@@ -1,4 +1,4 @@
-import { useMemo, useState } from 'react'
+import { useState } from 'react'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { Alert, Button, Input, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
import { EyeOutlined, ReloadOutlined } from '@ant-design/icons'
@@ -145,7 +145,7 @@ export function AdminDisputesPage() {
setDetailOpen(true)
}
- const columns = useMemo(() => buildColumns(openDetail, canView), [canView])
+ const columns = buildColumns(openDetail, canView)
const patchFilter = (patch: Partial) => {
setDraft(current => ({ ...current, ...patch }))
diff --git a/frontend/src/features/admin/AdminDistributionDashboardPage.tsx b/frontend/src/features/admin/AdminDistributionDashboardPage.tsx
index f2cbf56..aad52dc 100644
--- a/frontend/src/features/admin/AdminDistributionDashboardPage.tsx
+++ b/frontend/src/features/admin/AdminDistributionDashboardPage.tsx
@@ -46,7 +46,7 @@ export function AdminDistributionDashboardPage() {
})
const data = analyticsQuery.data
- const trendDays = data?.trend_days ?? []
+ const trendDays = useMemo(() => data?.trend_days ?? [], [data?.trend_days])
const maxProfit = useMemo(
() => Math.max(1, ...trendDays.map(point => point.profit_cent)),
[trendDays]
@@ -208,11 +208,7 @@ export function AdminDistributionDashboardPage() {
近 7 天经营趋势
}
- extra={
-
- 收号 / 订单 / GMV / 站点利润
-
- }
+ extra={收号 / 订单 / GMV / 站点利润}
>
{trendDays.length === 0 ? (
diff --git a/frontend/src/features/admin/AdminDistributionInvitationPage.tsx b/frontend/src/features/admin/AdminDistributionInvitationPage.tsx
index fc3f234..11a3ce4 100644
--- a/frontend/src/features/admin/AdminDistributionInvitationPage.tsx
+++ b/frontend/src/features/admin/AdminDistributionInvitationPage.tsx
@@ -143,9 +143,7 @@ export function AdminDistributionInvitationPage() {
) : null}
{savedAt ? 保存于 {formatDateTime(savedAt)} : null}
- {config?.updated_at ? (
- 服务端更新 {formatDateTime(config.updated_at)}
- ) : null}
+ {config?.updated_at ? 服务端更新 {formatDateTime(config.updated_at)} : null}
}
extra={
diff --git a/frontend/src/features/admin/AdminDistributionListingsPage.tsx b/frontend/src/features/admin/AdminDistributionListingsPage.tsx
index e9db505..cc3fb1c 100644
--- a/frontend/src/features/admin/AdminDistributionListingsPage.tsx
+++ b/frontend/src/features/admin/AdminDistributionListingsPage.tsx
@@ -159,13 +159,20 @@ export function AdminDistributionListingsPage() {
})
const receivedRows = receivedQuery.data?.items ?? []
- const accountRows = accountsQuery.data ?? []
+ const accountRows = useMemo(() => accountsQuery.data ?? [], [accountsQuery.data])
const filteredAccounts = useMemo(() => {
const keyword = accountKeyword.trim().toLowerCase()
if (!keyword) return accountRows
return accountRows.filter(row =>
- [row.title, row.listing_no, row.game_name, row.server_region, row.rank_level, row.login_platform]
+ [
+ row.title,
+ row.listing_no,
+ row.game_name,
+ row.server_region,
+ row.rank_level,
+ row.login_platform,
+ ]
.filter(Boolean)
.some(value => String(value).toLowerCase().includes(keyword))
)
@@ -173,10 +180,7 @@ export function AdminDistributionListingsPage() {
const pagedAccounts = useMemo(
() =>
- filteredAccounts.slice(
- (accountPage - 1) * accountPageSize,
- accountPage * accountPageSize
- ),
+ filteredAccounts.slice((accountPage - 1) * accountPageSize, accountPage * accountPageSize),
[accountPage, accountPageSize, filteredAccounts]
)
@@ -285,11 +289,7 @@ export function AdminDistributionListingsPage() {
const values = await batchMarkupForm.validateFields()
setWorking(true)
try {
- await batchUpdateAdminAccountMarkup(
- siteId,
- selectedAccountIds,
- values.shelf_ratio_delta
- )
+ await batchUpdateAdminAccountMarkup(siteId, selectedAccountIds, values.shelf_ratio_delta)
message.success('批量加价已更新')
setBatchMarkupOpen(false)
setSelectedAccountIds([])
@@ -319,8 +319,12 @@ export function AdminDistributionListingsPage() {
title: '账号同步完成',
content: (
- 同步 {result.synced_count} 件,跳过 {result.skipped_count} 件
- 更新 {result.updated_count} 件,恢复 {result.reenabled_count} 件
+
+ 同步 {result.synced_count} 件,跳过 {result.skipped_count} 件
+
+
+ 更新 {result.updated_count} 件,恢复 {result.reenabled_count} 件
+
清理失效 {result.disabled_stale_count} 件
当前展示 {result.total_active} 件
@@ -552,9 +556,7 @@ export function AdminDistributionListingsPage() {
description="查看分站收到的账号资产,控制账号展示、上下架与站点加价。"
meta={
-
- 收货商品 {receivedQuery.data?.total ?? 0}
-
+ 收货商品 {receivedQuery.data?.total ?? 0}
账号展示 {accountRows.length}
{receivedQuery.isFetching || accountsQuery.isFetching ? (
正在更新
@@ -564,11 +566,7 @@ export function AdminDistributionListingsPage() {
extra={
{canManage ? (
- }
- disabled={!siteId}
- onClick={openSync}
- >
+ } disabled={!siteId} onClick={openSync}>
同步账号
) : null}
@@ -629,9 +627,7 @@ export function AdminDistributionListingsPage() {
loading={receivedQuery.isFetching}
extra={
- {receivedQuery.isPending
- ? '加载中'
- : `当前 ${receivedRows.length} 件`}
+ {receivedQuery.isPending ? '加载中' : `当前 ${receivedRows.length} 件`}
}
>
@@ -763,7 +759,9 @@ export function AdminDistributionListingsPage() {
) : (
- {accountsQuery.isPending ? '加载中' : `当前 ${filteredAccounts.length} 个账号`}
+ {accountsQuery.isPending
+ ? '加载中'
+ : `当前 ${filteredAccounts.length} 个账号`}
)
}
@@ -837,7 +835,9 @@ export function AdminDistributionListingsPage() {
(working ? undefined : setMarkupTarget(undefined))}
footer={
diff --git a/frontend/src/features/admin/AdminDistributionOrdersPage.tsx b/frontend/src/features/admin/AdminDistributionOrdersPage.tsx
index 52e314c..599abce 100644
--- a/frontend/src/features/admin/AdminDistributionOrdersPage.tsx
+++ b/frontend/src/features/admin/AdminDistributionOrdersPage.tsx
@@ -1,27 +1,12 @@
import { useMemo, useState } from 'react'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
-import {
- Alert,
- Button,
- Input,
- Select,
- Space,
- Tag,
- Typography,
- type TableColumnsType,
-} from 'antd'
+import { Alert, Button, Input, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
import { ReloadOutlined, ShareAltOutlined, WalletOutlined } from '@ant-design/icons'
-import {
- fetchAdminDistributionOrder,
- fetchAdminDistributionOrders,
-} from '@/features/admin/api'
+import { fetchAdminDistributionOrder, fetchAdminDistributionOrders } from '@/features/admin/api'
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
import { DataTable } from '@/features/admin/components/DataTable'
-import {
- DetailDrawer,
- type DetailSection,
-} from '@/features/admin/components/DetailDrawer'
+import { DetailDrawer, type DetailSection } from '@/features/admin/components/DetailDrawer'
import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
import { StatusTag } from '@/features/admin/components/StatusTag'
import {
@@ -31,10 +16,7 @@ import {
refundStatusMap,
type StatusMeta,
} from '@/features/admin/labels'
-import type {
- AdminDistributionOrder,
- AdminDistributionOrderQuery,
-} from '@/features/admin/types'
+import type { AdminDistributionOrder, AdminDistributionOrderQuery } from '@/features/admin/types'
import { useAdminSessionStore } from '@/stores/adminSession'
import { useDistributionSiteSelection } from '@/features/admin/components/DistributionSiteSelect'
import { formatDateTime, formatMoney, formatText, maskPhone } from '@/shared/utils/format'
@@ -85,13 +67,29 @@ function buildSections(
title: '订单与站点',
icon: ,
fields: [
- { key: 'order_no', label: '订单号', value: {order.order_no} },
+ {
+ key: 'order_no',
+ label: '订单号',
+ value: {order.order_no},
+ },
{ key: 'site', label: '分销站点', value: siteName(order.site_id) },
- { key: 'mode', label: '分销模式', value: modeLabels[order.distribution_mode] ?? formatText(order.distribution_mode) },
+ {
+ key: 'mode',
+ label: '分销模式',
+ value: modeLabels[order.distribution_mode] ?? formatText(order.distribution_mode),
+ },
{ key: 'listing', label: '商品', value: formatText(order.title || order.listing_no) },
- { key: 'game', label: '游戏/区服', value: `${formatText(order.game_name)} · ${formatText(order.server_region)}` },
+ {
+ key: 'game',
+ label: '游戏/区服',
+ value: `${formatText(order.game_name)} · ${formatText(order.server_region)}`,
+ },
{ key: 'platform', label: '登录平台', value: formatText(order.login_platform) },
- { key: 'hours', label: '租用时长', value: order.rent_duration_hours ? `${order.rent_duration_hours} 小时` : '—' },
+ {
+ key: 'hours',
+ label: '租用时长',
+ value: order.rent_duration_hours ? `${order.rent_duration_hours} 小时` : '—',
+ },
{
key: 'renter',
label: '租客',
@@ -108,7 +106,11 @@ function buildSections(
fields: [
{ key: 'gross', label: '实付金额', value: formatMoney(order.gross_paid_amount_cent) },
{ key: 'refund', label: '退款金额', value: formatMoney(order.refund_amount_cent) },
- { key: 'net', label: '净付款', value: {formatMoney(order.net_paid_amount_cent)} },
+ {
+ key: 'net',
+ label: '净付款',
+ value: {formatMoney(order.net_paid_amount_cent)},
+ },
{ key: 'payment_fee', label: '支付手续费', value: formatMoney(order.payment_fee_cent) },
{ key: 'service_fee', label: '平台服务费', value: formatMoney(order.service_fee_cent) },
{ key: 'markup', label: '分站加价', value: formatMoney(order.markup_amount_cent) },
@@ -131,8 +133,16 @@ function buildSections(
value: {formatMoney(order.distributor_earning_cent)},
},
{ key: 'role', label: '分站角色', value: roleLabel(order.distributor_role) },
- { key: 'distributor_markup', label: '其中加价', value: formatMoney(order.distributor_markup_cent) },
- { key: 'distributor_split', label: '其中分成', value: formatMoney(order.distributor_split_cent) },
+ {
+ key: 'distributor_markup',
+ label: '其中加价',
+ value: formatMoney(order.distributor_markup_cent),
+ },
+ {
+ key: 'distributor_split',
+ label: '其中分成',
+ value: formatMoney(order.distributor_split_cent),
+ },
{
key: 'release',
label: '释放状态',
@@ -155,8 +165,16 @@ function buildSections(
},
{ key: 'settlement_status', label: '履约结算', value: formatText(order.settlement_status) },
{ key: 'handoff_status', label: '交接状态', value: formatText(order.handoff_status) },
- { key: 'order_id', label: '订单 ID', value: #{order.order_id} },
- { key: 'listing_no', label: '商品编号', value: {formatText(order.listing_no)} },
+ {
+ key: 'order_id',
+ label: '订单 ID',
+ value: #{order.order_id},
+ },
+ {
+ key: 'listing_no',
+ label: '商品编号',
+ value: {formatText(order.listing_no)},
+ },
],
},
]
@@ -262,7 +280,9 @@ export function AdminDistributionOrdersPage() {
align: 'right',
render: (_, row) => (
- {formatMoney(row.distributor_earning_cent)}
+
+ {formatMoney(row.distributor_earning_cent)}
+
加价 {formatMoney(row.distributor_markup_cent)}
分成 {formatMoney(row.distributor_split_cent)}
@@ -285,9 +305,7 @@ export function AdminDistributionOrdersPage() {
width: 150,
render: (_, row) => (
-
+
释放 {formatDateTime(row.release_at)}
),
@@ -464,8 +482,7 @@ export function AdminDistributionOrdersPage() {
subtitle={
detailQuery.data
? `${siteName(detailQuery.data.site_id)} · ${
- modeLabels[detailQuery.data.distribution_mode] ??
- detailQuery.data.distribution_mode
+ modeLabels[detailQuery.data.distribution_mode] ?? detailQuery.data.distribution_mode
}`
: undefined
}
@@ -479,7 +496,9 @@ export function AdminDistributionOrdersPage() {
detailQuery.data.distributor_settlement_status
)}
/>
- 退款 {readStatusMeta(refundStatusMap, detailQuery.data.refund_status).label}
+
+ 退款 {readStatusMeta(refundStatusMap, detailQuery.data.refund_status).label}
+
) : undefined
}
diff --git a/frontend/src/features/admin/AdminDistributionPaymentChannelsPage.tsx b/frontend/src/features/admin/AdminDistributionPaymentChannelsPage.tsx
index 8f679c2..f7fdbe3 100644
--- a/frontend/src/features/admin/AdminDistributionPaymentChannelsPage.tsx
+++ b/frontend/src/features/admin/AdminDistributionPaymentChannelsPage.tsx
@@ -81,12 +81,9 @@ export function AdminDistributionPaymentChannelsPage() {
})
const channels = channelsQuery.data ?? []
- const configs = configsQuery.data ?? []
+ const configs = useMemo(() => configsQuery.data ?? [], [configsQuery.data])
- const configMap = useMemo(
- () => new Map(configs.map(config => [config.id, config])),
- [configs]
- )
+ const configMap = useMemo(() => new Map(configs.map(config => [config.id, config])), [configs])
const openBind = () => {
form.setFieldsValue({ config_id: undefined as unknown as number, priority: 0 })
@@ -148,7 +145,7 @@ export function AdminDistributionPaymentChannelsPage() {
{config?.name || `配置 #${row.config_id}`}
- {config ? providerLabels[config.provider] ?? config.provider : '未知服务商'}
+ {config ? (providerLabels[config.provider] ?? config.provider) : '未知服务商'}
{config?.pay_way ? ` · ${config.pay_way}` : ''}
配置 ID {row.config_id}
@@ -217,12 +214,7 @@ export function AdminDistributionPaymentChannelsPage() {
extra={
{canManage ? (
- }
- disabled={!siteId}
- onClick={openBind}
- >
+ } disabled={!siteId} onClick={openBind}>
绑定支付配置
) : null}
diff --git a/frontend/src/features/admin/AdminDistributionQrCodesPage.tsx b/frontend/src/features/admin/AdminDistributionQrCodesPage.tsx
index 4fea9a2..db456e7 100644
--- a/frontend/src/features/admin/AdminDistributionQrCodesPage.tsx
+++ b/frontend/src/features/admin/AdminDistributionQrCodesPage.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query'
import {
Alert,
@@ -47,10 +47,7 @@ import {
import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
import { StatusTag } from '@/features/admin/components/StatusTag'
import { readStatusMeta, type StatusMeta } from '@/features/admin/labels'
-import type {
- AdminDistributionQrCode,
- AdminDistributionQrCodeQuery,
-} from '@/features/admin/types'
+import type { AdminDistributionQrCode, AdminDistributionQrCodeQuery } from '@/features/admin/types'
import { useAdminSessionStore } from '@/stores/adminSession'
import { uploadAdminFile } from '@/shared/api/files'
import { formatDateTime, formatText } from '@/shared/utils/format'
@@ -377,128 +374,124 @@ export function AdminDistributionQrCodesPage() {
}
}
- const columns = useMemo>(
- () => [
- {
- title: '二维码',
- dataIndex: 'image_url',
- width: 90,
- render: value => (
-
- ),
- },
- {
- title: '分组 / 备注',
- key: 'group',
- width: 220,
- render: (_, row) => (
-
- {formatText(row.group_name)}
- {formatText(row.note)}
- ID #{row.id}
-
- ),
- },
- {
- title: '状态',
- dataIndex: 'status',
- width: 100,
- render: value => ,
- },
- {
- title: '绑定群',
- key: 'bound',
- width: 200,
- render: (_, row) => (
-
-
- {row.conversation_id
- ? row.bound_conversation_title || `会话 #${row.conversation_id}`
- : '未绑定'}
-
- 企微改名:{row.wecom_renamed ? '是' : '否'}
-
- ),
- },
- {
- title: '有效期',
- key: 'expire',
- width: 190,
- render: (_, row) => (
-
- 过期 {formatDateTime(row.expires_at)}
- 发放 {formatDateTime(row.used_at)}
-
- ),
- },
- {
- title: '创建时间',
- dataIndex: 'created_at',
- width: 170,
- render: value => formatDateTime(value),
- },
- {
- title: '操作',
- key: 'actions',
- width: 220,
- fixed: 'right',
- render: (_, row) => (
-
- {row.status === 'unused' ? (
- }
- disabled={!canManage}
- onClick={() => changeStatus(row, 'disabled')}
- >
- 停用
-
- ) : null}
- {row.status === 'disabled' ? (
- }
- disabled={!canManage}
- onClick={() => changeStatus(row, 'unused')}
- >
- 启用
-
- ) : null}
- }
- disabled={!canManage}
- onClick={() => openEdit(row)}
- >
- 编辑
-
+ const columns: TableColumnsType = [
+ {
+ title: '二维码',
+ dataIndex: 'image_url',
+ width: 90,
+ render: value => (
+
+ ),
+ },
+ {
+ title: '分组 / 备注',
+ key: 'group',
+ width: 220,
+ render: (_, row) => (
+
+ {formatText(row.group_name)}
+ {formatText(row.note)}
+ ID #{row.id}
+
+ ),
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ width: 100,
+ render: value => ,
+ },
+ {
+ title: '绑定群',
+ key: 'bound',
+ width: 200,
+ render: (_, row) => (
+
+
+ {row.conversation_id
+ ? row.bound_conversation_title || `会话 #${row.conversation_id}`
+ : '未绑定'}
+
+ 企微改名:{row.wecom_renamed ? '是' : '否'}
+
+ ),
+ },
+ {
+ title: '有效期',
+ key: 'expire',
+ width: 190,
+ render: (_, row) => (
+
+ 过期 {formatDateTime(row.expires_at)}
+ 发放 {formatDateTime(row.used_at)}
+
+ ),
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'created_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+ {
+ title: '操作',
+ key: 'actions',
+ width: 220,
+ fixed: 'right',
+ render: (_, row) => (
+
+ {row.status === 'unused' ? (
}
+ icon={}
disabled={!canManage}
- onClick={() => removeOne(row)}
+ onClick={() => changeStatus(row, 'disabled')}
>
- 删除
+ 停用
-
- ),
- },
- ],
- // 操作回调依赖当前站点与权限,站点切换时需要重建列。
- [canManage, siteId]
- )
+ ) : null}
+ {row.status === 'disabled' ? (
+ }
+ disabled={!canManage}
+ onClick={() => changeStatus(row, 'unused')}
+ >
+ 启用
+
+ ) : null}
+ }
+ disabled={!canManage}
+ onClick={() => openEdit(row)}
+ >
+ 编辑
+
+ }
+ disabled={!canManage}
+ onClick={() => removeOne(row)}
+ >
+ 删除
+
+
+ ),
+ },
+ ]
return (
@@ -617,9 +610,7 @@ export function AdminDistributionQrCodesPage() {
value={draftFilters.expire_status}
placeholder="全部"
options={expireOptions}
- onChange={value =>
- setDraftFilters(current => ({ ...current, expire_status: value }))
- }
+ onChange={value => setDraftFilters(current => ({ ...current, expire_status: value }))}
/>
diff --git a/frontend/src/features/admin/AdminDistributionRulesPage.tsx b/frontend/src/features/admin/AdminDistributionRulesPage.tsx
index 4b3c4af..072d407 100644
--- a/frontend/src/features/admin/AdminDistributionRulesPage.tsx
+++ b/frontend/src/features/admin/AdminDistributionRulesPage.tsx
@@ -191,7 +191,7 @@ export function AdminDistributionRulesPage() {
const scope = Form.useWatch('scope', form) ?? 'global'
const mode = Form.useWatch('mode', form) ?? 'two_party_distributor_receive'
const serviceFeeUnit = Form.useWatch('service_fee_unit', form) ?? 'percent'
- const rows = rulesQuery.data ?? []
+ const rows = useMemo(() => rulesQuery.data ?? [], [rulesQuery.data])
const siteName = (siteId?: number | null) => {
if (!siteId) return '全局默认'
diff --git a/frontend/src/features/admin/AdminDistributionSettlementsPage.tsx b/frontend/src/features/admin/AdminDistributionSettlementsPage.tsx
index 2a9e7df..502de32 100644
--- a/frontend/src/features/admin/AdminDistributionSettlementsPage.tsx
+++ b/frontend/src/features/admin/AdminDistributionSettlementsPage.tsx
@@ -104,7 +104,7 @@ export function AdminDistributionSettlementsPage() {
enabled: canView && Boolean(itemsBatch?.id),
})
- const batches = batchesQuery.data ?? []
+ const batches = useMemo(() => batchesQuery.data ?? [], [batchesQuery.data])
const summary = useMemo(() => {
const pending = batches.filter(
@@ -320,11 +320,7 @@ export function AdminDistributionSettlementsPage() {
dataIndex: 'status',
width: 110,
render: value =>
- value === 'settled' ? (
-
已入账
- ) : (
-
待入账
- ),
+ value === 'settled' ?
已入账 :
待入账,
},
]
@@ -470,9 +466,7 @@ export function AdminDistributionSettlementsPage() {
title={itemsBatch ? `批次明细 · ${itemsBatch.batch_no}` : '批次明细'}
width={760}
onCancel={() => setItemsBatch(undefined)}
- footer={
-
setItemsBatch(undefined)}>关闭
- }
+ footer={
setItemsBatch(undefined)}>关闭}
>
diff --git a/frontend/src/features/admin/AdminDistributionSitesPage.tsx b/frontend/src/features/admin/AdminDistributionSitesPage.tsx
index bb2158f..6c7c6c4 100644
--- a/frontend/src/features/admin/AdminDistributionSitesPage.tsx
+++ b/frontend/src/features/admin/AdminDistributionSitesPage.tsx
@@ -95,7 +95,11 @@ function buildSections(site: AdminDistributionSite): DetailSection[] {
icon: ,
fields: [
{ key: 'name', label: '站点名称', value: site.site_name },
- { key: 'code', label: '站点标识', value: {site.site_code} },
+ {
+ key: 'code',
+ label: '站点标识',
+ value: {site.site_code},
+ },
{
key: 'domain',
label: '专属域名',
@@ -146,7 +150,7 @@ function buildSections(site: AdminDistributionSite): DetailSection[] {
key: 'member_type',
label: '成员角色',
value: site.member_type
- ? distributionMemberTypeMap[site.member_type] ?? site.member_type
+ ? (distributionMemberTypeMap[site.member_type] ?? site.member_type)
: '平台全量可见',
},
{
@@ -263,7 +267,7 @@ function buildColumns(
{adminName(row)}
{row.member_type
- ? distributionMemberTypeMap[row.member_type] ?? row.member_type
+ ? (distributionMemberTypeMap[row.member_type] ?? row.member_type)
: '平台账号'}
@@ -344,8 +348,8 @@ export function AdminDistributionSitesPage() {
const permissions = useAdminSessionStore(state => state.admin?.permissions)
const canView = Boolean(
permissions?.includes('*') ||
- permissions?.includes('distribution:site_view') ||
- permissions?.includes('qrcode:view')
+ permissions?.includes('distribution:site_view') ||
+ permissions?.includes('qrcode:view')
)
const canManage = Boolean(
permissions?.includes('*') || permissions?.includes('distribution:site_manage')
@@ -357,7 +361,7 @@ export function AdminDistributionSitesPage() {
enabled: canView,
})
- const sites = data ?? []
+ const sites = useMemo(() => data ?? [], [data])
const filtered = useMemo(() => {
const text = keyword.trim().toLowerCase()
@@ -481,10 +485,7 @@ export function AdminDistributionSitesPage() {
})
}
- const columns = useMemo(
- () => buildColumns(openDetail, openEditor, canManage),
- [canManage]
- )
+ const columns = buildColumns(openDetail, openEditor, canManage)
const detailFooter = selected ? (
@@ -515,9 +516,7 @@ export function AdminDistributionSitesPage() {
meta={
共 {sites.length} 个站点
-
- 运营中 {sites.filter(item => item.status === 'active').length}
-
+ 运营中 {sites.filter(item => item.status === 'active').length}
{isFetching && !isPending ? 正在更新 : null}
}
diff --git a/frontend/src/features/admin/AdminDistributionUsersPage.tsx b/frontend/src/features/admin/AdminDistributionUsersPage.tsx
index 93617c3..7f4840c 100644
--- a/frontend/src/features/admin/AdminDistributionUsersPage.tsx
+++ b/frontend/src/features/admin/AdminDistributionUsersPage.tsx
@@ -16,32 +16,17 @@ import { ReloadOutlined, TeamOutlined, UserOutlined } from '@ant-design/icons'
import { fetchAdminDistributionUsers } from '@/features/admin/api'
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
import { DataTable } from '@/features/admin/components/DataTable'
-import {
- DetailDrawer,
- type DetailSection,
-} from '@/features/admin/components/DetailDrawer'
+import { DetailDrawer, type DetailSection } from '@/features/admin/components/DetailDrawer'
import {
DistributionSiteSelect,
useDistributionSiteSelection,
} from '@/features/admin/components/DistributionSiteSelect'
import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
import { StatusTag } from '@/features/admin/components/StatusTag'
-import {
- readStatusMeta,
- realnameStatusMap,
- userStatusMap,
-} from '@/features/admin/labels'
-import type {
- AdminDistributionUser,
- AdminDistributionUserQuery,
-} from '@/features/admin/types'
+import { readStatusMeta, realnameStatusMap, userStatusMap } from '@/features/admin/labels'
+import type { AdminDistributionUser, AdminDistributionUserQuery } from '@/features/admin/types'
import { useAdminSessionStore } from '@/stores/adminSession'
-import {
- formatDateTime,
- formatMoney,
- formatText,
- maskPhone,
-} from '@/shared/utils/format'
+import { formatDateTime, formatMoney, formatText, maskPhone } from '@/shared/utils/format'
const sourceLabels: Record = {
register: '本站注册',
@@ -69,7 +54,11 @@ function buildSections(user: AdminDistributionUser): DetailSection[] {
icon: ,
fields: [
{ key: 'id', label: '用户 ID', value: #{user.id} },
- { key: 'phone', label: '手机号', value: {formatText(user.phone)} },
+ {
+ key: 'phone',
+ label: '手机号',
+ value: {formatText(user.phone)},
+ },
{ key: 'nickname', label: '昵称', value: formatText(user.nickname) },
{
key: 'source',
@@ -161,9 +150,7 @@ export function AdminDistributionUsersPage() {
title: '来源',
dataIndex: 'source',
width: 130,
- render: value => (
- {sourceLabels[value] ?? formatText(value)}
- ),
+ render: value => {sourceLabels[value] ?? formatText(value)},
},
{
title: '实名 / 状态',
@@ -188,9 +175,7 @@ export function AdminDistributionUsersPage() {
dataIndex: 'total_spent_cent',
width: 140,
align: 'right',
- render: value => (
- {formatMoney(value)}
- ),
+ render: value => {formatMoney(value)},
},
{
title: '最近下单',
diff --git a/frontend/src/features/admin/AdminDistributionWalletPage.tsx b/frontend/src/features/admin/AdminDistributionWalletPage.tsx
index bc5089b..449b933 100644
--- a/frontend/src/features/admin/AdminDistributionWalletPage.tsx
+++ b/frontend/src/features/admin/AdminDistributionWalletPage.tsx
@@ -384,12 +384,7 @@ export function AdminDistributionWalletPage() {
},
]}
>
-
+
},
- { key: 'fee', label: '手续费', value: {formatMoney(item.fee_cent)} },
+ {
+ key: 'amount',
+ label: '申请金额',
+ value: {formatMoney(item.amount_cent)},
+ },
+ {
+ key: 'fee',
+ label: '手续费',
+ value: {formatMoney(item.fee_cent)},
+ },
{
key: 'actual',
label: '实际出款',
@@ -320,7 +324,12 @@ function buildSections(item: FinanceDisbursementItem): DetailSection[] {
? [
{ key: 'voided_at', label: '作废时间', value: formatDateTime(item.voided_at) },
{ key: 'voided_by', label: '作废人', value: formatText(item.voided_by_name) },
- { key: 'void_reason', label: '作废原因', span: 2 as const, value: formatText(item.void_reason) },
+ {
+ key: 'void_reason',
+ label: '作废原因',
+ span: 2 as const,
+ value: formatText(item.void_reason),
+ },
]
: []),
],
@@ -362,11 +371,11 @@ export function AdminFinanceDisbursementsPage() {
const defaultRange = defaultFilters().range
const hasActiveFilters = Boolean(
draft.source_type ||
- draft.status ||
- draft.keyword ||
- draft.date_type !== 'created' ||
- draft.range[0].format('YYYY-MM-DD') !== defaultRange[0].format('YYYY-MM-DD') ||
- draft.range[1].format('YYYY-MM-DD') !== defaultRange[1].format('YYYY-MM-DD')
+ draft.status ||
+ draft.keyword ||
+ draft.date_type !== 'created' ||
+ draft.range[0].format('YYYY-MM-DD') !== defaultRange[0].format('YYYY-MM-DD') ||
+ draft.range[1].format('YYYY-MM-DD') !== defaultRange[1].format('YYYY-MM-DD')
)
const applyFilters = () => {
@@ -576,7 +585,11 @@ export function AdminFinanceDisbursementsPage() {
-
+
@@ -533,9 +537,7 @@ export function AdminManualEntriesPage() {
open={optionsOpen}
title="录单选项管理"
onCancel={() => setOptionsOpen(false)}
- footer={
- setOptionsOpen(false)}>关闭
- }
+ footer={ setOptionsOpen(false)}>关闭}
width={640}
>
void
-): TableColumnsType => [
+const columns = (onRead: (row: AdminNotification) => void): TableColumnsType => [
{
title: '标题',
key: 'title',
@@ -79,8 +77,7 @@ export function AdminNotificationsPage() {
const rows = notificationsQuery.data?.items ?? []
- const invalidate = () =>
- queryClient.invalidateQueries({ queryKey: ['admin-notifications'] })
+ const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin-notifications'] })
if (!canView) {
return (
diff --git a/frontend/src/features/admin/AdminOperatingExpensesPage.tsx b/frontend/src/features/admin/AdminOperatingExpensesPage.tsx
index e158b46..9bc6688 100644
--- a/frontend/src/features/admin/AdminOperatingExpensesPage.tsx
+++ b/frontend/src/features/admin/AdminOperatingExpensesPage.tsx
@@ -279,11 +279,11 @@ export function AdminOperatingExpensesPage() {
const defaultRange = defaultFilters().range
const hasActiveFilters = Boolean(
draft.status ||
- draft.category ||
- draft.keyword ||
- draft.date_type !== 'created' ||
- draft.range[0].format('YYYY-MM-DD') !== defaultRange[0].format('YYYY-MM-DD') ||
- draft.range[1].format('YYYY-MM-DD') !== defaultRange[1].format('YYYY-MM-DD')
+ draft.category ||
+ draft.keyword ||
+ draft.date_type !== 'created' ||
+ draft.range[0].format('YYYY-MM-DD') !== defaultRange[0].format('YYYY-MM-DD') ||
+ draft.range[1].format('YYYY-MM-DD') !== defaultRange[1].format('YYYY-MM-DD')
)
const applyFilters = () => {
@@ -467,7 +467,11 @@ export function AdminOperatingExpensesPage() {
-
+
@@ -561,7 +563,9 @@ export function AdminOperatingExpensesPage() {
open={detailOpen}
onClose={() => setDetailOpen(false)}
title={selected ? `开支详情 · ${selected.expense_no}` : '开支详情'}
- subtitle={selected ? `${selected.category} · ${formatDateTime(selected.occurred_at)}` : undefined}
+ subtitle={
+ selected ? `${selected.category} · ${formatDateTime(selected.occurred_at)}` : undefined
+ }
tags={
selected ? (
@@ -650,7 +654,12 @@ export function AdminOperatingExpensesPage() {
label="开支用途 / 详细备注"
rules={[{ required: true, message: '请填写开支用途' }]}
>
-
+
diff --git a/frontend/src/features/admin/AdminOrderDetailDrawer.tsx b/frontend/src/features/admin/AdminOrderDetailDrawer.tsx
index e521ac4..d05a558 100644
--- a/frontend/src/features/admin/AdminOrderDetailDrawer.tsx
+++ b/frontend/src/features/admin/AdminOrderDetailDrawer.tsx
@@ -32,7 +32,11 @@ function buildSections(order: AdminOrderDetail): DetailSection[] {
key: 'overview',
title: '订单概览',
fields: [
- { key: 'order_no', label: '订单号', value: {order.order_no} },
+ {
+ key: 'order_no',
+ label: '订单号',
+ value: {order.order_no},
+ },
{
key: 'status',
label: '订单状态',
@@ -40,7 +44,11 @@ function buildSections(order: AdminOrderDetail): DetailSection[] {
},
{ key: 'created_at', label: '创建时间', value: formatDateTime(order.created_at) },
{ key: 'updated_at', label: '更新时间', value: formatDateTime(order.updated_at) },
- { key: 'payment_deadline', label: '支付截止', value: formatDateTime(order.payment_deadline_at) },
+ {
+ key: 'payment_deadline',
+ label: '支付截止',
+ value: formatDateTime(order.payment_deadline_at),
+ },
{ key: 'listing_no', label: '商品编号', value: formatText(order.listing_no) },
],
},
@@ -55,7 +63,9 @@ function buildSections(order: AdminOrderDetail): DetailSection[] {
{
key: 'duration',
label: '预计租期',
- value: order.estimated_duration_hours ? `${order.estimated_duration_hours} 小时` : emptyText,
+ value: order.estimated_duration_hours
+ ? `${order.estimated_duration_hours} 小时`
+ : emptyText,
},
{ key: 'estimated_end', label: '预计结束', value: formatDateTime(order.estimated_end_at) },
{ key: 'rented_at', label: '起租时间', value: formatDateTime(order.rented_at) },
@@ -103,8 +113,16 @@ function buildSections(order: AdminOrderDetail): DetailSection[] {
{ key: 'owner_rent', label: '号主租金', value: formatMoney(order.owner_rent_amount_cent) },
{ key: 'platform_fee', label: '平台服务费', value: formatMoney(order.platform_fee_cent) },
{ key: 'deposit', label: '实收押金', value: formatMoney(order.deposit_amount_cent) },
- { key: 'deposit_original', label: '原始押金', value: formatMoney(order.deposit_original_amount_cent) },
- { key: 'deposit_waived', label: '减免押金', value: formatMoney(order.deposit_waived_amount_cent) },
+ {
+ key: 'deposit_original',
+ label: '原始押金',
+ value: formatMoney(order.deposit_original_amount_cent),
+ },
+ {
+ key: 'deposit_waived',
+ label: '减免押金',
+ value: formatMoney(order.deposit_waived_amount_cent),
+ },
{
key: 'refund',
label: '退款',
@@ -126,7 +144,11 @@ function buildSections(order: AdminOrderDetail): DetailSection[] {
label: '订单类型',
value: ,
},
- { key: 'handoff_status', label: '交接状态', value: },
+ {
+ key: 'handoff_status',
+ label: '交接状态',
+ value: ,
+ },
{
key: 'settlement_mode',
label: '结算方式',
@@ -149,9 +171,22 @@ function buildSections(order: AdminOrderDetail): DetailSection[] {
/>
),
},
- { key: 'offline_amount', label: '线下结算金额', value: formatMoney(order.offline_settlement_amount_cent) },
- { key: 'offline_remark', label: '线下结算备注', value: formatText(order.offline_settlement_remark), span: 2 },
- { key: 'offline_at', label: '线下结算时间', value: formatDateTime(order.offline_settled_at) },
+ {
+ key: 'offline_amount',
+ label: '线下结算金额',
+ value: formatMoney(order.offline_settlement_amount_cent),
+ },
+ {
+ key: 'offline_remark',
+ label: '线下结算备注',
+ value: formatText(order.offline_settlement_remark),
+ span: 2,
+ },
+ {
+ key: 'offline_at',
+ label: '线下结算时间',
+ value: formatDateTime(order.offline_settled_at),
+ },
{
key: 'deposit_hold',
label: '押金冻结',
@@ -162,7 +197,12 @@ function buildSections(order: AdminOrderDetail): DetailSection[] {
),
},
- { key: 'deposit_hold_reason', label: '冻结原因', value: formatText(order.deposit_hold_reason), span: 2 },
+ {
+ key: 'deposit_hold_reason',
+ label: '冻结原因',
+ value: formatText(order.deposit_hold_reason),
+ span: 2,
+ },
],
},
]
@@ -183,12 +223,32 @@ function buildSections(order: AdminOrderDetail): DetailSection[] {
label: '协商轮次',
value: `${checkout.round_count} / ${checkout.max_rounds}`,
},
- { key: 'checkout_consumable', label: '消耗品费用', value: formatMoney(checkout.consumable_amount_cent) },
+ {
+ key: 'checkout_consumable',
+ label: '消耗品费用',
+ value: formatMoney(checkout.consumable_amount_cent),
+ },
{ key: 'checkout_coin', label: '哈夫币消耗', value: `${checkout.coin_consumed_m} M` },
- { key: 'checkout_other', label: '其他费用', value: formatMoney(checkout.other_amount_cent) },
- { key: 'checkout_deduct', label: '押金扣除', value: formatMoney(checkout.deposit_deduct_amount_cent) },
- { key: 'checkout_renter_refund', label: '租客可退', value: formatMoney(checkout.renter_refund_amount_cent) },
- { key: 'checkout_owner_income', label: '号主到手', value: formatMoney(checkout.owner_income_amount_cent) },
+ {
+ key: 'checkout_other',
+ label: '其他费用',
+ value: formatMoney(checkout.other_amount_cent),
+ },
+ {
+ key: 'checkout_deduct',
+ label: '押金扣除',
+ value: formatMoney(checkout.deposit_deduct_amount_cent),
+ },
+ {
+ key: 'checkout_renter_refund',
+ label: '租客可退',
+ value: formatMoney(checkout.renter_refund_amount_cent),
+ },
+ {
+ key: 'checkout_owner_income',
+ label: '号主到手',
+ value: formatMoney(checkout.owner_income_amount_cent),
+ },
{
key: 'checkout_shortfall',
label: '差额',
diff --git a/frontend/src/features/admin/AdminOrdersPage.tsx b/frontend/src/features/admin/AdminOrdersPage.tsx
index bc805df..25cbeb8 100644
--- a/frontend/src/features/admin/AdminOrdersPage.tsx
+++ b/frontend/src/features/admin/AdminOrdersPage.tsx
@@ -197,9 +197,7 @@ export function AdminOrdersPage() {
setPage(1)
}
- const hasActiveFilters = Object.entries(draft).some(
- ([, value]) => value !== '' && value != null
- )
+ const hasActiveFilters = Object.entries(draft).some(([, value]) => value !== '' && value != null)
const errorText = error instanceof Error ? error.message : '请稍后重试或重新登录后台。'
@@ -212,9 +210,7 @@ export function AdminOrdersPage() {
meta={
共 {data?.total ?? 0} 单
- {applied.user_id ? (
- 筛选用户 #{applied.user_id}
- ) : null}
+ {applied.user_id ? 筛选用户 #{applied.user_id} : null}
{isFetching && !isPending ? 正在更新 : null}
}
diff --git a/frontend/src/features/admin/AdminPaymentConfigsPage.tsx b/frontend/src/features/admin/AdminPaymentConfigsPage.tsx
index bb3b239..d773062 100644
--- a/frontend/src/features/admin/AdminPaymentConfigsPage.tsx
+++ b/frontend/src/features/admin/AdminPaymentConfigsPage.tsx
@@ -111,7 +111,9 @@ export function AdminPaymentConfigsPage() {
const [editing, setEditing] = useState
()
const [submitting, setSubmitting] = useState(false)
const permissions = useAdminSessionStore(state => state.admin?.permissions)
- const canView = Boolean(permissions?.includes('*') || permissions?.includes('payment_config:list'))
+ const canView = Boolean(
+ permissions?.includes('*') || permissions?.includes('payment_config:list')
+ )
const canCreate = Boolean(
permissions?.includes('*') || permissions?.includes('payment_config:create')
)
@@ -130,7 +132,7 @@ export function AdminPaymentConfigsPage() {
})
const data = listQuery.data
- const rows = data?.items ?? []
+ const rows = useMemo(() => data?.items ?? [], [data?.items])
const summary = useMemo(
() => ({
@@ -182,9 +184,7 @@ export function AdminPaymentConfigsPage() {
status: row.status,
environment: row.environment,
business_tags_text: (row.business_tags ?? []).join(', '),
- extra_config_json: row.extra_config
- ? JSON.stringify(row.extra_config, null, 2)
- : '',
+ extra_config_json: row.extra_config ? JSON.stringify(row.extra_config, null, 2) : '',
})
setEditorOpen(true)
}
@@ -194,7 +194,9 @@ export function AdminPaymentConfigsPage() {
try {
extraConfig = parseExtraConfig(values.extra_config_json)
} catch (error) {
- throw new Error(error instanceof Error ? error.message : '额外配置 JSON 不正确')
+ throw new Error(error instanceof Error ? error.message : '额外配置 JSON 不正确', {
+ cause: error,
+ })
}
const payload: AdminPaymentConfigPayload = {
name: values.name.trim(),
@@ -297,7 +299,11 @@ export function AdminPaymentConfigsPage() {
width: 150,
render: (_, row) => (
-
+
{statusLabels[row.status] ?? row.status}
{row.environment === 'production' ? '生产' : '沙箱'}
@@ -583,12 +589,20 @@ export function AdminPaymentConfigsPage() {
-
+
-
+
@@ -641,7 +655,10 @@ export function AdminPaymentConfigsPage() {
label="额外配置 JSON"
extra="拉卡拉需 app_id / serial_no / term_no;顺诚需 secret_id"
>
-
+
支付配置变更会记录审计日志;密钥不会回显。
diff --git a/frontend/src/features/admin/AdminPaymentsPage.tsx b/frontend/src/features/admin/AdminPaymentsPage.tsx
index c6ae0d8..feeb488 100644
--- a/frontend/src/features/admin/AdminPaymentsPage.tsx
+++ b/frontend/src/features/admin/AdminPaymentsPage.tsx
@@ -13,7 +13,12 @@ import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
type Filters = Pick
-const emptyFilters: Filters = { order_no: '', biz_type: undefined, status: undefined, provider: undefined }
+const emptyFilters: Filters = {
+ order_no: '',
+ biz_type: undefined,
+ status: undefined,
+ provider: undefined,
+}
const statusOptions = [
{ value: 'pending', label: '待支付' },
diff --git a/frontend/src/features/admin/AdminPickupDetailPage.tsx b/frontend/src/features/admin/AdminPickupDetailPage.tsx
index 061529e..876609a 100644
--- a/frontend/src/features/admin/AdminPickupDetailPage.tsx
+++ b/frontend/src/features/admin/AdminPickupDetailPage.tsx
@@ -34,7 +34,11 @@ import {
} from '@/features/admin/api'
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
import type { AdminPickupFinancialAdjustment } from '@/features/admin/types'
-import { offlineSettlementMeta, pickupSourceLabel, pickupStatusMeta } from '@/features/admin/AdminPickupsPage'
+import {
+ offlineSettlementMeta,
+ pickupSourceLabel,
+ pickupStatusMeta,
+} from '@/features/admin/AdminPickupsPage'
import { useAdminSessionStore } from '@/stores/adminSession'
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
@@ -86,12 +90,20 @@ export function AdminPickupDetailPage() {
const navigate = useNavigate()
const { message } = App.useApp()
const queryClient = useQueryClient()
- const [completeForm] = Form.useForm<{ settle_amount?: number; profit_amount?: number; remark?: string }>()
+ const [completeForm] = Form.useForm<{
+ settle_amount?: number
+ profit_amount?: number
+ remark?: string
+ }>()
const [cancelForm] = Form.useForm<{ reason: string }>()
const [settleForm] = Form.useForm<{ remark?: string }>()
const [profitForm] = Form.useForm<{ profit_amount?: number; reason?: string }>()
const [thirdPartyForm] = Form.useForm<{ third_party_order_no: string }>()
- const [adjustForm] = Form.useForm<{ profit_amount?: number; settle_amount?: number; reason: string }>()
+ const [adjustForm] = Form.useForm<{
+ profit_amount?: number
+ settle_amount?: number
+ reason: string
+ }>()
const [completeOpen, setCompleteOpen] = useState(false)
const [cancelOpen, setCancelOpen] = useState(false)
const [settleOpen, setSettleOpen] = useState(false)
@@ -220,16 +232,18 @@ export function AdminPickupDetailPage() {
>
完成提号
- {
- cancelForm.setFieldsValue({ reason: '' })
- setCancelOpen(true)
- }}>
+ {
+ cancelForm.setFieldsValue({ reason: '' })
+ setCancelOpen(true)
+ }}
+ >
取消提号
>
) : null}
- {pickup?.status === 'completed' &&
- pickup.offline_settlement_status === 'pending' ? (
+ {pickup?.status === 'completed' && pickup.offline_settlement_status === 'pending' ? (
{
@@ -248,7 +262,9 @@ export function AdminPickupDetailPage() {
{formatText(pickup?.pickup_no)}
{formatText(pickup?.listing_no)}
- {formatText(pickup?.account_title)}
+
+ {formatText(pickup?.account_title)}
+
{formatText(pickup?.server_region)} · {formatText(pickup?.login_platform)}
@@ -274,8 +290,12 @@ export function AdminPickupDetailPage() {
- {formatText(pickup?.settlement_mode)}
- {formatDateTime(pickup?.created_at)}
+
+ {formatText(pickup?.settlement_mode)}
+
+
+ {formatDateTime(pickup?.created_at)}
+
{pickup?.completed_at ? formatDateTime(pickup.completed_at) : '—'}
@@ -456,7 +476,10 @@ export function AdminPickupDetailPage() {
onCancel={() => (submitting ? undefined : setCancelOpen(false))}
onOk={async () => {
const values = await cancelForm.validateFields()
- const ok = await run(() => cancelAdminPickup(pickupId, values.reason.trim()), '提号已取消')
+ const ok = await run(
+ () => cancelAdminPickup(pickupId, values.reason.trim()),
+ '提号已取消'
+ )
if (ok) setCancelOpen(false)
}}
>
@@ -565,9 +588,13 @@ export function AdminPickupDetailPage() {
onOk={async () => {
const values = await adjustForm.validateFields()
const profit =
- values.profit_amount == null ? undefined : Math.round(Number(values.profit_amount) * 100)
+ values.profit_amount == null
+ ? undefined
+ : Math.round(Number(values.profit_amount) * 100)
const settle =
- values.settle_amount == null ? undefined : Math.round(Number(values.settle_amount) * 100)
+ values.settle_amount == null
+ ? undefined
+ : Math.round(Number(values.settle_amount) * 100)
if (profit == null && settle == null) {
message.warning('至少填写利润或结算金额')
return
diff --git a/frontend/src/features/admin/AdminPickupsPage.tsx b/frontend/src/features/admin/AdminPickupsPage.tsx
index 9396343..34d8b69 100644
--- a/frontend/src/features/admin/AdminPickupsPage.tsx
+++ b/frontend/src/features/admin/AdminPickupsPage.tsx
@@ -33,7 +33,11 @@ import {
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
import { DataTable } from '@/features/admin/components/DataTable'
import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
-import type { AdminPickup, AdminPickupCreatePayload, AdminPickupQuery } from '@/features/admin/types'
+import type {
+ AdminPickup,
+ AdminPickupCreatePayload,
+ AdminPickupQuery,
+} from '@/features/admin/types'
import { useAdminSessionStore } from '@/stores/adminSession'
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
@@ -161,8 +165,7 @@ export function AdminPickupsPage() {
enabled: canManage && shopOpen,
})
- const invalidate = () =>
- queryClient.invalidateQueries({ queryKey: ['admin-pickups'] })
+ const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin-pickups'] })
const openCreate = () => {
setSourceType('')
@@ -298,11 +301,10 @@ export function AdminPickupsPage() {
width: 170,
render: (_, row) => {
const status = pickupStatusMeta[row.status] ?? { label: row.status, color: 'default' }
- const settlement =
- offlineSettlementMeta[row.offline_settlement_status] ?? {
- label: row.offline_settlement_status || '—',
- color: 'default',
- }
+ const settlement = offlineSettlementMeta[row.offline_settlement_status] ?? {
+ label: row.offline_settlement_status || '—',
+ color: 'default',
+ }
return (
@@ -401,9 +403,7 @@ export function AdminPickupsPage() {
allowClear
value={filters.keyword}
placeholder="提号编号 / 上架编号 / 账号标题 / 手机号"
- onChange={event =>
- setFilters(current => ({ ...current, keyword: event.target.value }))
- }
+ onChange={event => setFilters(current => ({ ...current, keyword: event.target.value }))}
/>
@@ -574,7 +574,7 @@ export function AdminPickupsPage() {
diff --git a/frontend/src/features/admin/AdminPushPage.tsx b/frontend/src/features/admin/AdminPushPage.tsx
index 4f138d5..77c379a 100644
--- a/frontend/src/features/admin/AdminPushPage.tsx
+++ b/frontend/src/features/admin/AdminPushPage.tsx
@@ -32,7 +32,9 @@ export function AdminPushPage() {
const [submitting, setSubmitting] = useState(false)
const permissions = useAdminSessionStore(state => state.admin?.permissions)
const canView = Boolean(permissions?.includes('*') || permissions?.includes('system_config:view'))
- const canManage = Boolean(permissions?.includes('*') || permissions?.includes('system_config:update'))
+ const canManage = Boolean(
+ permissions?.includes('*') || permissions?.includes('system_config:update')
+ )
const channelsQuery = useQuery({
queryKey: ['admin-push-channels'],
diff --git a/frontend/src/features/admin/AdminQrCodePoolPage.tsx b/frontend/src/features/admin/AdminQrCodePoolPage.tsx
index fdf5f49..c7c32b1 100644
--- a/frontend/src/features/admin/AdminQrCodePoolPage.tsx
+++ b/frontend/src/features/admin/AdminQrCodePoolPage.tsx
@@ -67,7 +67,8 @@ const statusMeta: Record = {
}
function errorText(error: unknown) {
- const response = (error as { response?: { data?: { message?: string; error?: string } } })?.response
+ const response = (error as { response?: { data?: { message?: string; error?: string } } })
+ ?.response
if (response?.data?.message) return response.data.message
if (response?.data?.error) return response.data.error
return error instanceof Error ? error.message : '请求失败,请稍后重试'
@@ -403,7 +404,12 @@ export function AdminQrCodePoolPage() {
>
批量导入
- } disabled={!canManage} onClick={openCreate}>
+ }
+ disabled={!canManage}
+ onClick={openCreate}
+ >
添加二维码
@@ -427,9 +433,7 @@ export function AdminQrCodePoolPage() {
allowClear
value={filters.keyword}
placeholder="群名 / 备注"
- onChange={event =>
- setFilters(current => ({ ...current, keyword: event.target.value }))
- }
+ onChange={event => setFilters(current => ({ ...current, keyword: event.target.value }))}
/>
@@ -526,9 +530,7 @@ export function AdminQrCodePoolPage() {
- {imageURL ? (
-
- ) : null}
+ {imageURL ? : null}
}>选择图片(可多选)
-
+
{batchFiles.map(item => (
diff --git a/frontend/src/features/admin/AdminQuickRepliesPage.tsx b/frontend/src/features/admin/AdminQuickRepliesPage.tsx
index 2853600..b7dd049 100644
--- a/frontend/src/features/admin/AdminQuickRepliesPage.tsx
+++ b/frontend/src/features/admin/AdminQuickRepliesPage.tsx
@@ -281,7 +281,12 @@ export function AdminQuickRepliesPage() {
{ max: 1000, message: '最多 1000 个字符' },
]}
>
-
+
diff --git a/frontend/src/features/admin/AdminRolesPage.tsx b/frontend/src/features/admin/AdminRolesPage.tsx
index 66625fa..2720df7 100644
--- a/frontend/src/features/admin/AdminRolesPage.tsx
+++ b/frontend/src/features/admin/AdminRolesPage.tsx
@@ -367,7 +367,11 @@ export function AdminRolesPage() {
setPermissionRole(undefined)} disabled={savingPermissions}>
取消
- void savePermissions()}>
+ void savePermissions()}
+ >
保存权限
@@ -396,9 +400,7 @@ export function AdminRolesPage() {
value={selectedPermissionIds}
onChange={values =>
setSelectedPermissionIds(current => {
- const other = current.filter(
- id => !items.some(item => item.id === id)
- )
+ const other = current.filter(id => !items.some(item => item.id === id))
return [...other, ...values.map(Number)]
})
}
diff --git a/frontend/src/features/admin/AdminSiteAnnouncementsPage.tsx b/frontend/src/features/admin/AdminSiteAnnouncementsPage.tsx
index 38833a5..6265a06 100644
--- a/frontend/src/features/admin/AdminSiteAnnouncementsPage.tsx
+++ b/frontend/src/features/admin/AdminSiteAnnouncementsPage.tsx
@@ -38,10 +38,7 @@ import {
} from '@/features/admin/api'
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
import { DataTable } from '@/features/admin/components/DataTable'
-import {
- DetailDrawer,
- type DetailSection,
-} from '@/features/admin/components/DetailDrawer'
+import { DetailDrawer, type DetailSection } from '@/features/admin/components/DetailDrawer'
import {
DistributionSiteSelect,
useDistributionSiteSelection,
@@ -55,10 +52,7 @@ import {
announcementStatusOptions,
readStatusMeta,
} from '@/features/admin/labels'
-import type {
- AdminSiteAnnouncement,
- AdminSiteAnnouncementPayload,
-} from '@/features/admin/types'
+import type { AdminSiteAnnouncement, AdminSiteAnnouncementPayload } from '@/features/admin/types'
import { useAdminSessionStore } from '@/stores/adminSession'
import { formatDateTime, formatText } from '@/shared/utils/format'
@@ -191,7 +185,10 @@ export function AdminSiteAnnouncementsPage() {
}
}
- const changeStatus = (item: AdminSiteAnnouncement, action: 'publish' | 'archive' | 'unarchive') => {
+ const changeStatus = (
+ item: AdminSiteAnnouncement,
+ action: 'publish' | 'archive' | 'unarchive'
+ ) => {
if (!siteId) return
const label = action === 'publish' ? '发布' : action === 'archive' ? '归档' : '取消归档'
modal.confirm({
@@ -249,7 +246,10 @@ export function AdminSiteAnnouncementsPage() {
setDetail(row)}>
{row.title}
- {row.content.slice(0, 80)}{row.content.length > 80 ? '…' : ''}
+
+ {row.content.slice(0, 80)}
+ {row.content.length > 80 ? '…' : ''}
+
{announcementCategoryMap[row.category] ?? row.category}
{row.is_pinned ? 置顶 : null}
diff --git a/frontend/src/features/admin/AdminStaffAccountsPage.tsx b/frontend/src/features/admin/AdminStaffAccountsPage.tsx
index 8971a2c..3299d16 100644
--- a/frontend/src/features/admin/AdminStaffAccountsPage.tsx
+++ b/frontend/src/features/admin/AdminStaffAccountsPage.tsx
@@ -88,7 +88,9 @@ export function AdminStaffAccountsPage() {
const [submitting, setSubmitting] = useState(false)
const currentAdminId = useAdminSessionStore(state => state.admin?.id)
const permissions = useAdminSessionStore(state => state.admin?.permissions)
- const canManage = Boolean(permissions?.includes('*') || permissions?.includes('admin_user:manage'))
+ const canManage = Boolean(
+ permissions?.includes('*') || permissions?.includes('admin_user:manage')
+ )
const accountsQuery = useQuery({
queryKey: ['admin-staff-accounts', page, pageSize],
@@ -109,8 +111,7 @@ export function AdminStaffAccountsPage() {
label: `${role.name}(${role.code})`,
}))
- const invalidate = () =>
- queryClient.invalidateQueries({ queryKey: ['admin-staff-accounts'] })
+ const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin-staff-accounts'] })
const openCreate = () => {
createForm.setFieldsValue({ username: '', password: '', nickname: '' })
@@ -262,7 +263,12 @@ export function AdminStaffAccountsPage() {
title: '说明',
key: 'description',
render: (_, row) =>
- formatText(row.roles.map(role => role.description).filter(Boolean).join(' / ')),
+ formatText(
+ row.roles
+ .map(role => role.description)
+ .filter(Boolean)
+ .join(' / ')
+ ),
},
{
title: '最后登录',
diff --git a/frontend/src/features/admin/AdminSupportGroupsPage.tsx b/frontend/src/features/admin/AdminSupportGroupsPage.tsx
index c9887be..e750f27 100644
--- a/frontend/src/features/admin/AdminSupportGroupsPage.tsx
+++ b/frontend/src/features/admin/AdminSupportGroupsPage.tsx
@@ -33,10 +33,7 @@ import {
} from '@/features/admin/api'
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
import { DataTable } from '@/features/admin/components/DataTable'
-import type {
- AdminSupportGroup,
- AdminSupportGroupPayload,
-} from '@/features/admin/types'
+import type { AdminSupportGroup, AdminSupportGroupPayload } from '@/features/admin/types'
import { useAdminSessionStore } from '@/stores/adminSession'
import { formatDateTime, formatText } from '@/shared/utils/format'
@@ -243,7 +240,9 @@ export function AdminSupportGroupsPage() {
meta={
共 {groups.length} 个分组
- 在线客服 {admins.filter(item => item.support_status === 'online').length}
+
+ 在线客服 {admins.filter(item => item.support_status === 'online').length}
+
}
extra={
diff --git a/frontend/src/features/admin/AdminSystemConfigsPage.tsx b/frontend/src/features/admin/AdminSystemConfigsPage.tsx
index df375dd..c2b0185 100644
--- a/frontend/src/features/admin/AdminSystemConfigsPage.tsx
+++ b/frontend/src/features/admin/AdminSystemConfigsPage.tsx
@@ -12,11 +12,7 @@ import {
Typography,
type TableColumnsType,
} from 'antd'
-import {
- EditOutlined,
- ReloadOutlined,
- SafetyCertificateOutlined,
-} from '@ant-design/icons'
+import { EditOutlined, ReloadOutlined, SafetyCertificateOutlined } from '@ant-design/icons'
import { fetchAdminSystemConfigs, updateAdminSystemConfig } from '@/features/admin/api'
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
@@ -194,7 +190,7 @@ export function AdminSystemConfigsPage() {
enabled: canView,
})
- const items = data ?? []
+ const items = useMemo(() => data ?? [], [data])
const filtered = useMemo(() => {
const text = keyword.trim().toLowerCase()
diff --git a/frontend/src/features/admin/AdminTrackingLinksPage.tsx b/frontend/src/features/admin/AdminTrackingLinksPage.tsx
index 5c25334..32b620d 100644
--- a/frontend/src/features/admin/AdminTrackingLinksPage.tsx
+++ b/frontend/src/features/admin/AdminTrackingLinksPage.tsx
@@ -86,7 +86,9 @@ export function AdminTrackingLinksPage() {
const [submitting, setSubmitting] = useState(false)
const permissions = useAdminSessionStore(state => state.admin?.permissions)
- const canManage = Boolean(permissions?.includes('*') || permissions?.includes('ad_tracking:manage'))
+ const canManage = Boolean(
+ permissions?.includes('*') || permissions?.includes('ad_tracking:manage')
+ )
const query = useMemo(
() => ({
@@ -232,7 +234,10 @@ export function AdminTrackingLinksPage() {
const next = link.status === 'active' ? 'disabled' : 'active'
modal.confirm({
title: next === 'active' ? `启用链接「${link.name}」?` : `停用链接「${link.name}」?`,
- content: next === 'active' ? '启用后短链会恢复跳转并继续统计。' : '停用后短链不再跳转,已有统计数据保留。',
+ content:
+ next === 'active'
+ ? '启用后短链会恢复跳转并继续统计。'
+ : '停用后短链不再跳转,已有统计数据保留。',
okText: next === 'active' ? '启用' : '停用',
okButtonProps: next === 'active' ? undefined : { danger: true },
cancelText: '取消',
@@ -295,7 +300,9 @@ export function AdminTrackingLinksPage() {
align: 'right',
render: (_, row) => (
- {row.click_pv} / {row.click_uv}
+
+ {row.click_pv} / {row.click_uv}
+
IP {row.click_ip}
),
@@ -307,7 +314,9 @@ export function AdminTrackingLinksPage() {
align: 'right',
render: (_, row) => (
-
{row.register_count} / {row.order_count}
+
+ {row.register_count} / {row.order_count}
+
转化 {(row.conversion_rate * 100).toFixed(1)}%
@@ -449,9 +458,7 @@ export function AdminTrackingLinksPage() {
allowClear
value={filters.keyword}
placeholder="名称 / 编码"
- onChange={event =>
- setFilters(current => ({ ...current, keyword: event.target.value }))
- }
+ onChange={event => setFilters(current => ({ ...current, keyword: event.target.value }))}
/>
@@ -460,9 +467,7 @@ export function AdminTrackingLinksPage() {
value={filters.channel_type}
options={channelOptions}
placeholder="全部渠道"
- onChange={value =>
- setFilters(current => ({ ...current, channel_type: value }))
- }
+ onChange={value => setFilters(current => ({ ...current, channel_type: value }))}
/>
@@ -522,11 +527,7 @@ export function AdminTrackingLinksPage() {
width={560}
>
+
= {
freeze: {
@@ -122,7 +106,11 @@ function buildSections(user: AdminUserItem): DetailSection[] {
key: 'deposit_free',
title: '免押额度',
fields: [
- { key: 'manual', label: '手动额度', value: formatMoney(user.deposit_free_manual_quota_cent) },
+ {
+ key: 'manual',
+ label: '手动额度',
+ value: formatMoney(user.deposit_free_manual_quota_cent),
+ },
{ key: 'level', label: '等级额度', value: formatMoney(user.deposit_free_level_quota_cent) },
{
key: 'effective',
diff --git a/frontend/src/features/admin/AdminUsersPage.tsx b/frontend/src/features/admin/AdminUsersPage.tsx
index 7b8f5ce..6066cb8 100644
--- a/frontend/src/features/admin/AdminUsersPage.tsx
+++ b/frontend/src/features/admin/AdminUsersPage.tsx
@@ -1,4 +1,4 @@
-import { useMemo, useState } from 'react'
+import { useState } from 'react'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { Alert, Button, Input, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
import { EyeOutlined, ReloadOutlined } from '@ant-design/icons'
@@ -159,7 +159,7 @@ export function AdminUsersPage() {
setDetailOpen(true)
}
- const columns = useMemo(() => buildColumns(openDetail, canView), [canView])
+ const columns = buildColumns(openDetail, canView)
const patchFilter = (patch: Partial) => {
setDraft(current => ({ ...current, ...patch }))
diff --git a/frontend/src/features/admin/AdminWalletLedgerPage.tsx b/frontend/src/features/admin/AdminWalletLedgerPage.tsx
index a2ea5e5..d887a17 100644
--- a/frontend/src/features/admin/AdminWalletLedgerPage.tsx
+++ b/frontend/src/features/admin/AdminWalletLedgerPage.tsx
@@ -100,7 +100,11 @@ const columns: TableColumnsType = [
]
export function AdminWalletLedgerPage() {
- const [filters, setFilters] = useState<{ user_id?: number; order_id?: number; biz_type?: string }>({})
+ const [filters, setFilters] = useState<{
+ user_id?: number
+ order_id?: number
+ biz_type?: string
+ }>({})
const [applied, setApplied] = useState(filters)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
@@ -178,7 +182,9 @@ export function AdminWalletLedgerPage() {
min={1}
style={{ width: '100%' }}
value={filters.order_id}
- onChange={value => setFilters(current => ({ ...current, order_id: value ?? undefined }))}
+ onChange={value =>
+ setFilters(current => ({ ...current, order_id: value ?? undefined }))
+ }
/>
diff --git a/frontend/src/features/admin/AdminWithdrawalsPage.tsx b/frontend/src/features/admin/AdminWithdrawalsPage.tsx
index 3a1f41a..a1c2218 100644
--- a/frontend/src/features/admin/AdminWithdrawalsPage.tsx
+++ b/frontend/src/features/admin/AdminWithdrawalsPage.tsx
@@ -9,7 +9,11 @@ import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
import { DataTable } from '@/features/admin/components/DataTable'
import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
import { StatusTag } from '@/features/admin/components/StatusTag'
-import { readStatusMeta, withdrawalStatusMap, withdrawalStatusOptions } from '@/features/admin/labels'
+import {
+ readStatusMeta,
+ withdrawalStatusMap,
+ withdrawalStatusOptions,
+} from '@/features/admin/labels'
import type { AdminWithdrawal, AdminWithdrawalQuery } from '@/features/admin/types'
import { useAdminSessionStore } from '@/stores/adminSession'
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
@@ -126,8 +130,8 @@ export function AdminWithdrawalsPage() {
const permissions = useAdminSessionStore(state => state.admin?.permissions)
const canView = Boolean(
permissions?.includes('*') ||
- permissions?.includes('withdrawal:list') ||
- permissions?.includes('withdrawal:detail')
+ permissions?.includes('withdrawal:list') ||
+ permissions?.includes('withdrawal:detail')
)
const { data, isPending, isFetching, isError, error, refetch } = useQuery({
@@ -136,10 +140,14 @@ export function AdminWithdrawalsPage() {
placeholderData: keepPreviousData,
})
- const columns = useMemo(() => buildColumns(row => {
- setDetailId(row.id)
- setDetailOpen(true)
- }, canView), [canView])
+ const columns = useMemo(
+ () =>
+ buildColumns(row => {
+ setDetailId(row.id)
+ setDetailOpen(true)
+ }, canView),
+ [canView]
+ )
const patchFilter = (patch: Partial) => {
setDraft(current => ({ ...current, ...patch }))
@@ -227,10 +235,14 @@ export function AdminWithdrawalsPage() {
setPage(nextPageSize !== pageSize ? 1 : nextPage)
setPageSize(nextPageSize)
}}
- onRowClick={canView ? row => {
- setDetailId(row.id)
- setDetailOpen(true)
- } : undefined}
+ onRowClick={
+ canView
+ ? row => {
+ setDetailId(row.id)
+ setDetailOpen(true)
+ }
+ : undefined
+ }
emptyDescription="没有匹配的提现申请"
/>
diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts
index 9472212..5bf0558 100644
--- a/frontend/src/features/admin/api.ts
+++ b/frontend/src/features/admin/api.ts
@@ -170,9 +170,10 @@ export async function fetchAdminMe() {
}
export async function updateAdminSupportStatus(status: 'online' | 'offline' | 'busy') {
- const { data } = await adminClient.put<
- ApiResponse<{ updated: boolean; support_status: string }>
- >('/admin/me/support-status', { status })
+ const { data } = await adminClient.put>(
+ '/admin/me/support-status',
+ { status }
+ )
return data.data
}
@@ -216,9 +217,12 @@ export async function fetchAdminOrder(id: number | string) {
}
export async function fetchAdminListings(query: AdminListingQuery) {
- const { data } = await adminClient.get>>('/admin/listings', {
- params: compactParams(query),
- })
+ const { data } = await adminClient.get>>(
+ '/admin/listings',
+ {
+ params: compactParams(query),
+ }
+ )
return {
items: data.data.items ?? [],
total: data.data.total ?? 0,
@@ -234,14 +238,15 @@ export async function fetchAdminListing(id: number | string) {
/** 审核队列使用独立接口:权限为 listing:approve,且返回全部待审商品。 */
export async function fetchAdminPendingListings() {
- const { data } = await adminClient.get>(
- '/admin/listings/pending'
- )
+ const { data } =
+ await adminClient.get>('/admin/listings/pending')
return data.data.items ?? []
}
export async function approveAdminListing(id: number | string) {
- const { data } = await adminClient.post>(`/admin/listings/${id}/approve`)
+ const { data } = await adminClient.post>(
+ `/admin/listings/${id}/approve`
+ )
return data.data
}
@@ -288,10 +293,9 @@ export async function fetchAdminUsers(query: AdminUserQuery) {
}
export async function freezeAdminUser(id: number | string, reason: string) {
- const { data } = await adminClient.post>(
- `/admin/users/${id}/freeze`,
- { reason }
- )
+ const { data } = await adminClient.post>(`/admin/users/${id}/freeze`, {
+ reason,
+ })
return data.data
}
@@ -382,9 +386,7 @@ export async function fetchAdminWithdrawals(query: AdminWithdrawalQuery) {
}
export async function fetchAdminWithdrawal(id: number | string) {
- const { data } = await adminClient.get>(
- `/admin/withdrawals/${id}`
- )
+ const { data } = await adminClient.get>(`/admin/withdrawals/${id}`)
return data.data
}
@@ -447,9 +449,12 @@ export interface FinanceRangeQuery {
}
export async function fetchFinanceDashboard(range: FinanceRangeQuery) {
- const { data } = await adminClient.get>('/admin/finance/dashboard', {
- params: compactParams(range),
- })
+ const { data } = await adminClient.get>(
+ '/admin/finance/dashboard',
+ {
+ params: compactParams(range),
+ }
+ )
return data.data
}
@@ -530,9 +535,12 @@ export async function deleteAdminAnnouncement(id: number | string) {
}
export async function fetchAdminAuditLogs(query: AdminAuditQuery) {
- const { data } = await adminClient.get>>('/admin/audit-logs', {
- params: compactParams(query),
- })
+ const { data } = await adminClient.get>>(
+ '/admin/audit-logs',
+ {
+ params: compactParams(query),
+ }
+ )
return {
items: data.data.items ?? [],
total: data.data.total ?? 0,
@@ -544,9 +552,8 @@ export async function fetchAdminAuditLogs(query: AdminAuditQuery) {
export type { AdminAuditLog }
export async function fetchAdminSystemConfigs() {
- const { data } = await adminClient.get>(
- '/admin/system-configs'
- )
+ const { data } =
+ await adminClient.get>('/admin/system-configs')
return data.data.items ?? []
}
@@ -706,10 +713,7 @@ export async function batchCreateAdminDistributionQrCodes(
return data.data
}
-export async function batchDeleteAdminDistributionQrCodes(
- id: number | string,
- ids: number[]
-) {
+export async function batchDeleteAdminDistributionQrCodes(id: number | string, ids: number[]) {
const { data } = await adminClient.post>(
`/admin/distribution/sites/${id}/qrcodes/batch-delete`,
{ ids }
@@ -906,10 +910,7 @@ export async function deleteAdminSiteAnnouncement(siteId: number, id: number) {
})
}
-export async function fetchAdminReceivedListings(
- siteId: number,
- query: AdminReceivedListingQuery
-) {
+export async function fetchAdminReceivedListings(siteId: number, query: AdminReceivedListingQuery) {
const { data } = await adminClient.get>>(
'/admin/distribution/received-listings',
{ params: compactParams({ ...query, site_id: siteId }) }
@@ -937,10 +938,7 @@ export async function fetchAdminAccountDisplays(siteId: number, status?: string)
return data.data.items ?? []
}
-export async function updateAdminAccountDisplay(
- id: number,
- displayStatus: 'enabled' | 'disabled'
-) {
+export async function updateAdminAccountDisplay(id: number, displayStatus: 'enabled' | 'disabled') {
const { data } = await adminClient.put>(
`/admin/distribution/accounts/${id}/display`,
{ display_status: displayStatus }
@@ -994,11 +992,7 @@ export async function fetchAdminPaymentChannels(siteId: number) {
return data.data.items ?? []
}
-export async function bindAdminPaymentChannel(
- siteId: number,
- configId: number,
- priority: number
-) {
+export async function bindAdminPaymentChannel(siteId: number, configId: number, priority: number) {
const { data } = await adminClient.post>(
'/admin/distribution/payment-channels',
{ config_id: configId, priority },
@@ -1012,9 +1006,10 @@ export async function unbindAdminPaymentChannel(id: number) {
}
export async function fetchAdminPaymentConfigOptions() {
- const { data } = await adminClient.get<
- ApiResponse<{ items?: AdminPaymentConfigOption[] }>
- >('/admin/payment-configs', { params: { page: 1, page_size: 100 } })
+ const { data } = await adminClient.get>(
+ '/admin/payment-configs',
+ { params: { page: 1, page_size: 100 } }
+ )
return data.data.items ?? []
}
@@ -1123,10 +1118,7 @@ export async function createAdminSupportGroup(payload: AdminSupportGroupPayload)
return data.data
}
-export async function updateAdminSupportGroup(
- id: number,
- payload: AdminSupportGroupPayload
-) {
+export async function updateAdminSupportGroup(id: number, payload: AdminSupportGroupPayload) {
const { data } = await adminClient.put>(
`/admin/chat-support-groups/${id}`,
payload
@@ -1158,9 +1150,7 @@ export async function fetchAdminStaffAccounts(query: AdminStaffAccountQuery = {}
}
export async function fetchAdminStaffAccount(id: number) {
- const { data } = await adminClient.get>(
- `/admin/admin-users/${id}`
- )
+ const { data } = await adminClient.get>(`/admin/admin-users/${id}`)
return data.data
}
@@ -1333,9 +1323,6 @@ export async function updateAdminTrackingLinkStatus(id: number, status: string)
return data.data
}
-
-
-
export async function fetchFinanceDisbursements(query: FinanceDisbursementQuery) {
const { data } = await adminClient.get>>(
'/admin/finance/disbursements',
@@ -1469,10 +1456,9 @@ export async function completeAdminPickup(
}
export async function cancelAdminPickup(id: number | string, reason: string) {
- const { data } = await adminClient.post>(
- `/admin/pickups/${id}/cancel`,
- { reason }
- )
+ const { data } = await adminClient.post>(`/admin/pickups/${id}/cancel`, {
+ reason,
+ })
return data.data
}
@@ -1574,10 +1560,7 @@ export async function createAdminPickupFinancialAdjustment(
return data.data
}
-export async function settleAdminPickupFinancialAdjustment(
- adjustmentId: number,
- remark: string
-) {
+export async function settleAdminPickupFinancialAdjustment(adjustmentId: number, remark: string) {
const { data } = await adminClient.post>(
`/admin/pickup-financial-adjustments/${adjustmentId}/settle`,
{ remark }
@@ -1698,10 +1681,9 @@ export async function createAdminQrCode(payload: AdminQrCodePayload) {
}
export async function batchCreateAdminQrCodes(items: AdminQrCodePayload[]) {
- const { data } = await adminClient.post<{ data: AdminQrCode[] }>(
- '/admin/chats/qrcodes/batch',
- { items }
- )
+ const { data } = await adminClient.post<{ data: AdminQrCode[] }>('/admin/chats/qrcodes/batch', {
+ items,
+ })
return data.data ?? []
}
@@ -1758,9 +1740,8 @@ export async function markAllAdminNotificationsRead() {
}
export async function fetchPushChannels() {
- const { data } = await adminClient.get>(
- '/admin/push-channels'
- )
+ const { data } =
+ await adminClient.get>('/admin/push-channels')
return data.data?.items ?? []
}
@@ -1799,9 +1780,8 @@ export async function testPushChannel(id: number) {
}
export async function fetchPushRules() {
- const { data } = await adminClient.get>(
- '/admin/push-rules'
- )
+ const { data } =
+ await adminClient.get>('/admin/push-rules')
return data.data?.items ?? []
}
@@ -1816,9 +1796,6 @@ export async function updatePushRule(
return data.data
}
-
-
-
export async function fetchAdminInvitationConfig(siteId?: number) {
const { data } = await adminClient.get>(
'/admin/invitations/configs',
@@ -1903,9 +1880,8 @@ export async function fetchAdminBackupStatus() {
}
export async function fetchAdminBackupSchedule() {
- const { data } = await adminClient.get>(
- '/admin/backups/schedule'
- )
+ const { data } =
+ await adminClient.get>('/admin/backups/schedule')
return data.data
}
@@ -1918,9 +1894,8 @@ export async function updateAdminBackupSchedule(payload: AdminBackupSchedule) {
}
export async function fetchCrashCategories() {
- const { data } = await adminClient.get>(
- '/admin/crash/categories'
- )
+ const { data } =
+ await adminClient.get>('/admin/crash/categories')
return data.data ?? []
}
@@ -2021,7 +1996,10 @@ export async function cancelCrashOrder(id: number, reason: string) {
await adminClient.post(`/admin/crash/orders/${id}/cancel`, { reason })
}
-export async function refundCrashOrder(id: number, payload: { reason?: string; amount_cent?: number }) {
+export async function refundCrashOrder(
+ id: number,
+ payload: { reason?: string; amount_cent?: number }
+) {
await adminClient.post(`/admin/crash/orders/${id}/refund`, payload)
}
diff --git a/frontend/src/features/admin/components/DataTable.tsx b/frontend/src/features/admin/components/DataTable.tsx
index 7299c24..4605bfe 100644
--- a/frontend/src/features/admin/components/DataTable.tsx
+++ b/frontend/src/features/admin/components/DataTable.tsx
@@ -41,7 +41,11 @@ export function DataTable({
className,
}: DataTableProps) {
return (
-
+
rowKey={rowKey}
size="middle"
diff --git a/frontend/src/features/admin/components/DistributionSiteSelect.tsx b/frontend/src/features/admin/components/DistributionSiteSelect.tsx
index 250226d..62ac631 100644
--- a/frontend/src/features/admin/components/DistributionSiteSelect.tsx
+++ b/frontend/src/features/admin/components/DistributionSiteSelect.tsx
@@ -25,7 +25,12 @@ export function useDistributionSiteSelection(
): DistributionSiteSelection {
const autoSelect = options.autoSelect ?? true
const [searchParams, setSearchParams] = useSearchParams()
- const { data = [], isPending, isError, refetch } = useQuery({
+ const {
+ data = [],
+ isPending,
+ isError,
+ refetch,
+ } = useQuery({
queryKey: ['admin-distribution-site-options'],
queryFn: fetchAdminDistributionSites,
staleTime: 60_000,
diff --git a/frontend/src/features/admin/components/JsonEditor.tsx b/frontend/src/features/admin/components/JsonEditor.tsx
index 6977332..311eb9f 100644
--- a/frontend/src/features/admin/components/JsonEditor.tsx
+++ b/frontend/src/features/admin/components/JsonEditor.tsx
@@ -1,12 +1,7 @@
import { useMemo, useRef, useState } from 'react'
import type { KeyboardEvent } from 'react'
import { Button, Space, Tag, Typography } from 'antd'
-import {
- CodeOutlined,
- CompressOutlined,
- CopyOutlined,
- UndoOutlined,
-} from '@ant-design/icons'
+import { CodeOutlined, CompressOutlined, CopyOutlined, UndoOutlined } from '@ant-design/icons'
export interface JsonParseResult {
valid: boolean
@@ -53,15 +48,7 @@ function scalarText(value: unknown) {
return String(value)
}
-function JsonNode({
- name,
- value,
- depth,
-}: {
- name: string
- value: unknown
- depth: number
-}) {
+function JsonNode({ name, value, depth }: { name: string; value: unknown; depth: number }) {
const [open, setOpen] = useState(depth < 1)
if (Array.isArray(value)) {
@@ -111,9 +98,7 @@ function JsonNode({
return (
{name}
-
- {scalarText(value)}
-
+ {scalarText(value)}
)
}
@@ -179,8 +164,7 @@ export function JsonEditor({
}
const previewValue = parsed.valid ? parsed.parsed : undefined
- const canPreview =
- parsed.valid && previewValue !== null && typeof previewValue === 'object'
+ const canPreview = parsed.valid && previewValue !== null && typeof previewValue === 'object'
return (
diff --git a/frontend/src/features/admin/components/PublishOptionsEditor.tsx b/frontend/src/features/admin/components/PublishOptionsEditor.tsx
index 2c32832..da12d6a 100644
--- a/frontend/src/features/admin/components/PublishOptionsEditor.tsx
+++ b/frontend/src/features/admin/components/PublishOptionsEditor.tsx
@@ -83,7 +83,8 @@ function asStringList(value: unknown): string[] {
function asObject(value: string): OptionsObject | null {
const parsed = parseJsonValue(value)
if (!parsed.valid) return null
- if (!parsed.parsed || typeof parsed.parsed !== 'object' || Array.isArray(parsed.parsed)) return null
+ if (!parsed.parsed || typeof parsed.parsed !== 'object' || Array.isArray(parsed.parsed))
+ return null
return parsed.parsed as OptionsObject
}
diff --git a/frontend/src/features/admin/components/StructuredJsonEditor.tsx b/frontend/src/features/admin/components/StructuredJsonEditor.tsx
index eaf3d9d..c41b835 100644
--- a/frontend/src/features/admin/components/StructuredJsonEditor.tsx
+++ b/frontend/src/features/admin/components/StructuredJsonEditor.tsx
@@ -154,11 +154,7 @@ function JsonNodeForm({
if (typeof value === 'number') {
return (
- onChange(next ?? 0)}
- style={{ width: '100%' }}
- />
+ onChange(next ?? 0)} style={{ width: '100%' }} />
)
}
@@ -195,9 +191,7 @@ function JsonNodeForm({
tokenSeparators={[',', ',', '\n']}
placeholder="输入后回车新增"
options={value.map(item => ({ value: String(item ?? ''), label: String(item ?? '') }))}
- onChange={next =>
- onChange(numeric ? next.map(item => Number(item) || 0) : next)
- }
+ onChange={next => onChange(numeric ? next.map(item => Number(item) || 0) : next)}
/>
)
}
@@ -246,11 +240,7 @@ function JsonNodeForm({
/>
))}
- onChange([...value, defaultFor(value[0])])}
- >
+ onChange([...value, defaultFor(value[0])])}>
新增一项
@@ -264,8 +254,7 @@ function JsonNodeForm({
{entries.map(([key, child]) => {
const wide =
isPlainObject(child) ||
- (Array.isArray(child) &&
- (!primitiveArray(child) || COMPLEX_ARRAY_KEYS.has(key))) ||
+ (Array.isArray(child) && (!primitiveArray(child) || COMPLEX_ARRAY_KEYS.has(key))) ||
isLongText(child)
return (
parseJsonValue(value), [value])
- const editable =
- parsed.valid && parsed.parsed !== null && typeof parsed.parsed === 'object'
+ const editable = parsed.valid && parsed.parsed !== null && typeof parsed.parsed === 'object'
const [tab, setTab] = useState<'form' | 'json'>('form')
useEffect(() => {
diff --git a/frontend/src/features/admin/labels.ts b/frontend/src/features/admin/labels.ts
index 150831c..ad4d66b 100644
--- a/frontend/src/features/admin/labels.ts
+++ b/frontend/src/features/admin/labels.ts
@@ -280,24 +280,24 @@ export const auditActionMap: Record
= {
'payment_config.export': '导出支付配置',
'payment_config.import': '导入支付配置',
'payment_config.view_secret': '查看支付密钥',
- 'manual_disbursement_create': '新建人工打款',
- 'manual_disbursement_void': '作废人工打款',
- 'operating_expense_create': '登记运营支出',
- 'operating_expense_void': '作废运营支出',
- 'pickup_create': '创建线下提号',
- 'pickup_cancel': '取消线下提号',
- 'pickup_complete': '完成线下提号',
- 'pickup_profit_update': '更新提号利润',
- 'pickup_offline_settlement': '提号线下结算',
- 'pickup_financial_adjustment_create': '新建提号财务调整',
- 'pickup_financial_adjustment_settle': '结算提号财务调整',
- 'pickup_shop_create': '新增提号店铺',
- 'pickup_shop_update': '更新提号店铺',
- 'pickup_shop_delete': '删除提号店铺',
- 'pickup_shop_map_historical': '关联历史捡漏店铺',
- 'pickup_shop_unmap_historical': '解除历史捡漏店铺',
- 'pickup_third_party_order_no_update': '更新第三方单号',
- 'distribution_financial_snapshot_create': '生成分销财务快照',
+ manual_disbursement_create: '新建人工打款',
+ manual_disbursement_void: '作废人工打款',
+ operating_expense_create: '登记运营支出',
+ operating_expense_void: '作废运营支出',
+ pickup_create: '创建线下提号',
+ pickup_cancel: '取消线下提号',
+ pickup_complete: '完成线下提号',
+ pickup_profit_update: '更新提号利润',
+ pickup_offline_settlement: '提号线下结算',
+ pickup_financial_adjustment_create: '新建提号财务调整',
+ pickup_financial_adjustment_settle: '结算提号财务调整',
+ pickup_shop_create: '新增提号店铺',
+ pickup_shop_update: '更新提号店铺',
+ pickup_shop_delete: '删除提号店铺',
+ pickup_shop_map_historical: '关联历史捡漏店铺',
+ pickup_shop_unmap_historical: '解除历史捡漏店铺',
+ pickup_third_party_order_no_update: '更新第三方单号',
+ distribution_financial_snapshot_create: '生成分销财务快照',
'auth.token_version.bump': '刷新登录令牌版本',
}
diff --git a/frontend/src/features/admin/listingDisplay.test.ts b/frontend/src/features/admin/listingDisplay.test.ts
new file mode 100644
index 0000000..ee822b3
--- /dev/null
+++ b/frontend/src/features/admin/listingDisplay.test.ts
@@ -0,0 +1,128 @@
+import { describe, expect, it } from 'vitest'
+
+import {
+ listingAccountLevel,
+ listingAllHeroText,
+ listingCharacterSkinText,
+ listingCodeText,
+ listingCoinText,
+ listingLevelText,
+ listingOwnerRemark,
+ listingPureCoinPriceText,
+ listingQuantity,
+ listingRatioText,
+ listingRegionText,
+ listingRentalDuration,
+ listingRentDepositText,
+ listingResourceText,
+ listingSkinGroupText,
+} from '@/features/admin/listingDisplay'
+import type { AdminListing } from '@/features/admin/types'
+
+function listing(overrides: Partial = {}): AdminListing {
+ return {
+ id: 1,
+ listing_no: '202611010001',
+ account_id: 1,
+ owner_id: 1,
+ title: '测试商品',
+ description: '',
+ game_name: 'delta_force',
+ server_region: '安卓',
+ login_platform: '扫码',
+ rank_level: '烽火地带',
+ haf_coin_amount: 104_300_000,
+ asset_summary: {},
+ screenshot_urls: [],
+ cover_url: '',
+ price_cent: 50_000,
+ deposit_amount_cent: 30_000,
+ is_accelerated_sale: false,
+ in_transaction: false,
+ status: 'published',
+ review_status: 'approved',
+ handoff_mode: 'owner',
+ settlement_mode: 'owner_wallet',
+ review_reason: '',
+ created_at: '2026-11-01T00:00:00Z',
+ updated_at: '2026-11-01T00:00:00Z',
+ ...overrides,
+ } as AdminListing
+}
+
+describe('admin listing display helpers', () => {
+ it('formats basic listing identity fields', () => {
+ expect(listingCodeText(listing())).toBe('202611010001')
+ expect(listingCoinText(listing())).toBe('104.3M')
+ expect(listingRegionText(listing())).toBe('安卓')
+ expect(
+ listingRegionText(listing({ asset_summary: { common_regions: ['北京', '上海'] } }))
+ ).toBe('北京、上海')
+ })
+
+ it('formats scalar asset, resource and skin values', () => {
+ const item = listing({
+ asset_summary: {
+ season_insurance: '3*3',
+ stamina_level: '7级',
+ load_level: '6级',
+ fire_level: 60,
+ secret_kd: 2.5,
+ resources: [
+ { key: 'awmAmmo', label: 'AWM子弹', quantity: 85 },
+ { key: 'helmet6', label: '六头', quantity: 3 },
+ { key: 'armor6', label: '六甲', quantity: 4 },
+ ],
+ skin_groups: {
+ melee: ['坠星者', '暗星'],
+ operatorRed: ['凌霄成卫'],
+ operatorGold: ['露娜-金牌射手'],
+ weapon: ['M7棱镜攻势S2'],
+ },
+ },
+ })
+
+ expect(listingLevelText(item, 'stamina_level')).toBe('7')
+ expect(listingAccountLevel(item)).toBe('60')
+ expect(listingQuantity(item, 'awmAmmo')).toBe(85)
+ expect(listingResourceText(item, 'helmet6')).toBe('3')
+ expect(listingSkinGroupText(item, 'melee')).toBe('坠星者、暗星')
+ expect(listingCharacterSkinText(item)).toBe(
+ '红皮:凌霄成卫 / 金皮:露娜-金牌射手 / 武器:M7棱镜攻势S2'
+ )
+ })
+
+ it('keeps all-hero display compatible with old and new field names', () => {
+ expect(listingAllHeroText(listing({ asset_summary: { all_hero: '是' } }))).toBe('是')
+ expect(listingAllHeroText(listing({ asset_summary: { all_heroes: '否' } }))).toBe('否')
+ expect(listingAllHeroText(listing({ asset_summary: {} }))).toBe('—')
+ })
+
+ it('calculates pure coin price, duration, rent/deposit and ratio', () => {
+ const item = listing({
+ price_cent: 55_000,
+ deposit_amount_cent: 30_000,
+ asset_summary: {
+ daily_loss_m: 30,
+ publish_ratio: 43,
+ price_breakdown: {
+ buyer_coin_base_price_cent: 24_190,
+ consumable_price_cent: 30_810,
+ },
+ },
+ })
+
+ expect(listingPureCoinPriceText(item)).toBe('¥241.90')
+ expect(listingRentalDuration(item)).toEqual({ days: '4天', dailyLoss: '日耗 30M' })
+ expect(listingRentDepositText(item)).toBe('¥550.00 / ¥300.00')
+ expect(listingRatioText(item)).toBe('1:43')
+ })
+
+ it('falls back safely when fields are absent', () => {
+ const item = listing({ listing_no: '', description: '', asset_summary: null })
+ expect(listingCodeText(item)).toBe('SP000001')
+ expect(listingOwnerRemark(item)).toBe('—')
+ expect(listingResourceText(item, 'awmAmmo')).toBe('—')
+ expect(listingRentalDuration(item)).toEqual({ days: '—', dailyLoss: '' })
+ })
+})
diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts
index a77d686..f1c29eb 100644
--- a/frontend/src/features/admin/types.ts
+++ b/frontend/src/features/admin/types.ts
@@ -1318,11 +1318,7 @@ export interface AdminDistributionWallet {
}
export type AdminDistributionWithdrawalStatus =
- | 'pending'
- | 'processing'
- | 'completed'
- | 'rejected'
- | 'cancelled'
+ 'pending' | 'processing' | 'completed' | 'rejected' | 'cancelled'
export interface AdminDistributionWithdrawal {
id: number
@@ -2095,9 +2091,6 @@ export interface AdminTrackingOverview {
today_paid_gmv_cent: number
}
-
-
-
export interface AdminBackupStatus {
generated_at: string
health: string
diff --git a/frontend/src/features/announcements/AnnouncementDetailPage.tsx b/frontend/src/features/announcements/AnnouncementDetailPage.tsx
index 01290ee..b599d67 100644
--- a/frontend/src/features/announcements/AnnouncementDetailPage.tsx
+++ b/frontend/src/features/announcements/AnnouncementDetailPage.tsx
@@ -20,7 +20,12 @@ export function AnnouncementDetailPage() {
{query.isError ? (
-
+
) : null}
diff --git a/frontend/src/features/announcements/AnnouncementsPage.tsx b/frontend/src/features/announcements/AnnouncementsPage.tsx
index fc1a159..3680763 100644
--- a/frontend/src/features/announcements/AnnouncementsPage.tsx
+++ b/frontend/src/features/announcements/AnnouncementsPage.tsx
@@ -47,9 +47,7 @@ export function AnnouncementsPage() {
}}
/>
- {query.isError ? (
-
- ) : null}
+ {query.isError ? : null}
{items.length === 0 && !query.isPending ? (
@@ -69,7 +67,11 @@ export function AnnouncementsPage() {
{item.title}
-
+
{item.content}
diff --git a/frontend/src/features/announcements/api.ts b/frontend/src/features/announcements/api.ts
index c454656..b35fdae 100644
--- a/frontend/src/features/announcements/api.ts
+++ b/frontend/src/features/announcements/api.ts
@@ -30,9 +30,7 @@ export function useAnnouncement(id?: string) {
return useQuery({
queryKey: ['announcement', id],
queryFn: () =>
- unwrapData(
- apiClient.get>(`/announcements/${id}`)
- ),
+ unwrapData(apiClient.get>(`/announcements/${id}`)),
enabled: Boolean(id),
})
}
diff --git a/frontend/src/features/auth/LoginPage.tsx b/frontend/src/features/auth/LoginPage.tsx
index 70d1ef6..c71f5c4 100644
--- a/frontend/src/features/auth/LoginPage.tsx
+++ b/frontend/src/features/auth/LoginPage.tsx
@@ -59,13 +59,15 @@ export function LoginPage() {
解忧商行把费用、押金、交接和结算规则逐项写清楚。每一步都有记录,每一笔账都能核对。
- {['实名审核后才能发布与下单', '交接、归还、结账全程留痕', '账单逐项列明,争议可申诉'].map(
- item => (
-
- {item}
-
- )
- )}
+ {[
+ '实名审核后才能发布与下单',
+ '交接、归还、结账全程留痕',
+ '账单逐项列明,争议可申诉',
+ ].map(item => (
+
+ {item}
+
+ ))}
解忧
@@ -122,9 +124,7 @@ export function LoginPage() {
{import.meta.env.DEV ? (
-
- 开发测试账号:13800138000 / Jieyou123
-
+ 开发测试账号:13800138000 / Jieyou123
) : null}
diff --git a/frontend/src/features/auth/RegisterPage.tsx b/frontend/src/features/auth/RegisterPage.tsx
index 154f2b1..55d750c 100644
--- a/frontend/src/features/auth/RegisterPage.tsx
+++ b/frontend/src/features/auth/RegisterPage.tsx
@@ -63,7 +63,7 @@ export function RegisterPage() {
useEffect(() => {
const fromQuery = searchParams.get('invite')
- let stored = ''
+ let stored: string
try {
stored = sessionStorage.getItem(INVITE_STORAGE_KEY) || ''
} catch {
@@ -154,13 +154,15 @@ export function RegisterPage() {
解忧商行把费用、押金、交接和结算规则逐项写清楚。每一步都有记录,每一笔账都能核对。
- {['实名审核后才能发布与下单', '交接、归还、结账全程留痕', '账单逐项列明,争议可申诉'].map(
- item => (
-
- {item}
-
- )
- )}
+ {[
+ '实名审核后才能发布与下单',
+ '交接、归还、结账全程留痕',
+ '账单逐项列明,争议可申诉',
+ ].map(item => (
+
+ {item}
+
+ ))}
解忧
diff --git a/frontend/src/features/crash/CrashDetailPage.tsx b/frontend/src/features/crash/CrashDetailPage.tsx
index 6ead12c..2a89f62 100644
--- a/frontend/src/features/crash/CrashDetailPage.tsx
+++ b/frontend/src/features/crash/CrashDetailPage.tsx
@@ -58,9 +58,20 @@ export function CrashDetailPage() {
{product ? (
-
+
{product.cover_url ? (
-
+
) : null}
@@ -88,12 +99,22 @@ export function CrashDetailPage() {
数量
- setQuantity(Number(value) || 1)} />
+ setQuantity(Number(value) || 1)}
+ />
合计 {formatMoney(product.price_cent * quantity)}
- void submit()}>
+ void submit()}
+ >
立即下单
diff --git a/frontend/src/features/crash/CrashListPage.tsx b/frontend/src/features/crash/CrashListPage.tsx
index dc0d03b..4dbe86d 100644
--- a/frontend/src/features/crash/CrashListPage.tsx
+++ b/frontend/src/features/crash/CrashListPage.tsx
@@ -1,5 +1,16 @@
import { useState } from 'react'
-import { Alert, Button, Card, Empty, Image, Pagination, Segmented, Space, Tag, Typography } from 'antd'
+import {
+ Alert,
+ Button,
+ Card,
+ Empty,
+ Image,
+ Pagination,
+ Segmented,
+ Space,
+ Tag,
+ Typography,
+} from 'antd'
import { Link, useNavigate } from 'react-router-dom'
import { useCrashCategories, useCrashProducts } from '@/features/crash/api'
@@ -67,7 +78,14 @@ export function CrashListPage() {
{item.cover_url ? (
-
+
) : null}
{item.title}
@@ -79,7 +97,10 @@ export function CrashListPage() {
) : null}
{item.category_name || '未分类'}
- 0 ? 'blue' : 'default'}>
+ 0 ? 'blue' : 'default'}
+ >
{item.stock < 0 ? '不限量' : `库存 ${item.stock}`}
diff --git a/frontend/src/features/crash/CrashOrdersPage.tsx b/frontend/src/features/crash/CrashOrdersPage.tsx
index f5376ad..85643a7 100644
--- a/frontend/src/features/crash/CrashOrdersPage.tsx
+++ b/frontend/src/features/crash/CrashOrdersPage.tsx
@@ -40,7 +40,11 @@ export function CrashOrdersPage() {
type="info"
showIcon
message="登录后查看撞车订单"
- action={ navigate('/login')}>去登录}
+ action={
+ navigate('/login')}>
+ 去登录
+
+ }
/>
)
@@ -68,7 +72,13 @@ export function CrashOrdersPage() {
{order.product_cover_url ? (
-
+
) : null}
@@ -82,7 +92,9 @@ export function CrashOrdersPage() {
{formatMoney(order.amount_cent)} · 数量 {order.quantity}
下单 {formatDateTime(order.created_at)}
- {order.cancel_reason ? 原因:{order.cancel_reason} : null}
+ {order.cancel_reason ? (
+ 原因:{order.cancel_reason}
+ ) : null}
{order.status === 'pending_payment' ? (
<>
@@ -122,7 +134,10 @@ export function CrashOrdersPage() {
>
) : null}
{order.qrcode_url_snapshot ? (
- window.open(order.qrcode_url_snapshot, '_blank')}>
+ window.open(order.qrcode_url_snapshot, '_blank')}
+ >
查看二维码
) : null}
diff --git a/frontend/src/features/crash/api.ts b/frontend/src/features/crash/api.ts
index ce13d58..ad91270 100644
--- a/frontend/src/features/crash/api.ts
+++ b/frontend/src/features/crash/api.ts
@@ -54,17 +54,13 @@ export function useCrashProduct(id?: string) {
return useQuery({
queryKey: ['crash-product', id],
queryFn: () =>
- unwrapData(
- apiClient.get>(`/crash/products/${id}`)
- ),
+ unwrapData(apiClient.get>(`/crash/products/${id}`)),
enabled: Boolean(id),
})
}
export function createCrashOrder(payload: { product_id: number; quantity: number }) {
- return unwrapData(
- apiClient.post>('/crash/orders', payload)
- )
+ return unwrapData(apiClient.post>('/crash/orders', payload))
}
export function fetchMyCrashOrders(page = 1, pageSize = 20) {
@@ -95,8 +91,8 @@ export function cancelCrashOrder(id: number) {
export function startCrashPayment(id: number) {
return unwrapData<{ pay_url?: string; jsapi?: string; qrcode?: string; status?: string }>(
- apiClient.post>(
- `/crash/orders/${id}/start-payment`
- )
+ apiClient.post<
+ ApiResponse<{ pay_url?: string; jsapi?: string; qrcode?: string; status?: string }>
+ >(`/crash/orders/${id}/start-payment`)
)
}
diff --git a/frontend/src/features/help/ProcessPage.tsx b/frontend/src/features/help/ProcessPage.tsx
index 12eddb9..c46c53d 100644
--- a/frontend/src/features/help/ProcessPage.tsx
+++ b/frontend/src/features/help/ProcessPage.tsx
@@ -13,7 +13,8 @@ const details = [
{
key: 'handoff',
label: '交接阶段要注意什么',
- children: '号主提交的账号信息只在订单内可见,租客确认前请先核对区服、段位与资产是否与商品一致。发现不符可发起申诉,不要先确认。',
+ children:
+ '号主提交的账号信息只在订单内可见,租客确认前请先核对区服、段位与资产是否与商品一致。发现不符可发起申诉,不要先确认。',
},
{
key: 'return',
@@ -23,7 +24,8 @@ const details = [
{
key: 'checkout',
label: '结账与额外消耗',
- children: '结账页会列出系统结算信息,包括租金、押金、额外消耗与退款。结账备注只是双方留言,不参与系统结算。',
+ children:
+ '结账页会列出系统结算信息,包括租金、押金、额外消耗与退款。结账备注只是双方留言,不参与系统结算。',
},
]
diff --git a/frontend/src/features/home/HomePortalRow.tsx b/frontend/src/features/home/HomePortalRow.tsx
index 52db6c1..f029396 100644
--- a/frontend/src/features/home/HomePortalRow.tsx
+++ b/frontend/src/features/home/HomePortalRow.tsx
@@ -125,7 +125,9 @@ export function HomePortalRow() {
) : (
{item.badge ? {item.badge} : null}
- {item.eyebrow ? {item.eyebrow} : null}
+ {item.eyebrow ? (
+ {item.eyebrow}
+ ) : null}
{item.title}
{item.pill ? {item.pill} : null}
@@ -149,7 +151,9 @@ export function HomePortalRow() {
可用余额
-
{wallet.data ? formatMoney(wallet.data.available_balance_cent) : '—'}
+
+ {wallet.data ? formatMoney(wallet.data.available_balance_cent) : '—'}
+
查看钱包
@@ -161,7 +165,10 @@ export function HomePortalRow() {
卖家中心
-
document.querySelector('#listings')?.scrollIntoView()}>
+ document.querySelector('#listings')?.scrollIntoView()}
+ >
我要租号
navigate('/seller/listings/new')}>我要出租
diff --git a/frontend/src/features/home/api.ts b/frontend/src/features/home/api.ts
index 64c285d..554e1ca 100644
--- a/frontend/src/features/home/api.ts
+++ b/frontend/src/features/home/api.ts
@@ -68,9 +68,7 @@ export const fallbackBanners: HomeBannerItem[] = [
]
export function fetchHomeConfig() {
- return unwrapData(
- apiClient.get>('/mobile-home-config')
- )
+ return unwrapData(apiClient.get>('/mobile-home-config'))
}
export function useHomeConfig() {
diff --git a/frontend/src/features/invitations/InvitationsPage.tsx b/frontend/src/features/invitations/InvitationsPage.tsx
index de072df..8f16ad2 100644
--- a/frontend/src/features/invitations/InvitationsPage.tsx
+++ b/frontend/src/features/invitations/InvitationsPage.tsx
@@ -1,6 +1,17 @@
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
-import { Alert, Button, Card, Empty, Space, Table, Tabs, Tag, Typography, type TableColumnsType } from 'antd'
+import {
+ Alert,
+ Button,
+ Card,
+ Empty,
+ Space,
+ Table,
+ Tabs,
+ Tag,
+ Typography,
+ type TableColumnsType,
+} from 'antd'
import { CopyOutlined, GiftOutlined, ReloadOutlined } from '@ant-design/icons'
import {
@@ -177,7 +188,11 @@ export function InvitationsPage() {
累计返利
{formatMoney(summary?.total_rebate_cent ?? 0)}
- } onClick={() => void copyLink()} disabled={!summary?.invite_link}>
+ }
+ onClick={() => void copyLink()}
+ disabled={!summary?.invite_link}
+ >
复制邀请链接
去实名}
+ action={
+
+ 去实名
+
+ }
/>
) : null}
@@ -228,9 +247,7 @@ export function InvitationsPage() {
- {summaryQuery.isError ? (
-
- ) : null}
+ {summaryQuery.isError ? : null}
- ) : (
-
- rowKey="id"
- size="middle"
- columns={recordColumns}
- dataSource={recordsQuery.data?.items ?? []}
- loading={recordsQuery.isFetching}
- scroll={{ x: 'max-content' }}
- pagination={{
- current: recordsQuery.data?.page ?? recordPage,
- pageSize: recordsQuery.data?.page_size ?? 20,
- total: recordsQuery.data?.total ?? 0,
- onChange: next => setRecordPage(next),
- showSizeChanger: false,
- }}
- />
- ),
+ children:
+ (recordsQuery.data?.items ?? []).length === 0 && !recordsQuery.isPending ? (
+
+ ) : (
+
+ rowKey="id"
+ size="middle"
+ columns={recordColumns}
+ dataSource={recordsQuery.data?.items ?? []}
+ loading={recordsQuery.isFetching}
+ scroll={{ x: 'max-content' }}
+ pagination={{
+ current: recordsQuery.data?.page ?? recordPage,
+ pageSize: recordsQuery.data?.page_size ?? 20,
+ total: recordsQuery.data?.total ?? 0,
+ onChange: next => setRecordPage(next),
+ showSizeChanger: false,
+ }}
+ />
+ ),
},
{
key: 'rebates',
label: '返利明细',
- children: (rebatesQuery.data?.items ?? []).length === 0 && !rebatesQuery.isPending ? (
-
- ) : (
-
- rowKey="id"
- size="middle"
- columns={rebateColumns}
- dataSource={rebatesQuery.data?.items ?? []}
- loading={rebatesQuery.isFetching}
- scroll={{ x: 'max-content' }}
- pagination={{
- current: rebatesQuery.data?.page ?? rebatePage,
- pageSize: rebatesQuery.data?.page_size ?? 20,
- total: rebatesQuery.data?.total ?? 0,
- onChange: next => setRebatePage(next),
- showSizeChanger: false,
- }}
- />
- ),
+ children:
+ (rebatesQuery.data?.items ?? []).length === 0 && !rebatesQuery.isPending ? (
+
+ ) : (
+
+ rowKey="id"
+ size="middle"
+ columns={rebateColumns}
+ dataSource={rebatesQuery.data?.items ?? []}
+ loading={rebatesQuery.isFetching}
+ scroll={{ x: 'max-content' }}
+ pagination={{
+ current: rebatesQuery.data?.page ?? rebatePage,
+ pageSize: rebatesQuery.data?.page_size ?? 20,
+ total: rebatesQuery.data?.total ?? 0,
+ onChange: next => setRebatePage(next),
+ showSizeChanger: false,
+ }}
+ />
+ ),
},
]}
/>
diff --git a/frontend/src/features/invitations/InviteLandingPage.tsx b/frontend/src/features/invitations/InviteLandingPage.tsx
index d5d0d20..104f979 100644
--- a/frontend/src/features/invitations/InviteLandingPage.tsx
+++ b/frontend/src/features/invitations/InviteLandingPage.tsx
@@ -70,7 +70,10 @@ export function InviteLandingPage() {
)}
- navigate(`/m/register${code ? `?invite=${code}` : ''}`)}>
+ navigate(`/m/register${code ? `?invite=${code}` : ''}`)}
+ >
立即注册
navigate('/m/login')}>已有账号,去登录
diff --git a/frontend/src/features/invitations/api.ts b/frontend/src/features/invitations/api.ts
index 4e387e6..827fed3 100644
--- a/frontend/src/features/invitations/api.ts
+++ b/frontend/src/features/invitations/api.ts
@@ -51,9 +51,19 @@ export function fetchMyRebates(page = 1, pageSize = PAGE_SIZE) {
}
export function fetchInviteCodeInfo(code: string) {
- return unwrapData<{ valid: boolean; invite_code: string; inviter_nickname: string; inviter_avatar: string }>(
- apiClient.get>(
- `/invitations/code-info/${code}`
- )
+ return unwrapData<{
+ valid: boolean
+ invite_code: string
+ inviter_nickname: string
+ inviter_avatar: string
+ }>(
+ apiClient.get<
+ ApiResponse<{
+ valid: boolean
+ invite_code: string
+ inviter_nickname: string
+ inviter_avatar: string
+ }>
+ >(`/invitations/code-info/${code}`)
)
}
diff --git a/frontend/src/features/listings/ListingCollectionsPage.tsx b/frontend/src/features/listings/ListingCollectionsPage.tsx
index 66a9472..e1a949a 100644
--- a/frontend/src/features/listings/ListingCollectionsPage.tsx
+++ b/frontend/src/features/listings/ListingCollectionsPage.tsx
@@ -76,8 +76,8 @@ export function ListingCollectionsPage() {
const historyItems = token ? remoteHistory : localHistory
const favoriteItems = token ? remoteFavorites : localFavorites
- const historyCount = token ? remote.data?.counts.history ?? 0 : local.history.length
- const favoriteCount = token ? remote.data?.counts.favorite ?? 0 : local.favorites.length
+ const historyCount = token ? (remote.data?.counts.history ?? 0) : local.history.length
+ const favoriteCount = token ? (remote.data?.counts.favorite ?? 0) : local.favorites.length
const favoriteIDs = new Set(favoriteItems.map(item => item.listing.id))
const items = tab === 'favorite' ? favoriteItems : historyItems
const loading = Boolean(token) && remote.isPending
diff --git a/frontend/src/features/listings/ListingDetailPage.tsx b/frontend/src/features/listings/ListingDetailPage.tsx
index 74c6761..81e62f5 100644
--- a/frontend/src/features/listings/ListingDetailPage.tsx
+++ b/frontend/src/features/listings/ListingDetailPage.tsx
@@ -31,11 +31,7 @@ import {
recordRemoteView,
removeRemoteFavorite,
} from '@/features/listings/collectionsApi'
-import {
- createOrder,
- useOrderAgreements,
- usePostRentalNotice,
-} from '@/features/orders/api'
+import { createOrder, useOrderAgreements, usePostRentalNotice } from '@/features/orders/api'
import { useSessionStore } from '@/stores/session'
import { formatCoinM } from '@/shared/utils/format'
import type { Listing } from '@/shared/api/types'
@@ -101,7 +97,9 @@ function skinGroupsOf(item: Listing) {
.map(([key, names]) => ({
key,
label: skinGroupLabels[key] ?? key,
- names: Array.isArray(names) ? names.filter((name): name is string => typeof name === 'string') : [],
+ names: Array.isArray(names)
+ ? names.filter((name): name is string => typeof name === 'string')
+ : [],
}))
.filter(group => group.names.length > 0)
}
@@ -192,7 +190,7 @@ export function ListingDetailPage() {
useEffect(() => {
if (!data) return
const images = galleryImages(data)
- setActiveImage(current => (current && images.includes(current) ? current : images[0] ?? ''))
+ setActiveImage(current => (current && images.includes(current) ? current : (images[0] ?? '')))
}, [data])
const showAgreement = (title: string, content?: string) => {
@@ -319,10 +317,7 @@ export function ListingDetailPage() {
{images.length > 0 ? (
-
+
) : (
@@ -583,9 +578,7 @@ export function ListingDetailPage() {
{money(data.price_cent)}
{money(data.deposit_amount_cent)}
-
- {formatCoinM(data.haf_coin_amount)}
-
+ {formatCoinM(data.haf_coin_amount)}
{data.owner_nickname || `#${data.owner_id}`}
diff --git a/frontend/src/features/listings/ListingFilterPanel.tsx b/frontend/src/features/listings/ListingFilterPanel.tsx
index 8c05803..e06f6d7 100644
--- a/frontend/src/features/listings/ListingFilterPanel.tsx
+++ b/frontend/src/features/listings/ListingFilterPanel.tsx
@@ -114,9 +114,40 @@ const loginOptions = ['扫码', '账号密码']
const fallbackRanks = ['青铜', '白银', '黄金', '铂金', '钻石', '黑鹰', '三角洲巅峰']
const fallbackRegions = [
- '北京', '天津', '河北', '山西', '内蒙古', '辽宁', '吉林', '黑龙江', '上海', '江苏', '浙江',
- '安徽', '福建', '江西', '山东', '河南', '湖北', '湖南', '广东', '广西', '海南', '重庆',
- '四川', '贵州', '云南', '陕西', '甘肃', '青海', '宁夏', '新疆', '西藏', '香港', '澳门', '台湾',
+ '北京',
+ '天津',
+ '河北',
+ '山西',
+ '内蒙古',
+ '辽宁',
+ '吉林',
+ '黑龙江',
+ '上海',
+ '江苏',
+ '浙江',
+ '安徽',
+ '福建',
+ '江西',
+ '山东',
+ '河南',
+ '湖北',
+ '湖南',
+ '广东',
+ '广西',
+ '海南',
+ '重庆',
+ '四川',
+ '贵州',
+ '云南',
+ '陕西',
+ '甘肃',
+ '青海',
+ '宁夏',
+ '新疆',
+ '西藏',
+ '香港',
+ '澳门',
+ '台湾',
]
interface Props {
@@ -206,8 +237,8 @@ export function ListingFilterPanel({
const quickKeys = ['melee', 'operatorRed']
const quickGroups = skinGroups.filter(group => quickKeys.includes(group.key))
const advancedSkinGroups = skinGroups.filter(group => !quickKeys.includes(group.key))
- const ranks = (rankOptions ?? []).length > 0 ? rankOptions ?? [] : fallbackRanks
- const regions = (regionOptions ?? []).length > 0 ? regionOptions ?? [] : fallbackRegions
+ const ranks = (rankOptions ?? []).length > 0 ? (rankOptions ?? []) : fallbackRanks
+ const regions = (regionOptions ?? []).length > 0 ? (regionOptions ?? []) : fallbackRegions
const toggle = (
key: 'insurance' | 'stamina' | 'load' | 'loginMethod' | 'ranks' | 'regions' | 'skinNames',
@@ -223,7 +254,16 @@ export function ListingFilterPanel({
}
const toggleRange = (
- key: 'coinKey' | 'priceKey' | 'ratioKey' | 'depositKey' | 'totalKey' | 'fireLevelKey' | 'dailyLossKey' | 'secretKdKey' | 'awmKey',
+ key:
+ | 'coinKey'
+ | 'priceKey'
+ | 'ratioKey'
+ | 'depositKey'
+ | 'totalKey'
+ | 'fireLevelKey'
+ | 'dailyLossKey'
+ | 'secretKdKey'
+ | 'awmKey',
nextKey: string
) => {
onChange({ ...value, [key]: value[key] === nextKey ? undefined : nextKey })
@@ -256,7 +296,9 @@ export function ListingFilterPanel({
筛选条件
{total} 个结果
- {activeCount > 0 ? 已选 {activeCount} 项 : null}
+ {activeCount > 0 ? (
+ 已选 {activeCount} 项
+ ) : null}
} onClick={onReset}>
重置全部条件
@@ -264,14 +306,54 @@ export function ListingFilterPanel({
-
toggleRange('coinKey', key)} />
- toggleRange('priceKey', key)} />
- toggleRange('ratioKey', key)} />
- toggle('insurance', option)} />
- toggleRange('awmKey', key)} />
- toggle('stamina', option)} />
- toggle('loginMethod', option)} />
- toggle('load', option)} />
+ toggleRange('coinKey', key)}
+ />
+ toggleRange('priceKey', key)}
+ />
+ toggleRange('ratioKey', key)}
+ />
+ toggle('insurance', option)}
+ />
+ toggleRange('awmKey', key)}
+ />
+ toggle('stamina', option)}
+ />
+ toggle('loginMethod', option)}
+ />
+ toggle('load', option)}
+ />
{quickGroups.map(group => (
- toggleRange('depositKey', key)} />
- toggleRange('totalKey', key)} />
- toggleRange('fireLevelKey', key)} />
- toggleRange('dailyLossKey', key)} />
- toggleRange('secretKdKey', key)} />
- toggle('ranks', option)} />
- toggle('regions', option)} wide />
+ toggleRange('depositKey', key)}
+ />
+ toggleRange('totalKey', key)}
+ />
+ toggleRange('fireLevelKey', key)}
+ />
+ toggleRange('dailyLossKey', key)}
+ />
+ toggleRange('secretKdKey', key)}
+ />
+ toggle('ranks', option)}
+ />
+ toggle('regions', option)}
+ wide
+ />
{advancedSkinGroups.map(group => (
updateFilters(emptyListingFilters)}
/>
-
- {zoneTabs.map(tab => {
- const count = data?.zone_counts?.[tab.key]
- return (
- {
- setZone(tab.key)
- setPage(1)
- }}
- >
- {tab.label}
- {typeof count === 'number' ? {count} : null}
-
- )
- })}
-
+
+ {zoneTabs.map(tab => {
+ const count = data?.zone_counts?.[tab.key]
+ return (
+ {
+ setZone(tab.key)
+ setPage(1)
+ }}
+ >
+ {tab.label}
+ {typeof count === 'number' ? {count} : null}
+
+ )
+ })}
+
- {isError ? (
- refetch()}>
- 重试
-
- }
- />
- ) : null}
+ {isError ? (
+ refetch()}>
+ 重试
+
+ }
+ />
+ ) : null}
{viewMode === 'grid' ? (
diff --git a/frontend/src/features/listings/collections.ts b/frontend/src/features/listings/collections.ts
index 5661cb2..6f13e57 100644
--- a/frontend/src/features/listings/collections.ts
+++ b/frontend/src/features/listings/collections.ts
@@ -75,9 +75,7 @@ function readItems(key: string): CollectionItem[] {
if (!raw) return []
const parsed = JSON.parse(raw) as unknown
if (!Array.isArray(parsed)) return []
- return parsed
- .map(normalizeItem)
- .filter((item): item is CollectionItem => Boolean(item))
+ return parsed.map(normalizeItem).filter((item): item is CollectionItem => Boolean(item))
} catch {
localStorage.removeItem(key)
return []
diff --git a/frontend/src/features/listings/labels.ts b/frontend/src/features/listings/labels.ts
index 9a05ecc..4b4c091 100644
--- a/frontend/src/features/listings/labels.ts
+++ b/frontend/src/features/listings/labels.ts
@@ -71,7 +71,9 @@ export function formatAssetValue(key: string, value: unknown): string {
if (text) return text
}
if (Array.isArray(value)) {
- return value.map(item => (typeof item === 'object' ? JSON.stringify(item) : String(item))).join('、')
+ return value
+ .map(item => (typeof item === 'object' ? JSON.stringify(item) : String(item)))
+ .join('、')
}
if (typeof value === 'object') {
const text = formatOnlineTime(value)
diff --git a/frontend/src/features/messages/ChatPage.tsx b/frontend/src/features/messages/ChatPage.tsx
index 960eab6..fab9aee 100644
--- a/frontend/src/features/messages/ChatPage.tsx
+++ b/frontend/src/features/messages/ChatPage.tsx
@@ -340,7 +340,11 @@ export function ChatPage() {
return false
}}
>
- } loading={uploading} disabled={pendingImages.length >= 9}>
+ }
+ loading={uploading}
+ disabled={pendingImages.length >= 9}
+ >
图片
diff --git a/frontend/src/features/messages/MessagesPage.tsx b/frontend/src/features/messages/MessagesPage.tsx
index 80ba732..c904457 100644
--- a/frontend/src/features/messages/MessagesPage.tsx
+++ b/frontend/src/features/messages/MessagesPage.tsx
@@ -155,10 +155,7 @@ export function MessagesPage() {
账号群聊、客服回复与系统通知只对当前账号开放。
- navigate('/login?redirect=/messages')}
- >
+ navigate('/login?redirect=/messages')}>
去登录
@@ -239,7 +236,11 @@ export function MessagesPage() {
/>
) : items.length === 0 ? (
- void openSupport()}>
+ void openSupport()}
+ >
联系平台客服
diff --git a/frontend/src/features/messages/api.ts b/frontend/src/features/messages/api.ts
index ca5785c..f11340e 100644
--- a/frontend/src/features/messages/api.ts
+++ b/frontend/src/features/messages/api.ts
@@ -2,7 +2,12 @@ import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
import { apiClient, unwrapData } from '@/shared/api/client'
import type { ApiResponse } from '@/shared/api/types'
-import type { AppNotification, ChatConversation, ChatMessage, ChatPage } from '@/features/messages/types'
+import type {
+ AppNotification,
+ ChatConversation,
+ ChatMessage,
+ ChatPage,
+} from '@/features/messages/types'
const PAGE_SIZE = 20
@@ -24,8 +29,7 @@ export function useConversations(enabled = true) {
queryKey: ['chat-conversations'],
queryFn: ({ pageParam }) => fetchConversations(pageParam),
initialPageParam: 1,
- getNextPageParam: last =>
- last.page * last.page_size < last.total ? last.page + 1 : undefined,
+ getNextPageParam: last => (last.page * last.page_size < last.total ? last.page + 1 : undefined),
enabled,
})
}
@@ -48,8 +52,7 @@ export function useNotifications(enabled = true) {
queryKey: ['notifications'],
queryFn: ({ pageParam }) => fetchNotifications(pageParam),
initialPageParam: 1,
- getNextPageParam: last =>
- last.page * last.page_size < last.total ? last.page + 1 : undefined,
+ getNextPageParam: last => (last.page * last.page_size < last.total ? last.page + 1 : undefined),
enabled,
})
}
@@ -81,8 +84,7 @@ export function useChatMessages(id?: string, enabled = true) {
queryKey: ['chat-messages', id],
queryFn: ({ pageParam }) => fetchChatMessages(id as string, pageParam),
initialPageParam: 1,
- getNextPageParam: last =>
- last.page * last.page_size < last.total ? last.page + 1 : undefined,
+ getNextPageParam: last => (last.page * last.page_size < last.total ? last.page + 1 : undefined),
enabled: Boolean(id) && enabled,
refetchInterval: 30_000,
})
@@ -129,10 +131,9 @@ export function useChatUnreadCount(enabled = true) {
queryKey: ['chat-unread-count'],
queryFn: () =>
unwrapData<{ unread_count: number }>(
- apiClient.get>(
- '/chats/unread-count',
- { skipErrorHandler: true } as never
- )
+ apiClient.get>('/chats/unread-count', {
+ skipErrorHandler: true,
+ } as never)
)
.then(data => data.unread_count ?? 0)
.catch(() => 0),
diff --git a/frontend/src/features/mobile/MobileAddHomeGuidePage.tsx b/frontend/src/features/mobile/MobileAddHomeGuidePage.tsx
index 4c4f2c6..f845dbc 100644
--- a/frontend/src/features/mobile/MobileAddHomeGuidePage.tsx
+++ b/frontend/src/features/mobile/MobileAddHomeGuidePage.tsx
@@ -18,9 +18,18 @@ export function MobileAddHomeGuidePage() {
-
-
-
+
+
+
diff --git a/frontend/src/features/mobile/MobileChatPage.tsx b/frontend/src/features/mobile/MobileChatPage.tsx
index 80fa5c6..cc4bd4a 100644
--- a/frontend/src/features/mobile/MobileChatPage.tsx
+++ b/frontend/src/features/mobile/MobileChatPage.tsx
@@ -42,7 +42,9 @@ function MessageBubble({ message }: { message: ChatMessage }) {
{(message.sender_name || '商').slice(0, 1)}
) : null}
- {!message.is_self ?
{message.sender_name} : null}
+ {!message.is_self ? (
+
{message.sender_name}
+ ) : null}
{message.content_type === 'image' && message.attachment_urls?.length ? (
diff --git a/frontend/src/features/mobile/MobileCrashDetailPage.tsx b/frontend/src/features/mobile/MobileCrashDetailPage.tsx
index 591fba1..6de4373 100644
--- a/frontend/src/features/mobile/MobileCrashDetailPage.tsx
+++ b/frontend/src/features/mobile/MobileCrashDetailPage.tsx
@@ -57,7 +57,13 @@ export function MobileCrashDetailPage() {
{product ? (
<>
{product.cover_url ? (
-
+
) : null}
@@ -76,7 +82,9 @@ export function MobileCrashDetailPage() {
void submit()}>
立即下单
-
下单需登录并完成实名;支付成功后由客服接单处理。
+
+ 下单需登录并完成实名;支付成功后由客服接单处理。
+
>
) : null}
diff --git a/frontend/src/features/mobile/MobileCrashListPage.tsx b/frontend/src/features/mobile/MobileCrashListPage.tsx
index 11f3dd3..8f6d0f8 100644
--- a/frontend/src/features/mobile/MobileCrashListPage.tsx
+++ b/frontend/src/features/mobile/MobileCrashListPage.tsx
@@ -26,10 +26,7 @@ export function MobileCrashListPage() {
-
setCategoryId(undefined)}
- >
+ setCategoryId(undefined)}>
全部
{(categoriesQuery.data ?? []).map(item => (
@@ -55,7 +52,13 @@ export function MobileCrashListPage() {
navigate(`/m/crash/${item.id}`)}>
{item.cover_url ? (
-
+
) : null}
{item.title}
diff --git a/frontend/src/features/mobile/MobileCrashOrdersPage.tsx b/frontend/src/features/mobile/MobileCrashOrdersPage.tsx
index 1a41ab9..0fd51c4 100644
--- a/frontend/src/features/mobile/MobileCrashOrdersPage.tsx
+++ b/frontend/src/features/mobile/MobileCrashOrdersPage.tsx
@@ -6,7 +6,10 @@ import { cancelCrashOrder, startCrashPayment, useMyCrashOrders } from '@/feature
import { useSessionStore } from '@/stores/session'
import { formatDateTime, formatMoney } from '@/shared/utils/format'
-const statusMeta: Record
= {
+const statusMeta: Record<
+ string,
+ { label: string; color: 'primary' | 'success' | 'warning' | 'danger' | 'default' }
+> = {
pending_payment: { label: '待支付', color: 'warning' },
paid: { label: '已支付', color: 'primary' },
received: { label: '已接单', color: 'primary' },
@@ -41,7 +44,10 @@ export function MobileCrashOrdersPage() {
) : (
{items.map(order => {
- const meta = statusMeta[order.status] ?? { label: order.status, color: 'default' as const }
+ const meta = statusMeta[order.status] ?? {
+ label: order.status,
+ color: 'default' as const,
+ }
return (
diff --git a/frontend/src/features/mobile/MobileHomePage.tsx b/frontend/src/features/mobile/MobileHomePage.tsx
index f584604..ad53911 100644
--- a/frontend/src/features/mobile/MobileHomePage.tsx
+++ b/frontend/src/features/mobile/MobileHomePage.tsx
@@ -1,6 +1,15 @@
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
-import { Button, Empty, ErrorBlock, Popup, PullToRefresh, SearchBar, Skeleton, Tag } from 'antd-mobile'
+import {
+ Button,
+ Empty,
+ ErrorBlock,
+ Popup,
+ PullToRefresh,
+ SearchBar,
+ Skeleton,
+ Tag,
+} from 'antd-mobile'
import { DownOutline, RightOutline, SearchOutline } from 'antd-mobile-icons'
import { Link } from 'react-router-dom'
@@ -322,7 +331,12 @@ export function MobileHomePage() {
setFilterOpen(false)}
- bodyStyle={{ height: '72vh', overflowY: 'auto', borderTopLeftRadius: 16, borderTopRightRadius: 16 }}
+ bodyStyle={{
+ height: '72vh',
+ overflowY: 'auto',
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ }}
>
diff --git a/frontend/src/features/mobile/MobileListingDetailPage.tsx b/frontend/src/features/mobile/MobileListingDetailPage.tsx
index b9152ec..52a7756 100644
--- a/frontend/src/features/mobile/MobileListingDetailPage.tsx
+++ b/frontend/src/features/mobile/MobileListingDetailPage.tsx
@@ -6,11 +6,7 @@ import { useNavigate, useParams } from 'react-router-dom'
import { BrandLogo } from '@/components/BrandLogo'
import { useListingDetail } from '@/features/listings/api'
import { ensureSupportConversation } from '@/features/messages/api'
-import {
- createOrder,
- useOrderAgreements,
- usePostRentalNotice,
-} from '@/features/orders/api'
+import { createOrder, useOrderAgreements, usePostRentalNotice } from '@/features/orders/api'
import { useSessionStore } from '@/stores/session'
import { formatCoinM } from '@/shared/utils/format'
@@ -130,7 +126,11 @@ export function MobileListingDetailPage() {
if (isError || !data) {
return (
-
+
void refetch()}>
重试
@@ -223,11 +223,7 @@ export function MobileListingDetailPage() {
-
void openSupport()}
- >
+ void openSupport()}>
联系客服
- void openSupport()}>
+ void openSupport()}
+ >
联系客服
diff --git a/frontend/src/features/mobile/MobileNotificationsPage.tsx b/frontend/src/features/mobile/MobileNotificationsPage.tsx
index 83e64fb..18f5871 100644
--- a/frontend/src/features/mobile/MobileNotificationsPage.tsx
+++ b/frontend/src/features/mobile/MobileNotificationsPage.tsx
@@ -70,11 +70,7 @@ export function MobileNotificationsPage() {
)}
{query.hasNextPage ? (
- void query.fetchNextPage()}
- >
+ void query.fetchNextPage()}>
加载更多
) : null}
diff --git a/frontend/src/features/mobile/MobileOrderActionPanel.tsx b/frontend/src/features/mobile/MobileOrderActionPanel.tsx
index a83c66e..085c2b9 100644
--- a/frontend/src/features/mobile/MobileOrderActionPanel.tsx
+++ b/frontend/src/features/mobile/MobileOrderActionPanel.tsx
@@ -1,14 +1,5 @@
import { useState } from 'react'
-import {
- ActionSheet,
- Button,
- Dialog,
- Input,
- Popup,
- Selector,
- TextArea,
- Toast,
-} from 'antd-mobile'
+import { ActionSheet, Button, Dialog, Input, Popup, Selector, TextArea, Toast } from 'antd-mobile'
import {
acceptCheckout,
@@ -332,12 +323,7 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa
{canPay ? (
- setPayOpen(true)}
- >
+ setPayOpen(true)}>
去支付
setHandoffOpen(false)}>
取消
- void submitHandoffAction()}>
+ void submitHandoffAction()}
+ >
提交交接
@@ -498,15 +489,30 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa
@@ -522,7 +528,12 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa
setCheckoutOpen(false)}>
取消
- void submitCheckoutAction()}>
+ void submitCheckoutAction()}
+ >
提交结账
@@ -536,20 +547,36 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa
>
修改结账方案
- 当前第 {checkout?.round_count ?? 1}/{checkout?.max_rounds ?? 6} 轮,修改后由对方继续确认或还价。
+ 当前第 {checkout?.round_count ?? 1}/{checkout?.max_rounds ?? 6}{' '}
+ 轮,修改后由对方继续确认或还价。
@@ -575,7 +602,12 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa
setCounterOpen(false)}>
取消
- void counterCheckoutAction()}>
+ void counterCheckoutAction()}
+ >
提交修改
@@ -627,7 +659,12 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa
setDisputeOpen(false)}>
取消
- void createDisputeAction()}>
+ void createDisputeAction()}
+ >
提交申诉
diff --git a/frontend/src/features/mobile/MobileOrderDetailPage.tsx b/frontend/src/features/mobile/MobileOrderDetailPage.tsx
index 418380e..05f1938 100644
--- a/frontend/src/features/mobile/MobileOrderDetailPage.tsx
+++ b/frontend/src/features/mobile/MobileOrderDetailPage.tsx
@@ -82,7 +82,11 @@ export function MobileOrderDetailPage() {
登录后查看订单详情
交接记录与结账明细只对交易双方可见。
-
navigate(`/m/login?redirect=/m/orders/${id}`)}>
+ navigate(`/m/login?redirect=/m/orders/${id}`)}
+ >
去登录
@@ -163,7 +167,9 @@ export function MobileOrderDetailPage() {
商品编号
区服
登录平台
-
+
预计租期
@@ -190,7 +196,13 @@ export function MobileOrderDetailPage() {
{checkout ? (
- {checkoutStatusMeta(checkout.status).label}}>
+
+ {checkoutStatusMeta(checkout.status).label}
+
+ }
+ >
结账状态
协商轮次
@@ -206,7 +218,13 @@ export function MobileOrderDetailPage() {
可退金额
号主到手
{checkout.shortfall_cent > 0 ? (
- {formatMoney(checkout.shortfall_cent)}}>
+
+ {formatMoney(checkout.shortfall_cent)}
+
+ }
+ >
押金缺口
) : null}
diff --git a/frontend/src/features/mobile/MobileOrdersPage.tsx b/frontend/src/features/mobile/MobileOrdersPage.tsx
index 6efa78e..0988d51 100644
--- a/frontend/src/features/mobile/MobileOrdersPage.tsx
+++ b/frontend/src/features/mobile/MobileOrdersPage.tsx
@@ -80,11 +80,7 @@ export function MobileOrdersPage() {
先登录,再看订单
订单、押金与结账明细只对交易双方可见。
- navigate('/m/login?redirect=/m/orders')}
- >
+ navigate('/m/login?redirect=/m/orders')}>
去登录
diff --git a/frontend/src/features/mobile/MobileRealnamePage.tsx b/frontend/src/features/mobile/MobileRealnamePage.tsx
index 62579d1..289e91a 100644
--- a/frontend/src/features/mobile/MobileRealnamePage.tsx
+++ b/frontend/src/features/mobile/MobileRealnamePage.tsx
@@ -1,15 +1,5 @@
import { useState } from 'react'
-import {
- Button,
- Dialog,
- ErrorBlock,
- Form,
- Input,
- List,
- Skeleton,
- Tag,
- Toast,
-} from 'antd-mobile'
+import { Button, Dialog, ErrorBlock, Form, Input, List, Skeleton, Tag, Toast } from 'antd-mobile'
import { CheckShieldOutline, LeftOutline } from 'antd-mobile-icons'
import { useNavigate } from 'react-router-dom'
diff --git a/frontend/src/features/mobile/MobileRegisterPage.tsx b/frontend/src/features/mobile/MobileRegisterPage.tsx
index 273fe2b..981e296 100644
--- a/frontend/src/features/mobile/MobileRegisterPage.tsx
+++ b/frontend/src/features/mobile/MobileRegisterPage.tsx
@@ -48,7 +48,7 @@ export function MobileRegisterPage() {
useEffect(() => {
const fromQuery = params.get('invite')
- let stored = ''
+ let stored: string
try {
stored = sessionStorage.getItem(INVITE_STORAGE_KEY) || ''
} catch {
@@ -131,7 +131,11 @@ export function MobileRegisterPage() {
+
diff --git a/frontend/src/features/mobile/MobileSellerListingsPage.tsx b/frontend/src/features/mobile/MobileSellerListingsPage.tsx
index 83d4e99..9601d56 100644
--- a/frontend/src/features/mobile/MobileSellerListingsPage.tsx
+++ b/frontend/src/features/mobile/MobileSellerListingsPage.tsx
@@ -6,11 +6,7 @@ import { useNavigate } from 'react-router-dom'
import { BrandLogo } from '@/components/BrandLogo'
import { listingReviewStatusMeta, listingStatusMeta } from '@/features/listings/labels'
-import {
- fetchSellerListings,
- offlineListing,
- submitListingReview,
-} from '@/features/seller/api'
+import { fetchSellerListings, offlineListing, submitListingReview } from '@/features/seller/api'
import type { SellerListing } from '@/features/seller/types'
import { useSessionStore } from '@/stores/session'
import { formatMoney } from '@/shared/utils/format'
@@ -163,7 +159,11 @@ export function MobileSellerListingsPage() {
登录后管理商品
发布、提审与下架只对当前账号开放。
- navigate('/m/login?redirect=/m/seller/listings')}>
+ navigate('/m/login?redirect=/m/seller/listings')}
+ >
去登录
@@ -252,7 +252,10 @@ export function MobileSellerListingsPage() {
{item.review_status === 'rejected' && item.review_reason ? (
驳回:{item.review_reason}
) : null}
- event.stopPropagation()}>
+
event.stopPropagation()}
+ >
{formatMoney(available)}
)}
-
navigate('/m/profile')}
- >
+ navigate('/m/profile')}>
返回我的
diff --git a/frontend/src/features/notifications/NotificationsPage.tsx b/frontend/src/features/notifications/NotificationsPage.tsx
index d9f8951..7fc6c05 100644
--- a/frontend/src/features/notifications/NotificationsPage.tsx
+++ b/frontend/src/features/notifications/NotificationsPage.tsx
@@ -55,9 +55,7 @@ export function NotificationsPage() {
- {notificationsQuery.isError ? (
-
- ) : null}
+ {notificationsQuery.isError ? : null}
{notifications.length === 0 && !notificationsQuery.isPending ? (
diff --git a/frontend/src/features/orders/OrderActionPanel.tsx b/frontend/src/features/orders/OrderActionPanel.tsx
index aabfec2..c6dabe1 100644
--- a/frontend/src/features/orders/OrderActionPanel.tsx
+++ b/frontend/src/features/orders/OrderActionPanel.tsx
@@ -313,7 +313,7 @@ export function OrderActionPanel({
if (!confirmed) return
await run('dispute', async () => {
await createOrderDispute(order.id, {
- type: actions.checkoutDisputeStage ? 'checkout_dispute' : values.disputeType ?? 'other',
+ type: actions.checkoutDisputeStage ? 'checkout_dispute' : (values.disputeType ?? 'other'),
description: values.content ?? '',
evidence_urls: evidencePayload(values),
})
diff --git a/frontend/src/features/orders/OrderDetailPage.tsx b/frontend/src/features/orders/OrderDetailPage.tsx
index c712edd..d252c96 100644
--- a/frontend/src/features/orders/OrderDetailPage.tsx
+++ b/frontend/src/features/orders/OrderDetailPage.tsx
@@ -241,9 +241,7 @@ export function OrderDetailPage() {
{formatText(order.server_region)}
- {order.estimated_duration_hours
- ? `${order.estimated_duration_hours} 小时`
- : '—'}
+ {order.estimated_duration_hours ? `${order.estimated_duration_hours} 小时` : '—'}
@@ -324,9 +322,7 @@ export function OrderDetailPage() {
{checkout.shortfall_cent > 0 ? (
-
- {formatMoney(checkout.shortfall_cent)}
-
+ {formatMoney(checkout.shortfall_cent)}
) : null}
{checkout.content ? (
@@ -341,9 +337,7 @@ export function OrderDetailPage() {
{order.active_dispute ? (
-
- #{order.active_dispute.id}
-
+ #{order.active_dispute.id}
{formatText(order.active_dispute.status)}
@@ -373,12 +367,8 @@ export function OrderDetailPage() {
{record.content}
- {record.confirmed_by_renter_at ? (
- 租客已确认
- ) : null}
- {record.confirmed_by_owner_at ? (
- 号主已确认
- ) : null}
+ {record.confirmed_by_renter_at ? 租客已确认 : null}
+ {record.confirmed_by_owner_at ? 号主已确认 : null}
))}
diff --git a/frontend/src/features/orders/OrderListPage.tsx b/frontend/src/features/orders/OrderListPage.tsx
index a1f900a..f921d4c 100644
--- a/frontend/src/features/orders/OrderListPage.tsx
+++ b/frontend/src/features/orders/OrderListPage.tsx
@@ -171,11 +171,7 @@ export function OrderListPage() {
订单、押金、交接与结账记录只对交易双方可见。
- navigate('/login?redirect=/orders')}
- >
+ navigate('/login?redirect=/orders')}>
去登录
diff --git a/frontend/src/features/orders/api.ts b/frontend/src/features/orders/api.ts
index b095193..bec3f20 100644
--- a/frontend/src/features/orders/api.ts
+++ b/frontend/src/features/orders/api.ts
@@ -34,8 +34,7 @@ export function useMyOrders(enabled = true) {
queryKey: ['my-orders'],
queryFn: ({ pageParam }) => fetchMyOrders(pageParam, MY_ORDER_PAGE_SIZE),
initialPageParam: 1,
- getNextPageParam: last =>
- last.page * last.page_size < last.total ? last.page + 1 : undefined,
+ getNextPageParam: last => (last.page * last.page_size < last.total ? last.page + 1 : undefined),
enabled,
})
}
diff --git a/frontend/src/features/profile/ProfilePage.tsx b/frontend/src/features/profile/ProfilePage.tsx
index 2d85f19..4024a33 100644
--- a/frontend/src/features/profile/ProfilePage.tsx
+++ b/frontend/src/features/profile/ProfilePage.tsx
@@ -26,13 +26,55 @@ const realnameMeta: Record
= {
}
const quickEntries = [
- { key: 'collections', label: '我的足迹', hint: '浏览记录与收藏账号', icon: , to: '/listings/collections' },
- { key: 'orders', label: '我的订单', hint: '查看租号进度与交接记录', icon: , to: '/orders' },
- { key: 'messages', label: '消息中心', hint: '账号群聊与客服会话', icon: , to: '/messages' },
- { key: 'wallet', label: '我的钱包', hint: '余额、流水与提现', icon: , to: '/wallet' },
- { key: 'seller', label: '卖家中心', hint: '发布账号与号主待办', icon: , to: '/seller/listings' },
- { key: 'invitations', label: '邀请好友', hint: '邀请码与返利记录', icon: , to: '/invitations' },
- { key: 'notifications', label: '我的通知', hint: '系统提醒与公告推送', icon: , to: '/notifications' },
+ {
+ key: 'collections',
+ label: '我的足迹',
+ hint: '浏览记录与收藏账号',
+ icon: ,
+ to: '/listings/collections',
+ },
+ {
+ key: 'orders',
+ label: '我的订单',
+ hint: '查看租号进度与交接记录',
+ icon: ,
+ to: '/orders',
+ },
+ {
+ key: 'messages',
+ label: '消息中心',
+ hint: '账号群聊与客服会话',
+ icon: ,
+ to: '/messages',
+ },
+ {
+ key: 'wallet',
+ label: '我的钱包',
+ hint: '余额、流水与提现',
+ icon: ,
+ to: '/wallet',
+ },
+ {
+ key: 'seller',
+ label: '卖家中心',
+ hint: '发布账号与号主待办',
+ icon: ,
+ to: '/seller/listings',
+ },
+ {
+ key: 'invitations',
+ label: '邀请好友',
+ hint: '邀请码与返利记录',
+ icon: ,
+ to: '/invitations',
+ },
+ {
+ key: 'notifications',
+ label: '我的通知',
+ hint: '系统提醒与公告推送',
+ icon: ,
+ to: '/notifications',
+ },
]
export function ProfilePage() {
@@ -126,7 +168,9 @@ export function ProfilePage() {
{meta.label}
- {user.credit_score != null ? 信用分 {user.credit_score} : null}
+ {user.credit_score != null ? (
+ 信用分 {user.credit_score}
+ ) : null}
{user.risk_status && user.risk_status !== 'normal' ? (
风险提示
) : null}
diff --git a/frontend/src/features/realname/RealnamePage.tsx b/frontend/src/features/realname/RealnamePage.tsx
index d8abd69..fb55ccc 100644
--- a/frontend/src/features/realname/RealnamePage.tsx
+++ b/frontend/src/features/realname/RealnamePage.tsx
@@ -1,16 +1,5 @@
import { useState } from 'react'
-import {
- Alert,
- App,
- Button,
- Card,
- Descriptions,
- Form,
- Input,
- Space,
- Tag,
- Typography,
-} from 'antd'
+import { Alert, App, Button, Card, Descriptions, Form, Input, Space, Tag, Typography } from 'antd'
import { CheckCircleOutlined, IdcardOutlined, ReloadOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
diff --git a/frontend/src/features/realname/api.ts b/frontend/src/features/realname/api.ts
index e134e0c..773972c 100644
--- a/frontend/src/features/realname/api.ts
+++ b/frontend/src/features/realname/api.ts
@@ -14,9 +14,7 @@ export interface RealnameStatus {
}
export function fetchRealnameStatus() {
- return unwrapData(
- apiClient.get>('/realname/status')
- )
+ return unwrapData(apiClient.get>('/realname/status'))
}
export function useRealnameStatus(enabled = true) {
diff --git a/frontend/src/features/seller/SellerEarningsPage.tsx b/frontend/src/features/seller/SellerEarningsPage.tsx
index 52f9a7c..d471e76 100644
--- a/frontend/src/features/seller/SellerEarningsPage.tsx
+++ b/frontend/src/features/seller/SellerEarningsPage.tsx
@@ -101,7 +101,9 @@ export function SellerEarningsPage() {
description="查看订单结算收入、线下提号结算与提现记录,全部以分为单位结算。"
meta={
- 可用 {formatMoney(balanceQuery.data?.available_balance_cent ?? 0)}
+
+ 可用 {formatMoney(balanceQuery.data?.available_balance_cent ?? 0)}
+
冻结 {formatMoney(balanceQuery.data?.frozen_balance_cent ?? 0)}
}
diff --git a/frontend/src/features/seller/SellerListingDetailPage.tsx b/frontend/src/features/seller/SellerListingDetailPage.tsx
index cde3dbe..8efff63 100644
--- a/frontend/src/features/seller/SellerListingDetailPage.tsx
+++ b/frontend/src/features/seller/SellerListingDetailPage.tsx
@@ -261,10 +261,7 @@ export function SellerListingDetailPage({ mobile = false }: { mobile?: boolean }
- navigate(`/login?redirect=${basePath}/${id}`)}
- >
+ navigate(`/login?redirect=${basePath}/${id}`)}>
去登录
@@ -313,7 +310,9 @@ export function SellerListingDetailPage({ mobile = false }: { mobile?: boolean }
const screenshots = screenshotsOf(listing)
const skinGroups = skinGroupsOf(listing)
const resources = Array.isArray(summary.resources)
- ? summary.resources.map(item => recordValue(item)).filter(item => numberValue(item.quantity) > 0)
+ ? summary.resources
+ .map(item => recordValue(item))
+ .filter(item => numberValue(item.quantity) > 0)
: []
const commonRegions = stringArray(summary.common_regions)
@@ -432,7 +431,10 @@ export function SellerListingDetailPage({ mobile = false }: { mobile?: boolean }
参考比例
- {ratioText(breakdown.seller_reference_ratio_bps, breakdown.seller_reference_ratio)}
+ {ratioText(
+ breakdown.seller_reference_ratio_bps,
+ breakdown.seller_reference_ratio
+ )}
@@ -443,14 +445,19 @@ export function SellerListingDetailPage({ mobile = false }: { mobile?: boolean }
纯币基础价
{formatMoney(
- moneyCent(breakdown.seller_coin_base_price_cent, breakdown.seller_coin_base_price)
+ moneyCent(
+ breakdown.seller_coin_base_price_cent,
+ breakdown.seller_coin_base_price
+ )
)}
消耗品价值
- {formatMoney(moneyCent(breakdown.consumable_price_cent, breakdown.consumable_price))}
+ {formatMoney(
+ moneyCent(breakdown.consumable_price_cent, breakdown.consumable_price)
+ )}
diff --git a/frontend/src/features/seller/SellerListingFormPage.tsx b/frontend/src/features/seller/SellerListingFormPage.tsx
index 8061aa3..c0dd980 100644
--- a/frontend/src/features/seller/SellerListingFormPage.tsx
+++ b/frontend/src/features/seller/SellerListingFormPage.tsx
@@ -123,11 +123,7 @@ function ratioInputFromBreakdown(
function toTimeValue(value: string): Dayjs | null {
const match = /^(\d{1,2}):(\d{2})/.exec(value.trim())
if (!match) return null
- return dayjs()
- .hour(Number(match[1]))
- .minute(Number(match[2]))
- .second(0)
- .millisecond(0)
+ return dayjs().hour(Number(match[1])).minute(Number(match[2])).second(0).millisecond(0)
}
function parseTimeMinute(value: string) {
@@ -502,7 +498,7 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
message.error('该商品订单已完成,不能再次编辑')
navigate(basePath)
}
- }, [isEdit, listing, message, navigate])
+ }, [basePath, isEdit, listing, message, navigate])
const screenshotSlots: PublishScreenshotSlot[] =
options.screenshot_slots.length > 0
@@ -521,12 +517,12 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
return Array.from(new Set(urls))
}, [screenshotFiles])
- const totalScreenshotCount = Object.values(screenshotFiles)
- .flat()
- .filter(Boolean).length
+ const totalScreenshotCount = Object.values(screenshotFiles).flat().filter(Boolean).length
const pricing = useMemo(() => {
- const hafCoinAmount = parseHafCoinMToUnits(form.haf_coin_amount === '' ? 0 : form.haf_coin_amount)
+ const hafCoinAmount = parseHafCoinMToUnits(
+ form.haf_coin_amount === '' ? 0 : form.haf_coin_amount
+ )
const coinMAmount = hafCoinUnitsToM(hafCoinAmount)
const dailyLossMAmount = Math.trunc(Number(form.daily_loss_m || 10))
const dailyLossRatioAdjustmentBPS = calculateDailyLossRatioAdjustmentBPS(dailyLossMAmount)
@@ -590,7 +586,9 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
}
}, [form, options, salePriceConfig, quantityValues, quantityModes, selectedSkins])
- const depositAmountCent = parseYuanToCent(form.depositAmountYuan === '' ? '' : form.depositAmountYuan)
+ const depositAmountCent = parseYuanToCent(
+ form.depositAmountYuan === '' ? '' : form.depositAmountYuan
+ )
const depositBelowRecommended =
pricing.recommendedDepositCent > 0 &&
(!Number.isFinite(depositAmountCent) || depositAmountCent < pricing.recommendedDepositCent)
@@ -808,16 +806,10 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
return '请填写押金'
}
if (depositAmountCent < 0) return '押金格式不正确'
- if (
- pricing.recommendedDepositCent > 0 &&
- depositAmountCent < pricing.recommendedDepositCent
- ) {
+ if (pricing.recommendedDepositCent > 0 && depositAmountCent < pricing.recommendedDepositCent) {
return `押金不能低于智能推荐 ${formatMoney(pricing.recommendedDepositCent)}`
}
- if (
- pricing.consumablePriceCent > 0 &&
- depositAmountCent <= pricing.consumablePriceCent
- ) {
+ if (pricing.consumablePriceCent > 0 && depositAmountCent <= pricing.consumablePriceCent) {
return `押金必须大于额外消耗品总价值 ${formatMoney(pricing.consumablePriceCent)}`
}
if (pricing.finalPriceCent <= 0) {
@@ -829,10 +821,7 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
if (pricing.referenceRatioBPS > 0 && ratioBPS < pricing.referenceRatioBPS) {
return `加速出售比例不能低于参考比例 1:${formatRatioBPS(pricing.referenceRatioBPS)}`
}
- if (
- pricing.maxAcceleratedSaleRatioBPS > 0 &&
- ratioBPS > pricing.maxAcceleratedSaleRatioBPS
- ) {
+ if (pricing.maxAcceleratedSaleRatioBPS > 0 && ratioBPS > pricing.maxAcceleratedSaleRatioBPS) {
return `加速出售比例不能超过 1:${formatRatioBPS(pricing.maxAcceleratedSaleRatioBPS)}`
}
}
@@ -1057,7 +1046,8 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
/>
) : null}
- {options.server_options.length === 0 || options.ratio_config.insurance_base_ratios.length === 0 ? (
+ {options.server_options.length === 0 ||
+ options.ratio_config.insurance_base_ratios.length === 0 ? (
{
updateForm('online_start', '00:00')
updateForm('online_end', '23:59')
@@ -1368,13 +1362,11 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
className={`seller-pill seller-skin-pill${active ? ' is-active' : ''}`}
onClick={() => {
const current = group.options.filter(item =>
- selectedSkins.includes(item),
+ selectedSkins.includes(item)
)
toggleGroupSkin(
group.key,
- active
- ? current.filter(item => item !== skin)
- : [...current, skin],
+ active ? current.filter(item => item !== skin) : [...current, skin]
)
}}
>
@@ -1415,7 +1407,12 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
{urls.map((url, index) => (
-
+
triggerUpload(slot.key)}
>
-
- {uploadingSlot === slot.key ? '上传中…' : '上传截图'}
-
+ {uploadingSlot === slot.key ? '上传中…' : '上传截图'}
@@ -1499,9 +1494,7 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
0
? `默认 ${formatRatioBPS(pricing.referenceRatioBPS)},最高 ${formatRatioBPS(pricing.maxAcceleratedSaleRatioBPS)}`
@@ -1593,7 +1586,9 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
@@ -648,7 +672,13 @@ export function AdminLayout() {
)
}
-export function AdminSectionPlaceholder({ title, description }: { title: string; description: string }) {
+export function AdminSectionPlaceholder({
+ title,
+ description,
+}: {
+ title: string
+ description: string
+}) {
return (
diff --git a/frontend/src/layouts/MobileLayout.tsx b/frontend/src/layouts/MobileLayout.tsx
index c4f3cbb..e67e039 100644
--- a/frontend/src/layouts/MobileLayout.tsx
+++ b/frontend/src/layouts/MobileLayout.tsx
@@ -57,11 +57,7 @@ export function MobileLayout() {
-
navigate(key)}
- safeArea
- >
+ navigate(key)} safeArea>
{tabs.map(tab => (
· JIEYOU TRADING HOUSE
租得明白,还得安心。
-
- © 2026 解忧商行 · 备案信息与客服信息待配置
-
+
© 2026 解忧商行 · 备案信息与客服信息待配置
diff --git a/frontend/src/shared/api/files.ts b/frontend/src/shared/api/files.ts
index c5c1b96..9ed9fc8 100644
--- a/frontend/src/shared/api/files.ts
+++ b/frontend/src/shared/api/files.ts
@@ -30,16 +30,13 @@ export async function uploadAdminFile(
scene: string,
options: { optimize?: boolean } = {}
) {
- const uploadTarget =
- options.optimize === false ? file : await optimizeImageForUpload(file, scene)
+ const uploadTarget = options.optimize === false ? file : await optimizeImageForUpload(file, scene)
const form = new FormData()
form.append('file', uploadTarget)
form.append('scene', scene)
- const { data } = await adminClient.post
>(
- '/admin/files/upload',
- form,
- { headers: { 'Content-Type': 'multipart/form-data' } }
- )
+ const { data } = await adminClient.post>('/admin/files/upload', form, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ })
return data.data
}
diff --git a/frontend/src/shared/utils/format.test.ts b/frontend/src/shared/utils/format.test.ts
new file mode 100644
index 0000000..819e93a
--- /dev/null
+++ b/frontend/src/shared/utils/format.test.ts
@@ -0,0 +1,44 @@
+import { describe, expect, it } from 'vitest'
+
+import {
+ emptyText,
+ formatCoinM,
+ formatDate,
+ formatDateTime,
+ formatMoney,
+ formatNumber,
+ formatText,
+ maskPhone,
+} from '@/shared/utils/format'
+
+describe('format utils', () => {
+ it('formats integer cents as yuan without floating-point multiplication', () => {
+ expect(formatMoney(0)).toBe('¥0.00')
+ expect(formatMoney(12345)).toBe('¥123.45')
+ expect(formatMoney(100_000_00)).toBe('¥100,000.00')
+ expect(formatMoney(null)).toBe(emptyText)
+ expect(formatMoney(Number.NaN)).toBe(emptyText)
+ })
+
+ it('formats hafu coin units as M with one decimal place', () => {
+ expect(formatCoinM(100_000_000)).toBe('100M')
+ expect(formatCoinM(1_500_000)).toBe('1.5M')
+ expect(formatCoinM(0)).toBe('0M')
+ expect(formatCoinM(null)).toBe(emptyText)
+ })
+
+ it('formats dates and empty text consistently', () => {
+ expect(formatDateTime('2026-11-01T12:30:00+08:00')).toBe('2026-11-01 12:30')
+ expect(formatDate('2026-11-01T12:30:00+08:00')).toBe('2026-11-01')
+ expect(formatDateTime('')).toBe(emptyText)
+ expect(formatNumber(1234567)).toBe('1,234,567')
+ expect(formatText('')).toBe(emptyText)
+ expect(formatText(0)).toBe('0')
+ })
+
+ it('masks phone numbers without leaking the middle digits', () => {
+ expect(maskPhone('13800138000')).toBe('138****8000')
+ expect(maskPhone('123456')).toBe('123456')
+ expect(maskPhone('')).toBe(emptyText)
+ })
+})
diff --git a/frontend/src/shared/utils/pricing.test.ts b/frontend/src/shared/utils/pricing.test.ts
new file mode 100644
index 0000000..59742c4
--- /dev/null
+++ b/frontend/src/shared/utils/pricing.test.ts
@@ -0,0 +1,94 @@
+import { describe, expect, it } from 'vitest'
+
+import {
+ bpsToRatio,
+ calculateConsumablePriceCent,
+ calculateSellerCoinBasePriceCent,
+ calculateSellerReferenceRatioBPS,
+ emptyPlatformPricingCent,
+ formatRatioBPS,
+ hafCoinUnitsToM,
+ parseHafCoinMToUnits,
+ parseUnitPriceCent,
+ parseYuanToCent,
+ ratioToBPS,
+ readFinalSaleRatioBPS,
+} from '@/shared/utils/pricing'
+
+describe('pricing integer and BPS helpers', () => {
+ it('parses yuan input to integer cents with half-up boundary rounding', () => {
+ expect(parseYuanToCent('12.34')).toBe(1234)
+ expect(parseYuanToCent('12.345')).toBe(1235)
+ expect(parseYuanToCent(0)).toBe(0)
+ expect(parseYuanToCent('-1')).toBe(Number.NaN)
+ expect(parseYuanToCent('abc')).toBe(Number.NaN)
+ })
+
+ it('converts hafu coin M values without floating-point multiplication', () => {
+ expect(parseHafCoinMToUnits('104.3')).toBe(104_300_000)
+ expect(parseHafCoinMToUnits('0.000001')).toBe(1)
+ expect(hafCoinUnitsToM(104_300_000)).toBe(104.3)
+ expect(parseHafCoinMToUnits('-1')).toBe(0)
+ })
+
+ it('converts and displays BPS ratios', () => {
+ expect(ratioToBPS('1.5')).toBe(15_000)
+ expect(ratioToBPS('-1')).toBe(0)
+ expect(bpsToRatio(43_500)).toBe(4.35)
+ expect(formatRatioBPS(435_000)).toBe('43.5')
+ expect(formatRatioBPS(0)).toBe('0.0')
+ })
+
+ it('parses unit prices and consumable prices in cents', () => {
+ expect(parseUnitPriceCent('8元/个')).toBe(800)
+ expect(parseUnitPriceCent('8元/2个')).toBe(400)
+ expect(parseUnitPriceCent('8元')).toBe(800)
+
+ const total = calculateConsumablePriceCent({
+ quantityItems: [
+ { key: 'awmAmmo', label: 'AWM子弹', price: '8元/个' },
+ { key: 'armor6', label: '六甲', price: '20元', price_cent: 2000 },
+ ],
+ quantityValues: { awmAmmo: 3, armor6: 2 },
+ quantityModes: { awmAmmo: '收费', armor6: '收费' },
+ seasonInsurance: '2*3',
+ })
+ expect(total).toBe(6400)
+ })
+
+ it('keeps seller ratio and accelerated ratio within integer BPS bounds', () => {
+ expect(calculateSellerCoinBasePriceCent(100_000_000, 400_000)).toBe(25_000)
+ expect(readFinalSaleRatioBPS(400_000, '', 500_000)).toBe(400_000)
+ expect(readFinalSaleRatioBPS(400_000, 60, 500_000)).toBe(500_000)
+ expect(readFinalSaleRatioBPS(400_000, 30, 500_000)).toBe(400_000)
+ expect(emptyPlatformPricingCent()).toEqual({
+ buyerCoinBasePriceCent: 0,
+ buyerTotalPriceCent: 0,
+ buyerRatioBPS: 0,
+ platformMarkupAmountCent: 0,
+ ruleType: 'none',
+ })
+ })
+
+ it('returns zero reference ratio when mandatory asset fields are missing', () => {
+ expect(
+ calculateSellerReferenceRatioBPS({
+ hafCoinAmount: 100_000_000,
+ form: {
+ season_insurance: '',
+ stamina_level: '7级',
+ load_level: '7级',
+ } as never,
+ ratioConfig: {
+ insurance_base_ratios: [],
+ config_items: [],
+ coin_corrections: [],
+ },
+ skinGroups: [],
+ selectedSkins: [],
+ levelOptions: [],
+ dailyLossRatioAdjustmentBPS: 0,
+ })
+ ).toBe(0)
+ })
+})
diff --git a/frontend/src/shared/utils/pricing.ts b/frontend/src/shared/utils/pricing.ts
index 8762666..0aad682 100644
--- a/frontend/src/shared/utils/pricing.ts
+++ b/frontend/src/shared/utils/pricing.ts
@@ -236,9 +236,7 @@ export function readFinalSaleRatioBPS(
}
export function maxAcceleratedSaleRatioBPS(referenceRatioBPS: number) {
- return referenceRatioBPS > 0
- ? referenceRatioBPS + MAX_ACCELERATED_RATIO_BPS_INCREMENT
- : 0
+ return referenceRatioBPS > 0 ? referenceRatioBPS + MAX_ACCELERATED_RATIO_BPS_INCREMENT : 0
}
export function hasAcceleratedSaleRatioInput(value: number | '' | string | null | undefined) {
@@ -380,7 +378,9 @@ function getInsuranceBaseRatioBPS(
) {
const item = config.insurance_base_ratios.find(row => row.insurance === insurance)
if (!item) return 0
- return Number(item.ratio_bps || 0) > 0 ? Math.trunc(Number(item.ratio_bps)) : ratioToBPS(item.ratio)
+ return Number(item.ratio_bps || 0) > 0
+ ? Math.trunc(Number(item.ratio_bps))
+ : ratioToBPS(item.ratio)
}
function calculateConfigPenaltyBPS(
diff --git a/frontend/src/stores/adminSession.ts b/frontend/src/stores/adminSession.ts
index 919f8c7..46fbf4a 100644
--- a/frontend/src/stores/adminSession.ts
+++ b/frontend/src/stores/adminSession.ts
@@ -23,7 +23,12 @@ interface AdminSessionState {
admin: AdminUser | null
loaded: boolean
loading: boolean
- login: (username: string, password: string, captchaId: string, captchaCode: string) => Promise
+ login: (
+ username: string,
+ password: string,
+ captchaId: string,
+ captchaCode: string
+ ) => Promise
loadMe: () => Promise
logout: () => Promise
changePassword: (oldPassword: string, newPassword: string) => Promise
diff --git a/frontend/src/styles/admin-login.css b/frontend/src/styles/admin-login.css
index a7c4edc..26d5586 100644
--- a/frontend/src/styles/admin-login.css
+++ b/frontend/src/styles/admin-login.css
@@ -83,7 +83,9 @@
border: 1px solid var(--line);
border-radius: 8px;
background: var(--paper-warm);
- box-shadow: 0 1px 2px rgba(29, 36, 33, 0.04), 0 20px 60px rgba(29, 36, 33, 0.06);
+ box-shadow:
+ 0 1px 2px rgba(29, 36, 33, 0.04),
+ 0 20px 60px rgba(29, 36, 33, 0.06);
}
.admin-login-card__head {
@@ -127,4 +129,3 @@
padding: 0;
color: var(--brand-ink);
}
-
diff --git a/frontend/src/styles/admin-misc.css b/frontend/src/styles/admin-misc.css
index 4c08f25..44ddfeb 100644
--- a/frontend/src/styles/admin-misc.css
+++ b/frontend/src/styles/admin-misc.css
@@ -209,5 +209,3 @@
grid-template-columns: 1fr;
}
}
-
-
diff --git a/frontend/src/styles/admin-shell.css b/frontend/src/styles/admin-shell.css
index 6ff9dd7..97005c7 100644
--- a/frontend/src/styles/admin-shell.css
+++ b/frontend/src/styles/admin-shell.css
@@ -2162,4 +2162,3 @@
grid-template-columns: 1fr;
}
}
-
diff --git a/frontend/src/styles/auth.css b/frontend/src/styles/auth.css
index 4338143..5739f2a 100644
--- a/frontend/src/styles/auth.css
+++ b/frontend/src/styles/auth.css
@@ -97,7 +97,9 @@
border: 1px solid var(--line);
border-radius: 8px;
background: var(--paper-warm);
- box-shadow: 0 1px 2px rgba(29, 36, 33, 0.04), 0 20px 60px rgba(29, 36, 33, 0.06);
+ box-shadow:
+ 0 1px 2px rgba(29, 36, 33, 0.04),
+ 0 20px 60px rgba(29, 36, 33, 0.06);
}
.login-form-card__top {
@@ -175,4 +177,3 @@
flex: 0 0 124px;
height: 40px;
}
-
diff --git a/frontend/src/styles/base.css b/frontend/src/styles/base.css
index f8278eb..2382ec3 100644
--- a/frontend/src/styles/base.css
+++ b/frontend/src/styles/base.css
@@ -30,8 +30,7 @@ body {
min-width: 320px;
background: var(--paper);
color: var(--ink);
- font-family:
- 'Noto Sans SC', 'Source Han Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
+ font-family: 'Noto Sans SC', 'Source Han Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
-webkit-font-smoothing: antialiased;
}
@@ -138,4 +137,3 @@ a {
color: var(--ink-muted);
font-size: 12px;
}
-
diff --git a/frontend/src/styles/components.css b/frontend/src/styles/components.css
index d670a4c..ea8779c 100644
--- a/frontend/src/styles/components.css
+++ b/frontend/src/styles/components.css
@@ -59,7 +59,9 @@
justify-content: center;
gap: 6px;
cursor: pointer;
- transition: border-color 0.2s ease, color 0.2s ease;
+ transition:
+ border-color 0.2s ease,
+ color 0.2s ease;
}
.evidence-upload__add:hover:not(:disabled) {
@@ -85,5 +87,3 @@
margin-top: 6px;
font-size: 12px;
}
-
-
diff --git a/frontend/src/styles/mobile-base.css b/frontend/src/styles/mobile-base.css
index c10b667..7f626f0 100644
--- a/frontend/src/styles/mobile-base.css
+++ b/frontend/src/styles/mobile-base.css
@@ -390,4 +390,3 @@
font-size: 12px;
line-height: 1.8;
}
-
diff --git a/frontend/src/styles/mobile-chat.css b/frontend/src/styles/mobile-chat.css
index 96a618c..7f22f3f 100644
--- a/frontend/src/styles/mobile-chat.css
+++ b/frontend/src/styles/mobile-chat.css
@@ -258,4 +258,3 @@
box-shadow: 0 0 60px rgba(29, 36, 33, 0.08);
}
}
-
diff --git a/frontend/src/styles/mobile-listing.css b/frontend/src/styles/mobile-listing.css
index a553012..a58c015 100644
--- a/frontend/src/styles/mobile-listing.css
+++ b/frontend/src/styles/mobile-listing.css
@@ -405,4 +405,3 @@
color: var(--brand-seal);
font-weight: 600;
}
-
diff --git a/frontend/src/styles/mobile-messages.css b/frontend/src/styles/mobile-messages.css
index 13fc153..6c87e6a 100644
--- a/frontend/src/styles/mobile-messages.css
+++ b/frontend/src/styles/mobile-messages.css
@@ -188,4 +188,3 @@
color: var(--ink-muted);
font-size: 11px;
}
-
diff --git a/frontend/src/styles/mobile-order-detail.css b/frontend/src/styles/mobile-order-detail.css
index 18a156a..8d83226 100644
--- a/frontend/src/styles/mobile-order-detail.css
+++ b/frontend/src/styles/mobile-order-detail.css
@@ -1,4 +1,3 @@
-
.mobile-order-detail {
padding: 16px 16px 36px;
}
@@ -261,4 +260,3 @@
gap: 10px;
margin-top: 18px;
}
-
diff --git a/frontend/src/styles/seller.css b/frontend/src/styles/seller.css
index 9a60739..c1f2e22 100644
--- a/frontend/src/styles/seller.css
+++ b/frontend/src/styles/seller.css
@@ -45,9 +45,7 @@
padding: 24px 26px;
border: 1px solid var(--line);
border-radius: 10px;
- background:
- linear-gradient(120deg, rgba(30, 77, 66, 0.07), transparent 46%),
- var(--paper-warm);
+ background: linear-gradient(120deg, rgba(30, 77, 66, 0.07), transparent 46%), var(--paper-warm);
}
.seller-form-head,
@@ -771,9 +769,7 @@ button.seller-code:not(:disabled):hover {
.seller-price-panel {
border-color: var(--brand-seal-line);
- background:
- linear-gradient(160deg, rgba(166, 67, 50, 0.06), transparent 42%),
- var(--paper-warm);
+ background: linear-gradient(160deg, rgba(166, 67, 50, 0.06), transparent 42%), var(--paper-warm);
}
.seller-price-panel__main {
diff --git a/frontend/src/styles/storefront-content.css b/frontend/src/styles/storefront-content.css
index 7e31b6f..343bf26 100644
--- a/frontend/src/styles/storefront-content.css
+++ b/frontend/src/styles/storefront-content.css
@@ -47,4 +47,3 @@
.rules-panel .ant-typography {
max-width: 720px;
}
-
diff --git a/frontend/src/styles/storefront-listings.css b/frontend/src/styles/storefront-listings.css
index dd59d15..36731fa 100644
--- a/frontend/src/styles/storefront-listings.css
+++ b/frontend/src/styles/storefront-listings.css
@@ -39,9 +39,7 @@
align-items: center;
justify-content: center;
gap: 12px;
- background:
- linear-gradient(0deg, rgba(30, 77, 66, 0.04), rgba(30, 77, 66, 0.04)),
- var(--paper);
+ background: linear-gradient(0deg, rgba(30, 77, 66, 0.04), rgba(30, 77, 66, 0.04)), var(--paper);
color: var(--brand-ink);
font-family: 'Noto Serif SC', 'Songti SC', serif;
letter-spacing: 0.12em;
@@ -604,4 +602,3 @@
justify-content: center;
margin-top: 32px;
}
-
diff --git a/frontend/src/styles/storefront-sections.css b/frontend/src/styles/storefront-sections.css
index a9a22d0..86dc2e3 100644
--- a/frontend/src/styles/storefront-sections.css
+++ b/frontend/src/styles/storefront-sections.css
@@ -93,4 +93,3 @@
color: var(--brand-ink) !important;
font-size: 32px !important;
}
-
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
new file mode 100644
index 0000000..b0bcc28
--- /dev/null
+++ b/frontend/vitest.config.ts
@@ -0,0 +1,25 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ },
+ },
+ test: {
+ environment: 'node',
+ include: ['src/**/*.test.{ts,tsx}'],
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'json-summary'],
+ reportsDirectory: 'coverage',
+ include: [
+ 'src/shared/utils/format.ts',
+ 'src/shared/utils/pricing.ts',
+ 'src/features/admin/listingDisplay.ts',
+ ],
+ },
+ },
+})
diff --git a/scripts/check-frontend-size.sh b/scripts/check-frontend-size.sh
new file mode 100755
index 0000000..72de4d4
--- /dev/null
+++ b/scripts/check-frontend-size.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+MAX_LINES="${FRONTEND_MAX_LINES:-1800}"
+failed=0
+
+while IFS= read -r -d '' file; do
+ lines="$(wc -l < "${file}")"
+ if ((lines > MAX_LINES)); then
+ printf '[frontend-size] %s %s 行,超过上限 %s\n' "${file#${ROOT_DIR}/}" "${lines}" "${MAX_LINES}" >&2
+ failed=1
+ fi
+done < <(find "${ROOT_DIR}/frontend/src" -name '*.tsx' -not -name '*.test.tsx' -print0)
+
+if ((failed != 0)); then
+ exit 1
+fi
+printf '[frontend-size] 所有 React 页面均不超过 %s 行\n' "${MAX_LINES}"
diff --git a/scripts/check.sh b/scripts/check.sh
index 51b2bf6..51bce81 100755
--- a/scripts/check.sh
+++ b/scripts/check.sh
@@ -8,7 +8,7 @@
# 包含:
# 数据库:SQL 约束与迁移完整性检查
# 后端:go vet + go build + go test
-# 前端:npm run check (prettier + eslint + typecheck) + npm run test + npm run build
+# 前端:npm run check (prettier + eslint + typecheck) + npm run test:coverage + npm run build
#
# 任一步骤失败即退出非零,阻止部署。
set -euo pipefail
@@ -113,13 +113,13 @@ if [[ "${SKIP_FRONTEND}" == "0" ]]; then
log "前端检查目录: ${FRONTEND_DIR}"
cd "${FRONTEND_DIR}"
- run_step "Vue 页面大小门禁" bash "${ROOT_DIR}/scripts/check-vue-size.sh" || FAILED=1
+ run_step "React 页面大小门禁" bash "${ROOT_DIR}/scripts/check-frontend-size.sh" || FAILED=1
if [[ ! -d node_modules ]]; then
run_step "npm ci (首次安装依赖)" npm ci || FAILED=1
fi
- # npm run check = prettier --check + eslint --max-warnings=0 + vue-tsc
+ # npm run check = prettier --check + eslint --max-warnings=0 + tsc --noEmit
run_step "npm run check (prettier + eslint + typecheck)" npm run check || FAILED=1
if [[ "${SKIP_TESTS}" == "0" ]]; then
run_step "npm run test + coverage (vitest)" npm run test:coverage || FAILED=1