From 6056afa436a7cc251139fb99f2164fe4ed812d65 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 1 Nov 2026 20:54:45 +0800 Subject: [PATCH] =?UTF-8?q?chore(frontend):=20=E6=8E=A5=E5=85=A5=20ESLint?= =?UTF-8?q?=E3=80=81Prettier=20=E4=B8=8E=20Vitest=20=E8=B4=A8=E9=87=8F?= =?UTF-8?q?=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .githooks/pre-push | 10 + frontend/.prettierignore | 4 + frontend/.prettierrc.json | 8 + frontend/eslint.config.js | 32 + frontend/package-lock.json | 1688 ++++++++++++++++- frontend/package.json | 19 +- frontend/src/components/EvidenceUpload.tsx | 8 +- .../features/admin/AdminAnnouncementsPage.tsx | 263 ++- .../src/features/admin/AdminAuditLogsPage.tsx | 12 +- .../src/features/admin/AdminBackupsPage.tsx | 19 +- .../features/admin/AdminChatWelcomePage.tsx | 7 +- .../src/features/admin/AdminChatsPage.tsx | 6 +- .../admin/AdminCrashCategoriesPage.tsx | 21 +- .../features/admin/AdminCrashConfigPage.tsx | 4 +- .../features/admin/AdminCrashOrdersPage.tsx | 32 +- .../features/admin/AdminCrashProductsPage.tsx | 30 +- .../src/features/admin/AdminDashboardPage.tsx | 14 +- .../src/features/admin/AdminDisputesPage.tsx | 4 +- .../admin/AdminDistributionDashboardPage.tsx | 8 +- .../admin/AdminDistributionInvitationPage.tsx | 4 +- .../admin/AdminDistributionListingsPage.tsx | 52 +- .../admin/AdminDistributionOrdersPage.tsx | 95 +- .../AdminDistributionPaymentChannelsPage.tsx | 16 +- .../admin/AdminDistributionQrCodesPage.tsx | 237 ++- .../admin/AdminDistributionRulesPage.tsx | 2 +- .../AdminDistributionSettlementsPage.tsx | 12 +- .../admin/AdminDistributionSitesPage.tsx | 25 +- .../admin/AdminDistributionUsersPage.tsx | 37 +- .../admin/AdminDistributionWalletPage.tsx | 7 +- .../admin/AdminFinanceDetailsPage.tsx | 12 +- .../admin/AdminFinanceDisbursementsPage.tsx | 48 +- .../src/features/admin/AdminFinancePage.tsx | 25 +- .../features/admin/AdminInvitationsPage.tsx | 43 +- .../src/features/admin/AdminListingsPage.tsx | 43 +- .../src/features/admin/AdminLoginPage.tsx | 13 +- .../features/admin/AdminManualEntriesPage.tsx | 16 +- .../features/admin/AdminNotificationsPage.tsx | 7 +- .../admin/AdminOperatingExpensesPage.tsx | 31 +- .../features/admin/AdminOrderDetailDrawer.tsx | 90 +- .../src/features/admin/AdminOrdersPage.tsx | 8 +- .../admin/AdminPaymentConfigsPage.tsx | 37 +- .../src/features/admin/AdminPaymentsPage.tsx | 7 +- .../features/admin/AdminPickupDetailPage.tsx | 57 +- .../src/features/admin/AdminPickupsPage.tsx | 24 +- frontend/src/features/admin/AdminPushPage.tsx | 4 +- .../features/admin/AdminQrCodePoolPage.tsx | 23 +- .../features/admin/AdminQuickRepliesPage.tsx | 7 +- .../src/features/admin/AdminRolesPage.tsx | 10 +- .../admin/AdminSiteAnnouncementsPage.tsx | 20 +- .../features/admin/AdminStaffAccountsPage.tsx | 14 +- .../features/admin/AdminSupportGroupsPage.tsx | 9 +- .../features/admin/AdminSystemConfigsPage.tsx | 8 +- .../features/admin/AdminTrackingLinksPage.tsx | 31 +- .../features/admin/AdminUserDetailDrawer.tsx | 26 +- .../src/features/admin/AdminUsersPage.tsx | 4 +- .../features/admin/AdminWalletLedgerPage.tsx | 10 +- .../features/admin/AdminWithdrawalsPage.tsx | 34 +- frontend/src/features/admin/api.ts | 146 +- .../features/admin/components/DataTable.tsx | 6 +- .../components/DistributionSiteSelect.tsx | 7 +- .../features/admin/components/JsonEditor.tsx | 24 +- .../admin/components/PublishOptionsEditor.tsx | 3 +- .../admin/components/StructuredJsonEditor.tsx | 22 +- frontend/src/features/admin/labels.ts | 36 +- .../src/features/admin/listingDisplay.test.ts | 128 ++ frontend/src/features/admin/types.ts | 9 +- .../announcements/AnnouncementDetailPage.tsx | 7 +- .../announcements/AnnouncementsPage.tsx | 10 +- frontend/src/features/announcements/api.ts | 4 +- frontend/src/features/auth/LoginPage.tsx | 20 +- frontend/src/features/auth/RegisterPage.tsx | 18 +- .../src/features/crash/CrashDetailPage.tsx | 29 +- frontend/src/features/crash/CrashListPage.tsx | 27 +- .../src/features/crash/CrashOrdersPage.tsx | 23 +- frontend/src/features/crash/api.ts | 14 +- frontend/src/features/help/ProcessPage.tsx | 6 +- frontend/src/features/home/HomePortalRow.tsx | 13 +- frontend/src/features/home/api.ts | 4 +- .../features/invitations/InvitationsPage.tsx | 107 +- .../invitations/InviteLandingPage.tsx | 5 +- frontend/src/features/invitations/api.ts | 18 +- .../listings/ListingCollectionsPage.tsx | 4 +- .../features/listings/ListingDetailPage.tsx | 21 +- .../features/listings/ListingFilterPanel.tsx | 162 +- .../src/features/listings/ListingListPage.tsx | 81 +- frontend/src/features/listings/collections.ts | 4 +- frontend/src/features/listings/labels.ts | 4 +- frontend/src/features/messages/ChatPage.tsx | 6 +- .../src/features/messages/MessagesPage.tsx | 11 +- frontend/src/features/messages/api.ts | 23 +- .../mobile/MobileAddHomeGuidePage.tsx | 15 +- .../src/features/mobile/MobileChatPage.tsx | 4 +- .../features/mobile/MobileCrashDetailPage.tsx | 12 +- .../features/mobile/MobileCrashListPage.tsx | 13 +- .../features/mobile/MobileCrashOrdersPage.tsx | 10 +- .../src/features/mobile/MobileHomePage.tsx | 18 +- .../mobile/MobileListingDetailPage.tsx | 18 +- .../features/mobile/MobileMessagesPage.tsx | 7 +- .../mobile/MobileNotificationsPage.tsx | 6 +- .../mobile/MobileOrderActionPanel.tsx | 91 +- .../features/mobile/MobileOrderDetailPage.tsx | 26 +- .../src/features/mobile/MobileOrdersPage.tsx | 6 +- .../features/mobile/MobileRealnamePage.tsx | 12 +- .../features/mobile/MobileRegisterPage.tsx | 8 +- .../mobile/MobileSellerListingsPage.tsx | 17 +- .../features/mobile/MobileWithdrawalPage.tsx | 7 +- .../notifications/NotificationsPage.tsx | 4 +- .../src/features/orders/OrderActionPanel.tsx | 2 +- .../src/features/orders/OrderDetailPage.tsx | 20 +- .../src/features/orders/OrderListPage.tsx | 6 +- frontend/src/features/orders/api.ts | 3 +- frontend/src/features/profile/ProfilePage.tsx | 60 +- .../src/features/realname/RealnamePage.tsx | 13 +- frontend/src/features/realname/api.ts | 4 +- .../features/seller/SellerEarningsPage.tsx | 4 +- .../seller/SellerListingDetailPage.tsx | 23 +- .../features/seller/SellerListingFormPage.tsx | 73 +- .../features/seller/SellerListingsPage.tsx | 41 +- .../src/features/seller/SellerPickupsPage.tsx | 9 +- frontend/src/features/seller/api.ts | 8 +- frontend/src/features/seller/options.ts | 8 +- .../features/wallet/PaymentAccountsPage.tsx | 4 +- .../src/features/wallet/WithdrawalPage.tsx | 10 +- frontend/src/features/wallet/types.ts | 7 +- frontend/src/layouts/AdminLayout.tsx | 68 +- frontend/src/layouts/MobileLayout.tsx | 6 +- frontend/src/layouts/StorefrontLayout.tsx | 4 +- frontend/src/shared/api/files.ts | 11 +- frontend/src/shared/utils/format.test.ts | 44 + frontend/src/shared/utils/pricing.test.ts | 94 + frontend/src/shared/utils/pricing.ts | 8 +- frontend/src/stores/adminSession.ts | 7 +- frontend/src/styles/admin-login.css | 5 +- frontend/src/styles/admin-misc.css | 2 - frontend/src/styles/admin-shell.css | 1 - frontend/src/styles/auth.css | 5 +- frontend/src/styles/base.css | 4 +- frontend/src/styles/components.css | 6 +- frontend/src/styles/mobile-base.css | 1 - frontend/src/styles/mobile-chat.css | 1 - frontend/src/styles/mobile-listing.css | 1 - frontend/src/styles/mobile-messages.css | 1 - frontend/src/styles/mobile-order-detail.css | 2 - frontend/src/styles/seller.css | 8 +- frontend/src/styles/storefront-content.css | 1 - frontend/src/styles/storefront-listings.css | 5 +- frontend/src/styles/storefront-sections.css | 1 - frontend/vitest.config.ts | 25 + scripts/check-frontend-size.sh | 19 + scripts/check.sh | 6 +- 150 files changed, 3895 insertions(+), 1349 deletions(-) create mode 100755 .githooks/pre-push create mode 100644 frontend/.prettierignore create mode 100644 frontend/.prettierrc.json create mode 100644 frontend/eslint.config.js create mode 100644 frontend/src/features/admin/listingDisplay.test.ts create mode 100644 frontend/src/shared/utils/format.test.ts create mode 100644 frontend/src/shared/utils/pricing.test.ts create mode 100644 frontend/vitest.config.ts create mode 100755 scripts/check-frontend-size.sh 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 ? ( - - ) : null} - {row.status === 'draft' && canManage ? ( - - ) : null} - {row.status === 'published' && canManage ? ( - - ) : null} - {row.status === 'archived' && canManage ? ( - - ) : null} - {canManage ? ( - - ) : 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 ? ( + + ) : null} + {row.status === 'draft' && canManage ? ( + + ) : null} + {row.status === 'published' && canManage ? ( + + ) : null} + {row.status === 'archived' && canManage ? ( + + ) : null} + {canManage ? ( + + ) : 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/AdminChatsPage.tsx b/frontend/src/features/admin/AdminChatsPage.tsx index 1ef8ad3..2bd205d 100644 --- a/frontend/src/features/admin/AdminChatsPage.tsx +++ b/frontend/src/features/admin/AdminChatsPage.tsx @@ -88,7 +88,7 @@ export function AdminChatsPage() { refetchInterval: 15_000, }) - const conversations = listQuery.data?.items ?? [] + const conversations = useMemo(() => listQuery.data?.items ?? [], [listQuery.data?.items]) useEffect(() => { if (selectedId || conversations.length === 0) return @@ -233,7 +233,9 @@ export function AdminChatsPage() { 未读 {conversations.reduce((sum, item) => sum + item.unread_count, 0)} - 待回复 {conversations.filter(item => item.needs_reply).length} + + 待回复 {conversations.filter(item => item.needs_reply).length} + } extra={ diff --git a/frontend/src/features/admin/AdminCrashCategoriesPage.tsx b/frontend/src/features/admin/AdminCrashCategoriesPage.tsx index b01ec1a..de49063 100644 --- a/frontend/src/features/admin/AdminCrashCategoriesPage.tsx +++ b/frontend/src/features/admin/AdminCrashCategoriesPage.tsx @@ -1,6 +1,18 @@ import { useState } from 'react' import { useQuery, useQueryClient } from '@tanstack/react-query' -import { Alert, App, Button, Form, Input, InputNumber, Modal, Select, Space, Tag, type TableColumnsType } from 'antd' +import { + Alert, + App, + Button, + Form, + Input, + InputNumber, + Modal, + Select, + Space, + Tag, + type TableColumnsType, +} from 'antd' import { PlusOutlined, ReloadOutlined } from '@ant-design/icons' import { @@ -35,7 +47,9 @@ export function AdminCrashCategoriesPage() { const [editing, setEditing] = useState() const [submitting, setSubmitting] = useState(false) const permissions = useAdminSessionStore(state => state.admin?.permissions) - const canView = Boolean(permissions?.includes('*') || permissions?.includes('mohong:product_view')) + const canView = Boolean( + permissions?.includes('*') || permissions?.includes('mohong:product_view') + ) const canManage = Boolean(permissions?.includes('*') || permissions?.includes('mohong:category')) const categoriesQuery = useQuery({ @@ -44,8 +58,7 @@ export function AdminCrashCategoriesPage() { enabled: canView, }) - const invalidate = () => - queryClient.invalidateQueries({ queryKey: ['admin-crash-categories'] }) + const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin-crash-categories'] }) const save = async () => { const values = await form.validateFields() diff --git a/frontend/src/features/admin/AdminCrashConfigPage.tsx b/frontend/src/features/admin/AdminCrashConfigPage.tsx index 8a83b42..b97b25f 100644 --- a/frontend/src/features/admin/AdminCrashConfigPage.tsx +++ b/frontend/src/features/admin/AdminCrashConfigPage.tsx @@ -95,9 +95,7 @@ export function AdminCrashConfigPage() { > 保存配置 - - 模板留空将回退到后端默认模板 - + 模板留空将回退到后端默认模板 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() { - 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 ? ( - ) : 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 ? ( - ) : 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' ? ( - - ) : null} - {row.status === 'disabled' ? ( - - ) : null} - + 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' ? ( - - ), - }, - ], - // 操作回调依赖当前站点与权限,站点切换时需要重建列。 - [canManage, siteId] - ) + ) : null} + {row.status === 'disabled' ? ( + + ) : null} + + +
+ ), + }, + ] 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={ - - } + footer={} > 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() { }, ]} > - + - + diff --git a/frontend/src/features/admin/AdminFinancePage.tsx b/frontend/src/features/admin/AdminFinancePage.tsx index 04d272c..63d5ac2 100644 --- a/frontend/src/features/admin/AdminFinancePage.tsx +++ b/frontend/src/features/admin/AdminFinancePage.tsx @@ -108,7 +108,9 @@ const dailyColumns: TableColumnsType = [ }, ] -function buildDetailColumns(onOpenOrder: (orderNo: string) => void): TableColumnsType { +function buildDetailColumns( + onOpenOrder: (orderNo: string) => void +): TableColumnsType { return [ { title: '订单', @@ -117,7 +119,11 @@ function buildDetailColumns(onOpenOrder: (orderNo: string) => void): TableColumn fixed: 'left', render: (_, row) => (
- @@ -202,10 +208,7 @@ function buildDetailColumns(onOpenOrder: (orderNo: string) => void): TableColumn export function AdminFinancePage() { const navigate = useNavigate() - const [range, setRange] = useState<[Dayjs, Dayjs]>([ - dayjs().subtract(6, 'day'), - dayjs(), - ]) + const [range, setRange] = useState<[Dayjs, Dayjs]>([dayjs().subtract(6, 'day'), dayjs()]) const [keyword, setKeyword] = useState('') const [appliedKeyword, setAppliedKeyword] = useState('') const [dateType, setDateType] = useState('settled') @@ -434,7 +437,9 @@ export function AdminFinancePage() { 待结算订单 {summary?.pending_settle_order_count ?? 0}
- 待线下结算 {formatMoney(summary?.offline_settlement_pending_amount_cent ?? 0)} + + 待线下结算 {formatMoney(summary?.offline_settlement_pending_amount_cent ?? 0)} + @@ -475,7 +480,11 @@ export function AdminFinancePage() { className="admin-finance-daily" title={每日趋势} - extra={数据更新于 {formatDateTime(dashboard.data?.generated_at)}} + extra={ + + 数据更新于 {formatDateTime(dashboard.data?.generated_at)} + + } rowKey="date" columns={dailyColumns} dataSource={daily} diff --git a/frontend/src/features/admin/AdminInvitationsPage.tsx b/frontend/src/features/admin/AdminInvitationsPage.tsx index 1c1d9c3..d49ae9b 100644 --- a/frontend/src/features/admin/AdminInvitationsPage.tsx +++ b/frontend/src/features/admin/AdminInvitationsPage.tsx @@ -28,10 +28,7 @@ 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 { - AdminInvitationRelation, - AdminInviteRebate, -} from '@/features/admin/types' +import type { AdminInvitationRelation, AdminInviteRebate } from '@/features/admin/types' import { useAdminSessionStore } from '@/stores/adminSession' import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format' @@ -172,7 +169,11 @@ export function AdminInvitationsPage() { const [relFilters, setRelFilters] = useState({ phone: '', invite_code: '' }) const [relApplied, setRelApplied] = useState({ phone: '', invite_code: '' }) const [relPage, setRelPage] = useState(1) - const [rebateFilters, setRebateFilters] = useState({ order_no: '', inviter_phone: '', invitee_phone: '' }) + const [rebateFilters, setRebateFilters] = useState({ + order_no: '', + inviter_phone: '', + invitee_phone: '', + }) const [rebateApplied, setRebateApplied] = useState(rebateFilters) const [rebatePage, setRebatePage] = useState(1) const [pageSize] = useState(20) @@ -181,10 +182,12 @@ export function AdminInvitationsPage() { const permissions = useAdminSessionStore(state => state.admin?.permissions) const canView = Boolean( permissions?.includes('*') || - permissions?.includes('distribution:site_view') || - permissions?.includes('distribution:settlement_view') + permissions?.includes('distribution:site_view') || + permissions?.includes('distribution:settlement_view') + ) + const canManage = Boolean( + permissions?.includes('*') || permissions?.includes('distribution:site_manage') ) - const canManage = Boolean(permissions?.includes('*') || permissions?.includes('distribution:site_manage')) const configQuery = useQuery({ queryKey: ['admin-invitation-config'], @@ -278,13 +281,31 @@ export function AdminInvitationsPage() {
- + - + - + diff --git a/frontend/src/features/admin/AdminListingsPage.tsx b/frontend/src/features/admin/AdminListingsPage.tsx index 9c00321..04d848b 100644 --- a/frontend/src/features/admin/AdminListingsPage.tsx +++ b/frontend/src/features/admin/AdminListingsPage.tsx @@ -44,10 +44,7 @@ import { listingStatusOptions, } from '@/features/listings/labels' import type { AdminListing, AdminListingQuery } from '@/features/admin/types' -import { - emptyListingPublishOptions, - fetchListingPublishOptions, -} from '@/features/seller/options' +import { emptyListingPublishOptions, fetchListingPublishOptions } from '@/features/seller/options' import { useAdminSessionStore } from '@/stores/adminSession' import { formatText } from '@/shared/utils/format' @@ -269,9 +266,7 @@ function buildColumns( key: 'pure_coin_price', width: 118, align: 'right', - render: (_, row) => ( - {listingPureCoinPriceText(row)} - ), + render: (_, row) => {listingPureCoinPriceText(row)}, }, { title: '租期', @@ -302,11 +297,7 @@ function buildColumns( key: 'ratio', width: 70, align: 'center', - render: (_, row) => ( - - {listingRatioText(row)} - - ), + render: (_, row) => {listingRatioText(row)}, }, { title: '详情', @@ -375,7 +366,10 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) { options: group.options.map(name => ({ value: name, label: name })), })) const selectedSkinNames = draft.skin_name - ? draft.skin_name.split(',').map(name => name.trim()).filter(Boolean) + ? draft.skin_name + .split(',') + .map(name => name.trim()) + .filter(Boolean) : [] const listQuery = useQuery({ @@ -390,7 +384,10 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) { enabled: isReviewMode, }) - const rawItems = isReviewMode ? pendingQuery.data ?? [] : listQuery.data?.items ?? [] + const rawItems = useMemo( + () => (isReviewMode ? (pendingQuery.data ?? []) : (listQuery.data?.items ?? [])), + [isReviewMode, pendingQuery.data, listQuery.data?.items] + ) const reviewFiltered = useMemo(() => { if (!isReviewMode) return rawItems @@ -403,15 +400,9 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) { .filter(Boolean) .some(value => String(value).toLowerCase().includes(keyword)) }) - }, [ - isReviewMode, - rawItems, - applied.keyword, - applied.owner_id, - applied.status, - ]) + }, [isReviewMode, rawItems, applied.keyword, applied.owner_id, applied.status]) - const total = isReviewMode ? reviewFiltered.length : listQuery.data?.total ?? 0 + const total = isReviewMode ? reviewFiltered.length : (listQuery.data?.total ?? 0) const items = isReviewMode ? reviewFiltered.slice((page - 1) * pageSize, page * pageSize) : rawItems @@ -431,10 +422,7 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) { setDetailOpen(true) } - const columns = useMemo( - () => buildColumns(row => openDetail(row.id), canView), - [canView, mode] - ) + const columns = buildColumns(row => openDetail(row.id), canView) const patchFilter = (patch: Partial) => { setDraft(current => ({ ...current, ...patch })) @@ -477,8 +465,7 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) { } const hasActiveFilters = Object.entries(draft).some( - ([key, value]) => - !(isReviewMode && key === 'review_status') && value !== '' && value != null + ([key, value]) => !(isReviewMode && key === 'review_status') && value !== '' && value != null ) const errorText = error instanceof Error ? error.message : '请稍后重试或重新登录后台。' const allActive = !applied.status && !applied.review_status diff --git a/frontend/src/features/admin/AdminLoginPage.tsx b/frontend/src/features/admin/AdminLoginPage.tsx index 0d6919f..23adaef 100644 --- a/frontend/src/features/admin/AdminLoginPage.tsx +++ b/frontend/src/features/admin/AdminLoginPage.tsx @@ -1,6 +1,11 @@ import { useEffect, useState } from 'react' import { Alert, App, Button, Form, Input, Space, Typography } from 'antd' -import { LockOutlined, ReloadOutlined, SafetyCertificateOutlined, UserOutlined } from '@ant-design/icons' +import { + LockOutlined, + ReloadOutlined, + SafetyCertificateOutlined, + UserOutlined, +} from '@ant-design/icons' import { Navigate, useNavigate, useSearchParams } from 'react-router-dom' import { BrandLogo } from '@/components/BrandLogo' @@ -116,7 +121,11 @@ export function AdminLoginPage() { name="username" rules={[{ required: true, message: '请输入管理员账号' }]} > - } placeholder="请输入管理员账号" autoComplete="username" /> + } + placeholder="请输入管理员账号" + autoComplete="username" + /> state.admin?.permissions) - const can = (code: string) => - Boolean(permissions?.includes('*') || permissions?.includes(code)) + const can = (code: string) => Boolean(permissions?.includes('*') || permissions?.includes(code)) const canView = can('cs_manual_entry:view') const canCreate = can('cs_manual_entry:create') const canUpdate = can('cs_manual_entry:update') @@ -172,7 +171,10 @@ export function AdminManualEntriesPage() { message.success('录单已提交') } setEditorOpen(false) - await Promise.all([invalidateEntries(), queryClient.invalidateQueries({ queryKey: ['admin-manual-entry-stats'] })]) + await Promise.all([ + invalidateEntries(), + queryClient.invalidateQueries({ queryKey: ['admin-manual-entry-stats'] }), + ]) } catch (error) { message.error(errorText(error)) } finally { @@ -439,7 +441,9 @@ export function AdminManualEntriesPage() { allowClear value={filters.entry_no} placeholder="精确或前缀匹配" - onChange={event => setFilters(current => ({ ...current, entry_no: event.target.value }))} + onChange={event => + setFilters(current => ({ ...current, entry_no: event.target.value })) + } /> @@ -533,9 +537,7 @@ export function AdminManualEntriesPage() { open={optionsOpen} title="录单选项管理" onCancel={() => setOptionsOpen(false)} - footer={ - - } + footer={} 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() { - + 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() { > 完成提号 - ) : null} - {pickup?.status === 'completed' && - pickup.offline_settlement_status === 'pending' ? ( + {pickup?.status === 'completed' && pickup.offline_settlement_status === 'pending' ? ( - {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() { > 批量导入 - @@ -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() { - @@ -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() { - {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({ />
))} -
@@ -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 ? ( - {product.title} + {product.title} ) : null}
@@ -88,12 +99,22 @@ export function CrashDetailPage() { 数量 - setQuantity(Number(value) || 1)} /> + setQuantity(Number(value) || 1)} + /> 合计 {formatMoney(product.price_cent * quantity)} - 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 ? ( - {item.title} + {item.title} ) : 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={} + action={ + + } />
) @@ -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 ? ( - ) : 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() { 卖家中心
- 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)} - } + 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() { )} - 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 ? ( - + {data.title} ) : ( @@ -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({
- 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 ( - - ) - })} -
+
+ {zoneTabs.map(tab => { + const count = data?.zone_counts?.[tab.key] + return ( + + ) + })} +
- {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 }} > - 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() { 账号群聊、客服回复与系统通知只对当前账号开放。 - @@ -239,7 +236,11 @@ export function MessagesPage() { /> ) : items.length === 0 ? ( - 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() { -
下单需登录并完成实名;支付成功后由客服接单处理。
+
+ 下单需登录并完成实名;支付成功后由客服接单处理。 +
) : 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 (
- + @@ -223,11 +223,7 @@ export function MobileListingDetailPage() {
-
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 ? ( - ) : 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 ? (
- -
@@ -498,15 +489,30 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa
@@ -522,7 +528,12 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa -
@@ -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 -
@@ -627,7 +659,12 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa -
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() {

登录后查看订单详情

交接记录与结账明细只对交易双方可见。

-
@@ -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() {

先登录,再看订单

订单、押金与结账明细只对交易双方可见。

-
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() {

登录后管理商品

发布、提审与下架只对当前账号开放。

-
@@ -252,7 +252,10 @@ export function MobileSellerListingsPage() { {item.review_status === 'rejected' && item.review_reason ? (

驳回:{item.review_reason}

) : null} -
event.stopPropagation()}> +
event.stopPropagation()} + >
)} - 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() { 订单、押金、交接与结账记录只对交易双方可见。 - 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 }
- @@ -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 ? (
@@ -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() { -
- © 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