diff --git a/frontend/admin-web/.env.development b/frontend/admin-web/.env.development new file mode 100644 index 00000000..ea3f2c53 --- /dev/null +++ b/frontend/admin-web/.env.development @@ -0,0 +1,6 @@ +# API Configuration +NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/api + +# Application Info +NEXT_PUBLIC_APP_NAME=榴莲认种管理后台 +NEXT_PUBLIC_VERSION=1.0.0-dev diff --git a/frontend/admin-web/.env.production b/frontend/admin-web/.env.production new file mode 100644 index 00000000..a0e2118a --- /dev/null +++ b/frontend/admin-web/.env.production @@ -0,0 +1,6 @@ +# API Configuration +NEXT_PUBLIC_API_BASE_URL=https://api.rwadurian.com/api + +# Application Info +NEXT_PUBLIC_APP_NAME=榴莲认种管理后台 +NEXT_PUBLIC_VERSION=1.0.0 diff --git a/frontend/admin-web/.eslintrc.json b/frontend/admin-web/.eslintrc.json new file mode 100644 index 00000000..78c81ea4 --- /dev/null +++ b/frontend/admin-web/.eslintrc.json @@ -0,0 +1,9 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"], + "rules": { + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/no-explicit-any": "warn", + "react/no-unescaped-entities": "off", + "prefer-const": "warn" + } +} diff --git a/frontend/admin-web/.gitignore b/frontend/admin-web/.gitignore new file mode 100644 index 00000000..c90a0ced --- /dev/null +++ b/frontend/admin-web/.gitignore @@ -0,0 +1,49 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env.local + +# Claude Code +.claude/ + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +Thumbs.db diff --git a/frontend/admin-web/.prettierrc b/frontend/admin-web/.prettierrc new file mode 100644 index 00000000..26f4ecf2 --- /dev/null +++ b/frontend/admin-web/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "tabWidth": 2, + "useTabs": false, + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/frontend/admin-web/README.MD b/frontend/admin-web/README.MD deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/admin-web/next.config.ts b/frontend/admin-web/next.config.ts new file mode 100644 index 00000000..445c7e0a --- /dev/null +++ b/frontend/admin-web/next.config.ts @@ -0,0 +1,27 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + reactStrictMode: true, + sassOptions: { + silenceDeprecations: ['legacy-js-api'], + }, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '**', + }, + ], + }, + async redirects() { + return [ + { + source: '/', + destination: '/dashboard', + permanent: false, + }, + ]; + }, +}; + +export default nextConfig; diff --git a/frontend/admin-web/package-lock.json b/frontend/admin-web/package-lock.json new file mode 100644 index 00000000..7c4ba56c --- /dev/null +++ b/frontend/admin-web/package-lock.json @@ -0,0 +1,6507 @@ +{ + "name": "rwadurian-admin-web", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rwadurian-admin-web", + "version": "1.0.0", + "dependencies": { + "@reduxjs/toolkit": "^2.3.0", + "@tanstack/react-query": "^5.62.16", + "axios": "^1.7.9", + "clsx": "^2.1.1", + "dayjs": "^1.11.13", + "next": "^15.1.6", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-redux": "^9.2.0", + "recharts": "^2.15.0", + "xlsx": "^0.18.5", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@types/node": "^22.10.7", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "eslint": "^9.18.0", + "eslint-config-next": "^15.1.6", + "prettier": "^3.4.2", + "sass": "^1.83.4", + "typescript": "^5.7.3" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "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.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.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/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.6.tgz", + "integrity": "sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.6.tgz", + "integrity": "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.6.tgz", + "integrity": "sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.6.tgz", + "integrity": "sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.6.tgz", + "integrity": "sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.6.tgz", + "integrity": "sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.6.tgz", + "integrity": "sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.6.tgz", + "integrity": "sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.6.tgz", + "integrity": "sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.6.tgz", + "integrity": "sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.0.tgz", + "integrity": "sha512-hBjYg0aaRL1O2Z0IqWhnTLytnjDIxekmRxm1snsHjHaKVmIF1HiImWqsq+PuEbn6zdMlkIj9WofK1vR8jjx+Xw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", + "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.11", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.11.tgz", + "integrity": "sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.11.tgz", + "integrity": "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.11" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", + "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/type-utils": "8.48.0", + "@typescript-eslint/utils": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.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.48.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", + "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", + "debug": "^4.3.4" + }, + "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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", + "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.48.0", + "@typescript-eslint/types": "^8.48.0", + "debug": "^4.3.4" + }, + "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.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", + "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0" + }, + "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.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", + "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", + "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", + "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", + "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.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", + "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.48.0", + "@typescript-eslint/tsconfig-utils": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.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.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", + "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz", + "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "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/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "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", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "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/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "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", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.6.tgz", + "integrity": "sha512-cGr3VQlPsZBEv8rtYp4BpG1KNXDqGvPo9VC1iaCgIA11OfziC/vczng+TnAS3WpRIR3Q5ye/6yl+CRUuZ1fPGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.5.6", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "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/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/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "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-equals": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.3.tgz", + "integrity": "sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/immer": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.0.0.tgz", + "integrity": "sha512-XtRG4SINt4dpqlnJvs70O2j6hH7H0X8fUzFsjMn1rwnETaxwp83HLNimXBjZ78MrKl3/d3/pkzDH0o0Lkxm37Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "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/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "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/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "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.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "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/next": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.6.tgz", + "integrity": "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.6", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.6", + "@next/swc-darwin-x64": "15.5.6", + "@next/swc-linux-arm64-gnu": "15.5.6", + "@next/swc-linux-arm64-musl": "15.5.6", + "@next/swc-linux-x64-gnu": "15.5.6", + "@next/swc-linux-x64-musl": "15.5.6", + "@next/swc-win32-arm64-msvc": "15.5.6", + "@next/swc-win32-x64-msvc": "15.5.6", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "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.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "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/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sass": { + "version": "1.94.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz", + "integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "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/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "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/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "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.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "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/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "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/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "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/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/admin-web/package.json b/frontend/admin-web/package.json new file mode 100644 index 00000000..4bcb7c4d --- /dev/null +++ b/frontend/admin-web/package.json @@ -0,0 +1,40 @@ +{ + "name": "rwadurian-admin-web", + "version": "1.0.0", + "private": true, + "description": "榴莲认种管理后台 - RWADurian Admin Web", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,scss,css,json}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,scss,css,json}\"", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@reduxjs/toolkit": "^2.3.0", + "@tanstack/react-query": "^5.62.16", + "axios": "^1.7.9", + "clsx": "^2.1.1", + "dayjs": "^1.11.13", + "next": "^15.1.6", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-redux": "^9.2.0", + "recharts": "^2.15.0", + "xlsx": "^0.18.5", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@types/node": "^22.10.7", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "eslint": "^9.18.0", + "eslint-config-next": "^15.1.6", + "prettier": "^3.4.2", + "sass": "^1.83.4", + "typescript": "^5.7.3" + } +} diff --git a/frontend/admin-web/public/Button-menu.svg b/frontend/admin-web/public/Button-menu.svg new file mode 100644 index 00000000..5a945104 --- /dev/null +++ b/frontend/admin-web/public/Button-menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Button.svg b/frontend/admin-web/public/Button.svg new file mode 100644 index 00000000..0058a5be --- /dev/null +++ b/frontend/admin-web/public/Button.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/admin-web/public/Button1.svg b/frontend/admin-web/public/Button1.svg new file mode 100644 index 00000000..a428db77 --- /dev/null +++ b/frontend/admin-web/public/Button1.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Button2.svg b/frontend/admin-web/public/Button2.svg new file mode 100644 index 00000000..b280b6c5 --- /dev/null +++ b/frontend/admin-web/public/Button2.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Container.svg b/frontend/admin-web/public/Container.svg new file mode 100644 index 00000000..a1dc590f --- /dev/null +++ b/frontend/admin-web/public/Container.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Container1.svg b/frontend/admin-web/public/Container1.svg new file mode 100644 index 00000000..126e54b0 --- /dev/null +++ b/frontend/admin-web/public/Container1.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Container10.svg b/frontend/admin-web/public/Container10.svg new file mode 100644 index 00000000..47e260f5 --- /dev/null +++ b/frontend/admin-web/public/Container10.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Container11.svg b/frontend/admin-web/public/Container11.svg new file mode 100644 index 00000000..cbd25427 --- /dev/null +++ b/frontend/admin-web/public/Container11.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Container2.svg b/frontend/admin-web/public/Container2.svg new file mode 100644 index 00000000..6e432d42 --- /dev/null +++ b/frontend/admin-web/public/Container2.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Container3.svg b/frontend/admin-web/public/Container3.svg new file mode 100644 index 00000000..5fdd8bd4 --- /dev/null +++ b/frontend/admin-web/public/Container3.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Container4.svg b/frontend/admin-web/public/Container4.svg new file mode 100644 index 00000000..d7cf5135 --- /dev/null +++ b/frontend/admin-web/public/Container4.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Container5.svg b/frontend/admin-web/public/Container5.svg new file mode 100644 index 00000000..bd314145 --- /dev/null +++ b/frontend/admin-web/public/Container5.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Container6.svg b/frontend/admin-web/public/Container6.svg new file mode 100644 index 00000000..b204e48c --- /dev/null +++ b/frontend/admin-web/public/Container6.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Container7.svg b/frontend/admin-web/public/Container7.svg new file mode 100644 index 00000000..faa850cb --- /dev/null +++ b/frontend/admin-web/public/Container7.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Container8.svg b/frontend/admin-web/public/Container8.svg new file mode 100644 index 00000000..c57957c9 --- /dev/null +++ b/frontend/admin-web/public/Container8.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Container9.svg b/frontend/admin-web/public/Container9.svg new file mode 100644 index 00000000..4cb6d6a2 --- /dev/null +++ b/frontend/admin-web/public/Container9.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Frame@3x.png b/frontend/admin-web/public/Frame@3x.png new file mode 100644 index 00000000..9026ea10 Binary files /dev/null and b/frontend/admin-web/public/Frame@3x.png differ diff --git a/frontend/admin-web/public/Label.svg b/frontend/admin-web/public/Label.svg new file mode 100644 index 00000000..59a6da7e --- /dev/null +++ b/frontend/admin-web/public/Label.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/admin-web/public/Label1.svg b/frontend/admin-web/public/Label1.svg new file mode 100644 index 00000000..5cc0f370 --- /dev/null +++ b/frontend/admin-web/public/Label1.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/admin-web/public/Margin.svg b/frontend/admin-web/public/Margin.svg new file mode 100644 index 00000000..3e5e283b --- /dev/null +++ b/frontend/admin-web/public/Margin.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Margin1.svg b/frontend/admin-web/public/Margin1.svg new file mode 100644 index 00000000..42c276f9 --- /dev/null +++ b/frontend/admin-web/public/Margin1.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/Margin2.svg b/frontend/admin-web/public/Margin2.svg new file mode 100644 index 00000000..e007c64a --- /dev/null +++ b/frontend/admin-web/public/Margin2.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/1@2x.png b/frontend/admin-web/public/images/1@2x.png new file mode 100644 index 00000000..33f9be5a Binary files /dev/null and b/frontend/admin-web/public/images/1@2x.png differ diff --git a/frontend/admin-web/public/images/@2x.png b/frontend/admin-web/public/images/@2x.png new file mode 100644 index 00000000..2cc44367 Binary files /dev/null and b/frontend/admin-web/public/images/@2x.png differ diff --git a/frontend/admin-web/public/images/Background.svg b/frontend/admin-web/public/images/Background.svg new file mode 100644 index 00000000..b7e42182 --- /dev/null +++ b/frontend/admin-web/public/images/Background.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/admin-web/public/images/Background1.svg b/frontend/admin-web/public/images/Background1.svg new file mode 100644 index 00000000..5b0781af --- /dev/null +++ b/frontend/admin-web/public/images/Background1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/admin-web/public/images/Button.svg b/frontend/admin-web/public/images/Button.svg new file mode 100644 index 00000000..5b990f9e --- /dev/null +++ b/frontend/admin-web/public/images/Button.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/admin-web/public/images/Cell@2x.png b/frontend/admin-web/public/images/Cell@2x.png new file mode 100644 index 00000000..d53c4cd0 Binary files /dev/null and b/frontend/admin-web/public/images/Cell@2x.png differ diff --git a/frontend/admin-web/public/images/Container.svg b/frontend/admin-web/public/images/Container.svg new file mode 100644 index 00000000..a1dc590f --- /dev/null +++ b/frontend/admin-web/public/images/Container.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container1.svg b/frontend/admin-web/public/images/Container1.svg new file mode 100644 index 00000000..a402562c --- /dev/null +++ b/frontend/admin-web/public/images/Container1.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container10.svg b/frontend/admin-web/public/images/Container10.svg new file mode 100644 index 00000000..05e4dbf5 --- /dev/null +++ b/frontend/admin-web/public/images/Container10.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container11.svg b/frontend/admin-web/public/images/Container11.svg new file mode 100644 index 00000000..3955aca2 --- /dev/null +++ b/frontend/admin-web/public/images/Container11.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container12.svg b/frontend/admin-web/public/images/Container12.svg new file mode 100644 index 00000000..699dca36 --- /dev/null +++ b/frontend/admin-web/public/images/Container12.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container13.svg b/frontend/admin-web/public/images/Container13.svg new file mode 100644 index 00000000..ad9e8832 --- /dev/null +++ b/frontend/admin-web/public/images/Container13.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container14.svg b/frontend/admin-web/public/images/Container14.svg new file mode 100644 index 00000000..e8dd8122 --- /dev/null +++ b/frontend/admin-web/public/images/Container14.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/admin-web/public/images/Container2.svg b/frontend/admin-web/public/images/Container2.svg new file mode 100644 index 00000000..430e7cef --- /dev/null +++ b/frontend/admin-web/public/images/Container2.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container3.svg b/frontend/admin-web/public/images/Container3.svg new file mode 100644 index 00000000..011d7b51 --- /dev/null +++ b/frontend/admin-web/public/images/Container3.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container4.svg b/frontend/admin-web/public/images/Container4.svg new file mode 100644 index 00000000..0b41c111 --- /dev/null +++ b/frontend/admin-web/public/images/Container4.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container5.svg b/frontend/admin-web/public/images/Container5.svg new file mode 100644 index 00000000..80284d09 --- /dev/null +++ b/frontend/admin-web/public/images/Container5.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container6.svg b/frontend/admin-web/public/images/Container6.svg new file mode 100644 index 00000000..b479b4be --- /dev/null +++ b/frontend/admin-web/public/images/Container6.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container7.svg b/frontend/admin-web/public/images/Container7.svg new file mode 100644 index 00000000..84170863 --- /dev/null +++ b/frontend/admin-web/public/images/Container7.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container8.svg b/frontend/admin-web/public/images/Container8.svg new file mode 100644 index 00000000..6693fdf0 --- /dev/null +++ b/frontend/admin-web/public/images/Container8.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container9.svg b/frontend/admin-web/public/images/Container9.svg new file mode 100644 index 00000000..9b81a1f4 --- /dev/null +++ b/frontend/admin-web/public/images/Container9.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Container@3x.png b/frontend/admin-web/public/images/Container@3x.png new file mode 100644 index 00000000..90997767 Binary files /dev/null and b/frontend/admin-web/public/images/Container@3x.png differ diff --git a/frontend/admin-web/public/images/Data1@2x.png b/frontend/admin-web/public/images/Data1@2x.png new file mode 100644 index 00000000..cc4e38a8 Binary files /dev/null and b/frontend/admin-web/public/images/Data1@2x.png differ diff --git a/frontend/admin-web/public/images/Data1@3x.png b/frontend/admin-web/public/images/Data1@3x.png new file mode 100644 index 00000000..81e70016 Binary files /dev/null and b/frontend/admin-web/public/images/Data1@3x.png differ diff --git a/frontend/admin-web/public/images/Data2@2x.png b/frontend/admin-web/public/images/Data2@2x.png new file mode 100644 index 00000000..05a80d64 Binary files /dev/null and b/frontend/admin-web/public/images/Data2@2x.png differ diff --git a/frontend/admin-web/public/images/Data2@3x.png b/frontend/admin-web/public/images/Data2@3x.png new file mode 100644 index 00000000..c2b5d7a2 Binary files /dev/null and b/frontend/admin-web/public/images/Data2@3x.png differ diff --git a/frontend/admin-web/public/images/Data3@2x.png b/frontend/admin-web/public/images/Data3@2x.png new file mode 100644 index 00000000..f8a0ec9b Binary files /dev/null and b/frontend/admin-web/public/images/Data3@2x.png differ diff --git a/frontend/admin-web/public/images/Data@2x.png b/frontend/admin-web/public/images/Data@2x.png new file mode 100644 index 00000000..cbc18174 Binary files /dev/null and b/frontend/admin-web/public/images/Data@2x.png differ diff --git a/frontend/admin-web/public/images/Data@3x.png b/frontend/admin-web/public/images/Data@3x.png new file mode 100644 index 00000000..9e602821 Binary files /dev/null and b/frontend/admin-web/public/images/Data@3x.png differ diff --git a/frontend/admin-web/public/images/Frame1@3x.png b/frontend/admin-web/public/images/Frame1@3x.png new file mode 100644 index 00000000..4526ac88 Binary files /dev/null and b/frontend/admin-web/public/images/Frame1@3x.png differ diff --git a/frontend/admin-web/public/images/Frame2@3x.png b/frontend/admin-web/public/images/Frame2@3x.png new file mode 100644 index 00000000..35a82fad Binary files /dev/null and b/frontend/admin-web/public/images/Frame2@3x.png differ diff --git a/frontend/admin-web/public/images/Frame3@3x.png b/frontend/admin-web/public/images/Frame3@3x.png new file mode 100644 index 00000000..04cc712f Binary files /dev/null and b/frontend/admin-web/public/images/Frame3@3x.png differ diff --git a/frontend/admin-web/public/images/Frame4@3x.png b/frontend/admin-web/public/images/Frame4@3x.png new file mode 100644 index 00000000..04cc712f Binary files /dev/null and b/frontend/admin-web/public/images/Frame4@3x.png differ diff --git a/frontend/admin-web/public/images/Frame5@3x.png b/frontend/admin-web/public/images/Frame5@3x.png new file mode 100644 index 00000000..04cc712f Binary files /dev/null and b/frontend/admin-web/public/images/Frame5@3x.png differ diff --git a/frontend/admin-web/public/images/Frame6@3x.png b/frontend/admin-web/public/images/Frame6@3x.png new file mode 100644 index 00000000..04cc712f Binary files /dev/null and b/frontend/admin-web/public/images/Frame6@3x.png differ diff --git a/frontend/admin-web/public/images/Frame@3x.png b/frontend/admin-web/public/images/Frame@3x.png new file mode 100644 index 00000000..04cc712f Binary files /dev/null and b/frontend/admin-web/public/images/Frame@3x.png differ diff --git a/frontend/admin-web/public/images/Image@2x.png b/frontend/admin-web/public/images/Image@2x.png new file mode 100644 index 00000000..d0cfc2fe Binary files /dev/null and b/frontend/admin-web/public/images/Image@2x.png differ diff --git a/frontend/admin-web/public/images/Img-logo-margin.svg b/frontend/admin-web/public/images/Img-logo-margin.svg new file mode 100644 index 00000000..2693fa91 --- /dev/null +++ b/frontend/admin-web/public/images/Img-logo-margin.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/admin-web/public/images/Input@2x.png b/frontend/admin-web/public/images/Input@2x.png new file mode 100644 index 00000000..a51553bb Binary files /dev/null and b/frontend/admin-web/public/images/Input@2x.png differ diff --git a/frontend/admin-web/public/images/Label.svg b/frontend/admin-web/public/images/Label.svg new file mode 100644 index 00000000..b4b02090 --- /dev/null +++ b/frontend/admin-web/public/images/Label.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/admin-web/public/images/Label1.svg b/frontend/admin-web/public/images/Label1.svg new file mode 100644 index 00000000..e83078d5 --- /dev/null +++ b/frontend/admin-web/public/images/Label1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/admin-web/public/images/Margin.svg b/frontend/admin-web/public/images/Margin.svg new file mode 100644 index 00000000..0036c7fa --- /dev/null +++ b/frontend/admin-web/public/images/Margin.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Margin1.svg b/frontend/admin-web/public/images/Margin1.svg new file mode 100644 index 00000000..90906a74 --- /dev/null +++ b/frontend/admin-web/public/images/Margin1.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Margin2.svg b/frontend/admin-web/public/images/Margin2.svg new file mode 100644 index 00000000..2e11f617 --- /dev/null +++ b/frontend/admin-web/public/images/Margin2.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Margin3.svg b/frontend/admin-web/public/images/Margin3.svg new file mode 100644 index 00000000..b5771627 --- /dev/null +++ b/frontend/admin-web/public/images/Margin3.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Margin4.svg b/frontend/admin-web/public/images/Margin4.svg new file mode 100644 index 00000000..d4c38c81 --- /dev/null +++ b/frontend/admin-web/public/images/Margin4.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Margin5.svg b/frontend/admin-web/public/images/Margin5.svg new file mode 100644 index 00000000..7efea78c --- /dev/null +++ b/frontend/admin-web/public/images/Margin5.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Margin6.svg b/frontend/admin-web/public/images/Margin6.svg new file mode 100644 index 00000000..a895c6e8 --- /dev/null +++ b/frontend/admin-web/public/images/Margin6.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/public/images/Margin7.svg b/frontend/admin-web/public/images/Margin7.svg new file mode 100644 index 00000000..55a5afa3 --- /dev/null +++ b/frontend/admin-web/public/images/Margin7.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/admin-web/src/app/(auth)/forgot-password/forgot-password.module.scss b/frontend/admin-web/src/app/(auth)/forgot-password/forgot-password.module.scss new file mode 100644 index 00000000..eb3bb74a --- /dev/null +++ b/frontend/admin-web/src/app/(auth)/forgot-password/forgot-password.module.scss @@ -0,0 +1,86 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.forgotPassword { + width: 100%; + max-width: 400px; + padding: $spacing-lg; + + &__card { + @include card-base; + padding: $spacing-xxxl; + } + + &__header { + text-align: center; + margin-bottom: $spacing-xxl; + } + + &__logo { + font-size: 48px; + margin-bottom: $spacing-base; + } + + &__icon { + @include flex-center; + width: 64px; + height: 64px; + margin: 0 auto $spacing-base; + border-radius: $border-radius-round; + background-color: rgba($primary-color, 0.1); + color: $primary-color; + + svg { + width: 32px; + height: 32px; + } + } + + &__title { + font-size: $font-size-xl; + font-weight: $font-weight-semibold; + color: $text-primary; + margin: 0 0 $spacing-sm; + } + + &__subtitle { + font-size: $font-size-sm; + color: $text-secondary; + margin: 0; + line-height: $line-height-loose; + } + + &__form { + display: flex; + flex-direction: column; + gap: $spacing-lg; + } + + &__actions { + display: flex; + flex-direction: column; + gap: $spacing-sm; + } + + &__footer { + text-align: center; + margin-top: $spacing-xl; + } + + &__link { + @include flex-center; + gap: $spacing-xs; + font-size: $font-size-sm; + color: $text-secondary; + @include transition-fast; + + &:hover { + color: $primary-color; + } + + svg { + width: 16px; + height: 16px; + } + } +} diff --git a/frontend/admin-web/src/app/(auth)/forgot-password/page.tsx b/frontend/admin-web/src/app/(auth)/forgot-password/page.tsx new file mode 100644 index 00000000..58b85841 --- /dev/null +++ b/frontend/admin-web/src/app/(auth)/forgot-password/page.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { Button, Input, toast } from '@/components/common'; +import { isValidEmail } from '@/utils/validators'; +import styles from './forgot-password.module.scss'; + +export default function ForgotPasswordPage() { + const [loading, setLoading] = useState(false); + const [email, setEmail] = useState(''); + const [error, setError] = useState(''); + const [sent, setSent] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!email) { + setError('请输入邮箱'); + return; + } + + if (!isValidEmail(email)) { + setError('请输入有效的邮箱地址'); + return; + } + + setLoading(true); + + try { + // 模拟发送重置邮件 + await new Promise((resolve) => setTimeout(resolve, 1500)); + setSent(true); + toast.success('重置邮件已发送'); + } catch { + toast.error('发送失败,请稍后重试'); + } finally { + setLoading(false); + } + }; + + if (sent) { + return ( +
+
+
+
+ + + + +
+

邮件已发送

+

+ 我们已向 {email} 发送了密码重置链接,请查收邮件并按照提示操作。 +

+
+ +
+ + + + +
+
+
+ ); + } + + return ( +
+
+
+
🔐
+

忘记密码

+

+ 请输入您的邮箱地址,我们将发送密码重置链接 +

+
+ +
+ { + setEmail(e.target.value); + setError(''); + }} + error={error} + prefix={ + + + + + } + /> + + +
+ +
+ + + + + + 返回登录 + +
+
+
+ ); +} diff --git a/frontend/admin-web/src/app/(auth)/layout.tsx b/frontend/admin-web/src/app/(auth)/layout.tsx new file mode 100644 index 00000000..bb25ee98 --- /dev/null +++ b/frontend/admin-web/src/app/(auth)/layout.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; + +export default function AuthLayout({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/admin-web/src/app/(auth)/login/login.module.scss b/frontend/admin-web/src/app/(auth)/login/login.module.scss new file mode 100644 index 00000000..47414952 --- /dev/null +++ b/frontend/admin-web/src/app/(auth)/login/login.module.scss @@ -0,0 +1,255 @@ +/* 登录页面样式 - 基于 UIPro Figma 设计 */ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.login { + width: 100%; + min-height: 100vh; + position: relative; + background-color: $white; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 30px; + box-sizing: border-box; + gap: 27.5px; + text-align: left; + font-family: 'Noto Sans SC', $font-family-base; + + &__header { + align-self: stretch; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + align-content: center; + gap: 12px; + } + + &__logo { + height: 44px; + width: 36px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + + img { + width: 100%; + height: 100%; + object-fit: contain; + } + } + + &__brandName { + margin: 0; + position: relative; + font-size: 24px; + line-height: 32px; + font-weight: 700; + color: #0f172a; + } + + &__card { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + padding: 2.5px 0; + box-sizing: border-box; + gap: 23.5px; + max-width: 448px; + } + + &__titleWrapper { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: center; + } + + &__title { + margin: 0; + position: relative; + font-size: 30px; + letter-spacing: -0.75px; + line-height: 36px; + font-weight: 700; + color: $black; + } + + &__form { + margin: 0; + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-end; + padding-top: 8.5px; + gap: 24px; + } + + &__inputGroup { + align-self: stretch; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 6px; + background-color: transparent; + display: flex; + flex-direction: column; + align-items: flex-start; + } + + &__inputWrapper { + align-self: stretch; + background-color: $white; + border: 1px solid #cbd5e1; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 12px; + + &:first-child { + border-radius: 6px 6px 0 0; + border-bottom: none; + } + + &:last-child { + border-radius: 0 0 6px 6px; + } + + &--error { + border-color: $error-color; + } + } + + &__input { + width: 100%; + border: none; + outline: none; + background-color: transparent; + align-self: stretch; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 2px 0 1px; + box-sizing: border-box; + font-family: 'Noto Sans SC', $font-family-base; + font-size: 14px; + color: $text-primary; + + &::placeholder { + color: #6b7280; + } + } + + &__error { + font-size: 12px; + color: $error-color; + margin-top: 4px; + padding-left: 12px; + } + + &__options { + align-self: stretch; + display: flex; + align-items: flex-end; + justify-content: flex-end; + } + + &__link { + position: relative; + font-size: 14px; + line-height: 20px; + font-weight: 500; + font-family: 'Noto Sans SC', $font-family-base; + color: #005a9c; + text-decoration: none; + cursor: pointer; + + &:hover { + text-decoration: underline; + } + } + + &__button { + cursor: pointer; + border: none; + padding: 13px 16px; + background-color: #005a9c; + align-self: stretch; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + @include transition-fast; + + &:hover { + background-color: #004a80; + } + + &:disabled { + opacity: 0.7; + cursor: not-allowed; + } + } + + &__buttonText { + flex: 1; + position: relative; + font-size: 14px; + line-height: 20px; + font-weight: 700; + font-family: 'Noto Sans SC', $font-family-base; + color: $white; + text-align: center; + } + + &__footer { + align-self: stretch; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + font-size: 14px; + color: #4b5563; + } + + &__footerText { + position: relative; + line-height: 20px; + } + + &__registerLink { + position: relative; + line-height: 20px; + font-weight: 500; + color: #005a9c; + text-decoration: none; + cursor: pointer; + + &:hover { + text-decoration: underline; + } + } + + /* Loading spinner */ + &__spinner { + width: 16px; + height: 16px; + border: 2px solid transparent; + border-top-color: $white; + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin-right: 8px; + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/frontend/admin-web/src/app/(auth)/login/page.tsx b/frontend/admin-web/src/app/(auth)/login/page.tsx new file mode 100644 index 00000000..6ae19393 --- /dev/null +++ b/frontend/admin-web/src/app/(auth)/login/page.tsx @@ -0,0 +1,198 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import { toast } from '@/components/common'; +import { useAppDispatch } from '@/store/redux/hooks'; +import { setCredentials } from '@/store/redux/slices/authSlice'; +import { isValidEmail, isValidPassword } from '@/utils/validators'; +import styles from './login.module.scss'; + +/** + * 登录页面组件 + * 基于 UIPro Figma 设计实现 + */ +export default function LoginPage() { + const router = useRouter(); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ + email: '', + password: '', + }); + const [errors, setErrors] = useState({ + email: '', + password: '', + }); + + // 表单验证 + const validateForm = (): boolean => { + const newErrors = { + email: '', + password: '', + }; + + if (!formData.email) { + newErrors.email = '请输入邮箱'; + } else if (!isValidEmail(formData.email)) { + newErrors.email = '请输入有效的邮箱地址'; + } + + if (!formData.password) { + newErrors.password = '请输入密码'; + } else if (!isValidPassword(formData.password)) { + newErrors.password = '密码长度应为6-20位'; + } + + setErrors(newErrors); + return !newErrors.email && !newErrors.password; + }; + + // 表单提交处理 + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + setLoading(true); + + try { + // 模拟登录请求 + await new Promise((resolve) => setTimeout(resolve, 1500)); + + // 模拟登录成功 + dispatch( + setCredentials({ + user: { + id: '1', + email: formData.email, + username: 'admin', + nickname: '管理员', + avatar: '', + role: 'super_admin', + status: 'active', + createdAt: new Date().toISOString(), + lastLoginAt: new Date().toISOString(), + }, + token: 'mock_token_' + Date.now(), + permissions: ['*'], + }) + ); + + toast.success('登录成功'); + router.push('/dashboard'); + } catch { + toast.error('登录失败,请检查账号密码'); + } finally { + setLoading(false); + } + }; + + // 输入变更处理 + const handleInputChange = (field: 'email' | 'password') => ( + e: React.ChangeEvent + ) => { + setFormData((prev) => ({ ...prev, [field]: e.target.value })); + if (errors[field]) { + setErrors((prev) => ({ ...prev, [field]: '' })); + } + }; + + return ( +
+ {/* 品牌 Logo 区域 */} +
+
+ RWADurian Logo +
+

榴莲认种管理后台

+
+ + {/* 登录表单卡片 */} +
+
+

欢迎回来

+
+ +
+ {/* 输入框组 */} +
+
+ +
+ {errors.email && ( + {errors.email} + )} + +
+ +
+ {errors.password && ( + {errors.password} + )} +
+ + {/* 忘记密码链接 */} +
+ + 忘记密码? + +
+ + {/* 登录按钮 */} + +
+ + {/* 注册链接 */} +
+ 还没有账户? + + 立即注册 + +
+
+
+ ); +} diff --git a/frontend/admin-web/src/app/(dashboard)/authorization/authorization.module.scss b/frontend/admin-web/src/app/(dashboard)/authorization/authorization.module.scss new file mode 100644 index 00000000..016da0c6 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/authorization/authorization.module.scss @@ -0,0 +1,673 @@ +/* 授权管理页面样式 - 基于 UIPro Figma 设计 */ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.authorization { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; + text-align: left; + font-family: 'Noto Sans SC', $font-family-base; +} + +/* 页面标题区域 */ +.authorization__header { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; +} + +.authorization__title { + margin: 0; + position: relative; + font-size: 30px; + line-height: 36px; + font-weight: 700; + color: #000; +} + +/* 通用卡片样式 */ +.authorization__card { + align-self: stretch; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 8px; + background-color: #fff; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 24px; + gap: 15px; +} + +.authorization__cardTitle { + align-self: stretch; + margin: 0; + font-size: 18px; + line-height: 28px; + font-weight: 700; + color: #1e293b; +} + +/* 筛选区域 */ +.authorization__filters { + align-self: stretch; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 16px; +} + +.authorization__select { + min-width: 160px; + height: 38px; + border-radius: 6px; + background-color: #f3f4f6; + border: 1px solid #e5e7eb; + padding: 9px 13px; + font-size: 14px; + line-height: 20px; + color: #1e293b; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 8px center; + background-repeat: no-repeat; + background-size: 16px; +} + +.authorization__input { + flex: 1; + min-width: 120px; + height: 36px; + border-radius: 6px; + background-color: #f3f4f6; + border: 1px solid #e5e7eb; + padding: 8px 12px; + font-size: 14px; + color: #1e293b; + outline: none; + font-family: inherit; + + &::placeholder { + color: #6b7280; + } +} + +.authorization__searchBtn { + cursor: pointer; + border: none; + border-radius: 6px; + background-color: #005a9c; + padding: 8px 16px; + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #fff; + font-family: inherit; + @include transition-fast; + + &:hover { + background-color: darken(#005a9c, 5%); + } +} + +/* 表格区域 */ +.authorization__table { + align-self: stretch; + overflow-x: auto; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.authorization__tableHeader { + align-self: stretch; + min-width: 800px; + background-color: #f3f4f6; + display: flex; + align-items: center; +} + +.authorization__tableRow { + align-self: stretch; + min-width: 800px; + display: flex; + align-items: center; + border-bottom: 1px solid #e5e7eb; + @include transition-fast; + + &:hover { + background-color: #f9fafb; + } +} + +.authorization__tableCell { + display: flex; + align-items: center; + padding: 12px 24px; + font-size: 14px; + line-height: 20px; + color: #1e293b; + + &--header { + font-size: 12px; + font-weight: 700; + color: #6b7280; + text-transform: uppercase; + } + + &--avatar { + width: 80px; + flex-shrink: 0; + justify-content: center; + } + + &--nickname { + width: 120px; + flex-shrink: 0; + font-weight: 500; + } + + &--accountId { + width: 120px; + flex-shrink: 0; + } + + &--province { + width: 100px; + flex-shrink: 0; + } + + &--city { + width: 140px; + flex-shrink: 0; + } + + &--teamAdoptions { + width: 180px; + flex-shrink: 0; + } + + &--status { + width: 120px; + flex-shrink: 0; + } + + &--actions { + flex: 1; + min-width: 200px; + gap: 12px; + } +} + +/* 用户头像 */ +.authorization__avatar { + width: 40px; + height: 40px; + border-radius: 9999px; + background-size: cover; + background-position: center; + background-color: #e5e7eb; +} + +/* 状态徽章 */ +.authorization__badge { + border-radius: 9999px; + padding: 2.5px 10px; + font-size: 12px; + line-height: 16px; + font-weight: 500; + + &--authorized { + background-color: #dcfce7; + color: #166534; + } + + &--pending { + background-color: #fef9c3; + color: #854d0e; + } +} + +/* 操作按钮 */ +.authorization__actionBtn { + cursor: pointer; + border: none; + padding: 0; + background-color: transparent; + font-size: 12px; + line-height: 16px; + font-family: inherit; + @include transition-fast; + + &--authorize { + color: #2563eb; + + &:hover { + text-decoration: underline; + } + } + + &--revoke { + color: #dc2626; + + &:hover { + text-decoration: underline; + } + } +} + +/* 帮助文本 */ +.authorization__help { + align-self: stretch; + font-size: 12px; + line-height: 16px; + color: #6b7280; + padding-top: 4px; +} + +/* 表单区域 */ +.authorization__form { + align-self: stretch; + display: flex; + flex-direction: column; + gap: 24px; +} + +.authorization__formRow { + align-self: stretch; + display: flex; + align-items: flex-start; + gap: 24px; + + @media (max-width: 768px) { + flex-direction: column; + } +} + +.authorization__formGroup { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + min-width: 250px; +} + +.authorization__formLabel { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #1e293b; +} + +.authorization__formHint { + font-size: 12px; + line-height: 16px; + color: #6b7280; +} + +.authorization__formInput { + align-self: stretch; + border-radius: 6px; + background-color: #f3f4f6; + border: 1px solid #e5e7eb; + padding: 8px 12px; + font-size: 14px; + line-height: 20px; + color: #1e293b; + outline: none; + font-family: inherit; +} + +/* 开关切换行 */ +.authorization__toggleRow { + align-self: stretch; + display: flex; + align-items: center; + justify-content: space-between; + padding: 9px 0; +} + +.authorization__toggleLabel { + flex: 1; + font-size: 14px; + line-height: 20px; + color: #1e293b; +} + +/* 开关样式 */ +.authorization__toggle { + position: relative; + width: 44px; + height: 24px; + border-radius: 9999px; + cursor: pointer; + @include transition-fast; + + &--on { + background-color: #005a9c; + } + + &--off { + background-color: #e5e7eb; + } +} + +.authorization__toggleHandle { + position: absolute; + top: 2px; + width: 20px; + height: 20px; + border-radius: 9999px; + background-color: #fff; + border: 1px solid #fff; + @include transition-fast; + + &--on { + left: 22px; + } + + &--off { + left: 2px; + } +} + +/* 保存按钮容器 */ +.authorization__saveWrapper { + align-self: stretch; + display: flex; + align-items: flex-end; + justify-content: flex-end; +} + +.authorization__saveBtn { + cursor: pointer; + border: none; + border-radius: 6px; + background-color: #005a9c; + padding: 8px 24px; + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #fff; + font-family: inherit; + @include transition-fast; + + &:hover { + background-color: darken(#005a9c, 5%); + } +} + +/* 正式公司授权管理区域 */ +.authorization__officialSection { + align-self: stretch; + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 24px; +} + +.authorization__officialCard { + flex: 1; + min-width: 310px; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 8px; + background-color: #fff; + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 24px; +} + +.authorization__officialTitle { + margin: 0; + font-size: 18px; + line-height: 28px; + font-weight: 700; + color: #1e293b; +} + +.authorization__officialDesc { + margin: 0; + font-size: 14px; + line-height: 20px; + color: #6b7280; + padding-bottom: 16px; +} + +.authorization__officialTable { + align-self: stretch; + background-color: #f3f4f6; + overflow: auto; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.authorization__officialRow { + align-self: stretch; + display: flex; + align-items: center; + flex-wrap: wrap; +} + +.authorization__officialCell { + flex: 1; + min-width: 56px; + padding: 8px 16px; + font-size: 12px; + line-height: 16px; + font-weight: 700; + color: #6b7280; + text-transform: uppercase; +} + +/* 授权限制规则 */ +.authorization__rules { + align-self: stretch; + display: flex; + flex-direction: column; + gap: 16px; +} + +.authorization__ruleItem { + align-self: stretch; + border-radius: 6px; + background-color: #f3f4f6; + padding: 12px; + font-size: 14px; + line-height: 20px; + color: #1e293b; +} + +.authorization__ruleInline { + display: flex; + align-items: center; + gap: 8px; +} + +.authorization__ruleNote { + font-size: 12px; + line-height: 16px; + color: #6b7280; +} + +.authorization__ruleInput { + width: 80px; + border-radius: 6px; + background-color: #fff; + border: 1px solid #e5e7eb; + padding: 8px 12px; + font-size: 14px; + line-height: 20px; + color: #1e293b; + text-align: center; + outline: none; + font-family: inherit; +} + +/* 特殊城市规则 */ +.authorization__specialRules { + align-self: stretch; + border-radius: 6px; + background-color: #f3f4f6; + padding: 12px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.authorization__specialTitle { + font-size: 14px; + line-height: 20px; + color: #1e293b; +} + +.authorization__specialTable { + align-self: stretch; + border-radius: 6px; + background-color: #fff; + overflow-x: auto; + display: flex; + flex-direction: column; +} + +.authorization__specialRow { + align-self: stretch; + display: flex; + align-items: center; +} + +.authorization__specialCity { + flex: 1; + padding: 10px 16px; + font-size: 14px; + line-height: 20px; + color: #1e293b; +} + +.authorization__specialToggle { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 16px; + text-align: right; +} + +.authorization__specialLabel { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #1e293b; +} + +/* 阶梯性考核目标 */ +.authorization__targetHeader { + align-self: stretch; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; +} + +.authorization__targetTitle { + margin: 0; + font-size: 18px; + line-height: 28px; + font-weight: 700; + color: #1e293b; +} + +.authorization__editBtn { + cursor: pointer; + border-radius: 6px; + border: 1px solid #e5e7eb; + background-color: transparent; + padding: 8px 16px; + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #6b7280; + font-family: inherit; + @include transition-fast; + + &:hover { + background-color: #f3f4f6; + } +} + +/* 目标表格 */ +.authorization__targetTable { + align-self: stretch; + overflow-x: auto; + display: flex; + flex-direction: column; +} + +.authorization__targetHeader2 { + align-self: stretch; + min-width: 660px; + background-color: #f3f4f6; + display: flex; + align-items: center; +} + +.authorization__targetBody { + align-self: stretch; + min-width: 660px; + display: flex; + flex-direction: column; +} + +.authorization__targetRow { + align-self: stretch; + display: flex; + align-items: center; + + &--bordered { + border-bottom: 1px solid #e5e7eb; + } +} + +.authorization__targetCell { + display: flex; + align-items: center; + justify-content: center; + padding: 12px 24px; + font-size: 14px; + line-height: 20px; + color: #1e293b; + text-align: center; + + &--header { + font-size: 12px; + font-weight: 700; + color: #6b7280; + text-transform: uppercase; + } + + &--month { + flex: 0.6; + min-width: 84px; + max-width: 132px; + } + + &--provinceMonthly, + &--provinceTotal, + &--cityTotal { + flex: 0.75; + min-width: 156px; + } + + &--cityMonthly { + flex: 1; + min-width: 108px; + } +} diff --git a/frontend/admin-web/src/app/(dashboard)/authorization/page.tsx b/frontend/admin-web/src/app/(dashboard)/authorization/page.tsx new file mode 100644 index 00000000..906e020f --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/authorization/page.tsx @@ -0,0 +1,447 @@ +'use client'; + +import { useState } from 'react'; +import { PageContainer } from '@/components/layout'; +import { cn } from '@/utils/helpers'; +import styles from './authorization.module.scss'; + +/** + * 省/市公司数据接口 + */ +interface CompanyItem { + id: string; + avatar: string; + nickname: string; + accountId: string; + province: string; + city?: string; + teamAdoptions: number; + authStatus: 'authorized' | 'pending'; +} + +/** + * 阶梯性考核目标数据接口 + */ +interface TargetItem { + month: string; + provinceMonthly: number; + provinceTotal: number; + cityMonthly: number; + cityTotal: number; +} + +/** + * 特殊城市规则接口 + */ +interface SpecialCityRule { + city: string; + enabled: boolean; +} + +// 模拟省公司数据 +const mockProvinceCompanies: CompanyItem[] = [ + { id: '1', avatar: '', nickname: '用户昵称一', accountId: '123456789', province: '广东省', teamAdoptions: 1234, authStatus: 'authorized' }, +]; + +// 模拟市公司数据 +const mockCityCompanies: CompanyItem[] = [ + { id: '1', avatar: '', nickname: '用户昵称二', accountId: '987654321', province: '广东省', city: '深圳市', teamAdoptions: 567, authStatus: 'pending' }, +]; + +// 阶梯性考核目标数据 +const targetData: TargetItem[] = [ + { month: '第 1 个月', provinceMonthly: 500, provinceTotal: 500, cityMonthly: 100, cityTotal: 100 }, + { month: '第 2 个月', provinceMonthly: 500, provinceTotal: 1000, cityMonthly: 100, cityTotal: 200 }, + { month: '...', provinceMonthly: 0, provinceTotal: 0, cityMonthly: 0, cityTotal: 0 }, + { month: '第 9 个月', provinceMonthly: 1000, provinceTotal: 6000, cityMonthly: 200, cityTotal: 1200 }, +]; + +// 特殊城市规则 +const specialCities: SpecialCityRule[] = [ + { city: '北京市', enabled: false }, + { city: '上海市', enabled: true }, +]; + +/** + * 授权管理页面 + * 基于 UIPro Figma 设计实现 + */ +export default function AuthorizationPage() { + // 省公司规则状态 + const [provinceThreshold, setProvinceThreshold] = useState('500'); + const [provinceBenefit, setProvinceBenefit] = useState('20'); + const [provinceAfterBenefit, setProvinceAfterBenefit] = useState('20'); + const [provinceResetEnabled, setProvinceResetEnabled] = useState(true); + + // 市公司规则状态 + const [cityThreshold, setCityThreshold] = useState('100'); + const [cityBenefit, setCityBenefit] = useState('40'); + const [cityAfterBenefit, setCityAfterBenefit] = useState('40'); + const [cityResetEnabled, setCityResetEnabled] = useState(true); + + // 授权限制规则 + const [topRankLimit, setTopRankLimit] = useState('10'); + const [specialRules, setSpecialRules] = useState(specialCities); + + // 渲染开关组件 + const renderToggle = (checked: boolean, onChange: (checked: boolean) => void) => ( +
onChange(!checked)} + role="switch" + aria-checked={checked} + tabIndex={0} + onKeyDown={(e) => e.key === 'Enter' && onChange(!checked)} + > +
+
+ ); + + // 渲染省公司表格 + const renderProvinceTable = () => ( +
+
+
头像
+
昵称
+
账户序列号
+
所属省份
+
伞下团队认种总量(棵)
+
授权状态
+
操作
+
+ {mockProvinceCompanies.map((item) => ( +
+
+
+
+
{item.nickname}
+
{item.accountId}
+
{item.province}
+
{item.teamAdoptions.toLocaleString()}
+
+ + {item.authStatus === 'authorized' ? '已授权' : '待授权'} + +
+
+ + +
+
+ ))} +
+ ); + + // 渲染市公司表格 + const renderCityTable = () => ( +
+
+
头像
+
昵称
+
账户序列号
+
所属省/市
+
伞下团队认种总量(棵)
+
授权状态
+
操作
+
+ {mockCityCompanies.map((item) => ( +
+
+
+
+
{item.nickname}
+
{item.accountId}
+
{item.province} / {item.city}
+
{item.teamAdoptions}
+
+ + {item.authStatus === 'authorized' ? '已授权' : '待授权'} + +
+
+ + +
+
+ ))} +
+ ); + + return ( + +
+ {/* 授权省公司管理 */} +
+

授权省公司管理

+
+ + + + +
+ {renderProvinceTable()} +

帮助:在此管理和筛选具备省公司资格的账号,并发起授权操作。

+
+ + {/* 授权省公司团队权益考核规则 */} +
+

授权省公司团队权益考核规则

+
+
+
+ + setProvinceThreshold(e.target.value)} + placeholder="500" + /> +
+
+ + setProvinceBenefit(e.target.value)} + placeholder="20" + /> + 即,累计完成 {provinceThreshold} 棵目标前,每新增 1 棵获得的权益 +
+
+
+
+ + setProvinceAfterBenefit(e.target.value)} + placeholder="20" + /> +
+
+ + +
+
+
+
+ 未达成本月考核目标时,权益失效并从 0 重新累计 {provinceThreshold} 棵 + {renderToggle(provinceResetEnabled, setProvinceResetEnabled)} +
+
+ +
+
+ + {/* 授权市公司管理 */} +
+

授权市公司管理

+
+ + + + + +
+ {renderCityTable()} +

帮助:在此管理和筛选具备市公司资格的账号,并发起授权操作。

+
+ + {/* 授权市公司团队权益考核规则 */} +
+

授权市公司团队权益考核规则

+
+
+
+ + setCityThreshold(e.target.value)} + placeholder="100" + /> +
+
+ + setCityBenefit(e.target.value)} + placeholder="40" + /> + 即,累计完成 {cityThreshold} 棵目标前,每新增 1 棵获得的权益 +
+
+
+
+ + setCityAfterBenefit(e.target.value)} + placeholder="40" + /> +
+
+ + +
+
+
+
+ 未达成本月考核目标时,权益失效并从 0 重新累计 {cityThreshold} 棵 + {renderToggle(cityResetEnabled, setCityResetEnabled)} +
+
+ +
+
+ + {/* 正式省公司/市公司授权管理 */} +
+
+

正式省公司授权管理

+

当省公司完成全部阶梯性考核后,可在此授权为正式省公司。

+
+
+
头像
+
昵称
+
序列号
+
省份
+
状态
+
操作
+
+
+
+
+

正式市公司授权管理

+

当市公司完成全部阶梯性考核后,可在此授权为正式市公司。

+
+
+
头像
+
昵称
+
序列号
+
省/市
+
状态
+
操作
+
+
+
+
+ + {/* 省公司 / 市公司 授权限制规则 */} +
+

省公司 / 市公司 授权限制规则

+
+
每个省份仅可授权 1 个省公司账号
+
+
+ 每个城市仅可授权 1 个市公司账号 + (例如:广东省深圳市) +
+
+
+
+ 伞下团队认种总量排名前 + setTopRankLimit(e.target.value)} + aria-label="排名限制" + /> + 的账号具备授权资格 +
+
+
+
特殊城市授权规则 (如直辖市、特别行政区)
+
+ {specialRules.map((rule, index) => ( +
+
{rule.city}
+
+ {renderToggle(rule.enabled, (enabled) => { + const newRules = [...specialRules]; + newRules[index] = { ...rule, enabled }; + setSpecialRules(newRules); + })} + 启用特例 +
+
+ ))} +
+
+
+
+ + {/* 省公司 / 市公司 阶梯性考核目标 */} +
+
+

省公司 / 市公司 阶梯性考核目标

+ +
+
+
+
考核月
+
省代当月目标(棵)
+
省代累计目标(棵)
+
市代当月目标(棵)
+
市代累计目标(棵)
+
+
+ {targetData.map((row, index) => ( +
+
{row.month}
+
{row.month === '...' ? '...' : row.provinceMonthly}
+
{row.month === '...' ? '...' : row.provinceTotal}
+
{row.month === '...' ? '...' : row.cityMonthly}
+
{row.month === '...' ? '...' : row.cityTotal}
+
+ ))} +
+
+

帮助:完成全部 9 个月阶梯考核后,可升级为正式省/市公司。

+
+
+
+ ); +} diff --git a/frontend/admin-web/src/app/(dashboard)/dashboard/dashboard.module.scss b/frontend/admin-web/src/app/(dashboard)/dashboard/dashboard.module.scss new file mode 100644 index 00000000..3e2b8a15 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/dashboard/dashboard.module.scss @@ -0,0 +1,50 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.dashboard { + display: flex; + flex-direction: column; + gap: $spacing-xl; + + &__stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: $spacing-lg; + + @include respond-below(xl) { + grid-template-columns: repeat(2, 1fr); + } + + @include respond-below(md) { + grid-template-columns: 1fr; + } + } + + &__charts { + display: grid; + grid-template-columns: 1fr 360px; + gap: $spacing-lg; + + @include respond-below(xl) { + grid-template-columns: 1fr; + } + } + + &__mainChart { + min-height: 400px; + } + + &__sidePanel { + min-height: 400px; + } + + &__bottom { + display: grid; + grid-template-columns: 1fr; + gap: $spacing-lg; + } + + &__regionChart { + min-height: 300px; + } +} diff --git a/frontend/admin-web/src/app/(dashboard)/dashboard/page.tsx b/frontend/admin-web/src/app/(dashboard)/dashboard/page.tsx new file mode 100644 index 00000000..bcb9a4d2 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/dashboard/page.tsx @@ -0,0 +1,155 @@ +'use client'; + +import { Button } from '@/components/common'; +import { PageContainer } from '@/components/layout'; +import { StatCard } from '@/components/features/dashboard/StatCard'; +import { TrendChart } from '@/components/features/dashboard/TrendChart'; +import { RegionDistribution } from '@/components/features/dashboard/RegionDistribution'; +import { RecentActivity } from '@/components/features/dashboard/RecentActivity'; +import styles from './dashboard.module.scss'; + +// 模拟统计数据 +const statsData = [ + { + title: '总认种量', + value: 12580, + suffix: '棵', + change: { value: 5.6, trend: 'up' as const }, + color: '#1565C0', + }, + { + title: '活跃用户', + value: 3240, + suffix: '人', + change: { value: 3.2, trend: 'up' as const }, + color: '#4CAF50', + }, + { + title: '省级公司', + value: 28, + suffix: '家', + change: { value: 2.1, trend: 'up' as const }, + color: '#F5A623', + }, + { + title: '市级公司', + value: 156, + suffix: '家', + change: { value: 4.8, trend: 'up' as const }, + color: '#9C27B0', + }, +]; + +// 模拟趋势数据 +const trendData = [ + { date: '03-19', value: 150 }, + { date: '03-20', value: 280 }, + { date: '03-21', value: 320 }, + { date: '03-22', value: 250 }, + { date: '03-23', value: 380 }, + { date: '03-24', value: 420 }, + { date: '03-25', value: 390 }, +]; + +// 模拟区域分布数据 +const regionData = [ + { region: '华东地区', percentage: 35, color: '#1565C0' }, + { region: '华南地区', percentage: 25, color: '#4CAF50' }, + { region: '华北地区', percentage: 20, color: '#F5A623' }, + { region: '华中地区', percentage: 12, color: '#9C27B0' }, + { region: '其他地区', percentage: 8, color: '#607D8B' }, +]; + +// 模拟最近活动数据 +const activityData = [ + { + id: '1', + type: 'user_register' as const, + icon: '👤', + title: '新用户注册', + description: '用户 张三 完成注册', + timestamp: '5分钟前', + }, + { + id: '2', + type: 'company_activity' as const, + icon: '🏢', + title: '公司授权', + description: '广东省公司完成授权', + timestamp: '15分钟前', + }, + { + id: '3', + type: 'system_update' as const, + icon: '⚙️', + title: '系统更新', + description: '龙虎榜规则已更新', + timestamp: '1小时前', + }, + { + id: '4', + type: 'report_generated' as const, + icon: '📊', + title: '报表生成', + description: '3月份运营报表已生成', + timestamp: '2小时前', + }, + { + id: '5', + type: 'user_register' as const, + icon: '👤', + title: '新用户注册', + description: '用户 李四 完成注册', + timestamp: '3小时前', + }, +]; + +export default function DashboardPage() { + const headerActions = ( + <> + + + + + ); + + return ( + +
+ {/* 统计卡片区 */} +
+ {statsData.map((stat, index) => ( + + ))} +
+ + {/* 图表区 */} +
+
+ +
+
+ +
+
+ + {/* 底部区域 */} +
+
+ +
+
+
+
+ ); +} diff --git a/frontend/admin-web/src/app/(dashboard)/help/help.module.scss b/frontend/admin-web/src/app/(dashboard)/help/help.module.scss new file mode 100644 index 00000000..d69487ee --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/help/help.module.scss @@ -0,0 +1,452 @@ +/* 帮助中心页面样式 - 基于 UIPro Figma 设计 */ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.help { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; + font-family: 'Noto Sans SC', $font-family-base; +} + +/* 页面头部 */ +.help__header { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + max-width: 960px; +} + +.help__title { + margin: 0; + font-size: 30px; + line-height: 36px; + font-weight: 700; + color: #0f172a; +} + +.help__desc { + font-size: 16px; + line-height: 24px; + color: #6b7280; +} + +/* 搜索框 */ +.help__searchBox { + align-self: stretch; + display: flex; + align-items: center; + padding-top: 11px; +} + +.help__searchIcon { + width: 20px; + height: 20px; + color: #6b7280; + margin-right: -28px; + z-index: 1; +} + +.help__searchInput { + flex: 1; + max-width: 928px; + border-radius: 6px; + background-color: #fff; + border: 1px solid #e5e7eb; + padding: 8px 12px 8px 36px; + font-size: 16px; + line-height: 24px; + color: #0f172a; + font-family: 'Noto Sans SC', $font-family-base; + + &::placeholder { + color: #6b7280; + } + + &:focus { + outline: none; + border-color: #0061a8; + } +} + +/* 通用卡片区块 */ +.help__section { + width: 100%; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 8px; + background-color: #fff; + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 24px; + gap: 16px; + max-width: 960px; +} + +.help__sectionTitle { + margin: 0; + font-size: 18px; + line-height: 28px; + font-weight: 700; + color: #0f172a; +} + +/* 推荐文档卡片列表 */ +.help__docCards { + align-self: stretch; + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 16px; +} + +/* 单个推荐文档卡片 */ +.help__docCard { + flex: 1; + min-width: 274px; + border-radius: 8px; + background-color: #f3f4f6; + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 16px; + gap: 3px; +} + +.help__docCardTitle { + font-size: 16px; + line-height: 24px; + font-weight: 500; + color: #0061a8; + margin: 0; +} + +.help__docCardDesc { + font-size: 14px; + line-height: 20px; + color: #6b7280; + margin: 0; +} + +.help__docCardFooter { + align-self: stretch; + border-top: 1px solid #e5e7eb; + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 16px; + margin-top: 13px; +} + +.help__docCardQuestion { + font-size: 12px; + line-height: 16px; + color: #6b7280; +} + +.help__docCardBtns { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.help__docCardBtn { + cursor: pointer; + border-radius: 6px; + border: 1px solid #e5e7eb; + background-color: transparent; + display: flex; + align-items: center; + justify-content: center; + padding: 4px 8px; + gap: 4px; + font-size: 12px; + line-height: 16px; + color: #0f172a; + @include transition-fast; + + &:hover { + background-color: #f9fafb; + } + + svg { + width: 16px; + height: 16px; + } +} + +/* 文档列表表格 */ +.help__tableWrapper { + align-self: stretch; + border-radius: 6px; + border: 1px solid #e5e7eb; + overflow: auto; +} + +.help__table { + min-width: 447px; + display: flex; + flex-direction: column; +} + +.help__tableHead { + background-color: #f8fafc; + display: flex; + align-items: center; +} + +.help__tableHeadCell { + padding: 12px; + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #64748b; + + &--title { + flex: 1; + min-width: 164px; + } + + &--category { + flex: 1; + min-width: 88px; + max-width: 168px; + } + + &--date { + flex: 1; + min-width: 103px; + max-width: 202px; + } + + &--rating { + flex: 1; + min-width: 92px; + max-width: 193px; + text-align: center; + } +} + +.help__tableBody { + display: flex; + flex-direction: column; +} + +.help__tableRow { + display: flex; + align-items: center; + border-top: 1px solid #e5e7eb; + + &:first-child { + border-top: none; + } +} + +.help__tableCell { + padding: 12px; + font-size: 14px; + line-height: 20px; + color: #6b7280; + + &--title { + flex: 1; + min-width: 164px; + color: #0061a8; + font-weight: 500; + cursor: pointer; + + &:hover { + text-decoration: underline; + } + } + + &--category { + flex: 1; + min-width: 88px; + max-width: 168px; + } + + &--date { + flex: 1; + min-width: 103px; + max-width: 202px; + } + + &--rating { + flex: 1; + min-width: 92px; + max-width: 193px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + } +} + +.help__ratingBtn { + cursor: pointer; + width: 30px; + height: 30px; + border-radius: 9999px; + border: none; + background-color: transparent; + display: flex; + align-items: center; + justify-content: center; + @include transition-fast; + + &:hover { + background-color: #f3f4f6; + } + + svg { + width: 20px; + height: 20px; + color: #64748b; + } + + &--active svg { + color: #0061a8; + } +} + +/* FAQ 和联系支持区域 */ +.help__bottomSection { + width: 100%; + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 24px; + max-width: 960px; +} + +/* FAQ 卡片 */ +.help__faqCard { + flex: 1; + min-width: 310px; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 8px; + background-color: #fff; + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 24px; + gap: 16px; +} + +.help__faqTitle { + margin: 0; + font-size: 18px; + line-height: 28px; + font-weight: 700; + color: #0f172a; +} + +.help__faqList { + align-self: stretch; + display: flex; + flex-direction: column; + gap: 16px; +} + +.help__faqItem { + align-self: stretch; + display: flex; + flex-direction: column; + gap: 3px; +} + +.help__faqQuestion { + font-size: 16px; + line-height: 24px; + font-weight: 500; + color: #0f172a; + margin: 0; +} + +.help__faqAnswer { + font-size: 14px; + line-height: 20px; + color: #6b7280; + margin: 0; +} + +/* 联系支持卡片 */ +.help__contactCard { + flex: 1; + min-width: 310px; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 8px; + background-color: #fff; + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 24px; + gap: 16px; +} + +.help__contactTitle { + margin: 0; + font-size: 18px; + line-height: 28px; + font-weight: 700; + color: #0f172a; +} + +.help__contactList { + align-self: stretch; + display: flex; + flex-direction: column; + gap: 16px; +} + +.help__contactItem { + align-self: stretch; + display: flex; + align-items: flex-start; + gap: 12px; +} + +.help__contactIcon { + width: 24px; + height: 24px; + color: #0061a8; + flex-shrink: 0; + + svg { + width: 100%; + height: 100%; + } +} + +.help__contactInfo { + flex: 1; + display: flex; + flex-direction: column; +} + +.help__contactLabel { + font-size: 16px; + line-height: 24px; + font-weight: 500; + color: #0f172a; + margin: 0; +} + +.help__contactValue { + font-size: 14px; + line-height: 20px; + color: #6b7280; + margin: 0; +} diff --git a/frontend/admin-web/src/app/(dashboard)/help/page.tsx b/frontend/admin-web/src/app/(dashboard)/help/page.tsx new file mode 100644 index 00000000..8782cecb --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/help/page.tsx @@ -0,0 +1,256 @@ +'use client'; + +import { useState } from 'react'; +import { PageContainer } from '@/components/layout'; +import { cn } from '@/utils/helpers'; +import styles from './help.module.scss'; + +/** + * 推荐文档数据接口 + */ +interface RecommendedDoc { + id: string; + title: string; + description: string; +} + +/** + * 文档列表数据接口 + */ +interface DocItem { + id: string; + title: string; + category: string; + lastUpdate: string; +} + +/** + * FAQ 数据接口 + */ +interface FAQItem { + question: string; + answer: string; +} + +// 推荐文档数据 +const recommendedDocs: RecommendedDoc[] = [ + { + id: '1', + title: '如何进行首次认种?', + description: '本文将引导您完成从注册到成功认种第一棵榴莲树的全部流程...', + }, + { + id: '2', + title: '结算参数配置指南', + description: '详细解释各项结算参数的含义,以及如何根据业务需求进行配置...', + }, + { + id: '3', + title: '龙虎榜规则详解', + description: '了解日榜、周榜、月榜的计算逻辑,以及虚拟账户的排名规则...', + }, +]; + +// 文档列表数据 +const docList: DocItem[] = [ + { id: '1', title: '考核规则设置说明', category: '系统设置', lastUpdate: '2023-10-25' }, + { id: '2', title: '用户管理与角色授权', category: '账号安全', lastUpdate: '2023-10-24' }, + { id: '3', title: '认种限额策略配置', category: '系统设置', lastUpdate: '2023-10-22' }, + { id: '4', title: '前端热度展示方式说明', category: '系统设置', lastUpdate: '2023-10-20' }, +]; + +// FAQ 数据 +const faqData: FAQItem[] = [ + { + question: 'Q: 忘记密码怎么办?', + answer: 'A: 您可以通过登录页面的"忘记密码"链接,使用注册邮箱或手机号重置密码。', + }, + { + question: 'Q: 如何导出数据报表?', + answer: 'A: 在"数据统计"页面,选择您需要的时间范围和数据类型,然后点击右上角的"导出"按钮即可。', + }, + { + question: 'Q: 敏感操作审批流程是怎样的?', + answer: 'A: 当您执行一项被标记为敏感的操作时,系统会要求多名管理员进行审批。只有达到预设的审批人数后,操作才会生效。', + }, +]; + +// 搜索图标 +const SearchIcon = () => ( + + + + +); + +// 点赞图标 +const ThumbUpIcon = () => ( + + + +); + +// 点踩图标 +const ThumbDownIcon = () => ( + + + +); + +// 电话图标 +const PhoneIcon = () => ( + + + +); + +// 邮件图标 +const MailIcon = () => ( + + + + +); + +// 聊天图标 +const ChatIcon = () => ( + + + +); + +export default function HelpPage() { + const [searchKeyword, setSearchKeyword] = useState(''); + + return ( + +
+ {/* 页面头部 */} +
+

帮助中心

+

查找文档、常见问题解答,或联系我们的支持团队。

+
+ + + + setSearchKeyword(e.target.value)} + /> +
+
+ + {/* 推荐文档 */} +
+

推荐文档

+
+ {recommendedDocs.map((doc) => ( +
+

{doc.title}

+

{doc.description}

+
+ 这篇文章有用吗? +
+ + +
+
+
+ ))} +
+
+ + {/* 文档列表 */} +
+

文档列表

+
+
+
+
标题
+
分类
+
最后更新
+
评价
+
+
+ {docList.map((doc) => ( +
+
{doc.title}
+
{doc.category}
+
{doc.lastUpdate}
+
+ + +
+
+ ))} +
+
+
+
+ + {/* FAQ 和联系支持 */} +
+ {/* 常见问题 */} +
+

常见问题 (FAQ)

+
+ {faqData.map((faq, index) => ( +
+

{faq.question}

+

{faq.answer}

+
+ ))} +
+
+ + {/* 联系支持 */} +
+

联系支持

+
+
+ + + +
+

技术支持热线

+

400-123-4567 (工作日 9:00 - 18:00)

+
+
+
+ + + +
+

支持邮箱

+

support@durian-adopt.com

+
+
+
+ + + +
+

在线客服

+

点击右下角浮动窗口,立即与我们的客服代表在线沟通。

+
+
+
+
+
+
+
+ ); +} diff --git a/frontend/admin-web/src/app/(dashboard)/layout.tsx b/frontend/admin-web/src/app/(dashboard)/layout.tsx new file mode 100644 index 00000000..7941db64 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/layout.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { ReactNode } from 'react'; +import { ToastContainer } from '@/components/common'; + +export default function DashboardLayout({ children }: { children: ReactNode }) { + return ( + <> + {children} + + + ); +} diff --git a/frontend/admin-web/src/app/(dashboard)/leaderboard/leaderboard.module.scss b/frontend/admin-web/src/app/(dashboard)/leaderboard/leaderboard.module.scss new file mode 100644 index 00000000..f77a3189 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/leaderboard/leaderboard.module.scss @@ -0,0 +1,639 @@ +/* 龙虎榜页面样式 - 基于 UIPro Figma 设计 */ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.leaderboard { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; + font-family: 'Noto Sans SC', $font-family-base; +} + +/* 页面头部 */ +.leaderboard__header { + align-self: stretch; + backdrop-filter: blur(4px); + background-color: rgba(255, 255, 255, 0.7); + border-bottom: 1px solid #e5e7eb; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + padding: 10px 0; + gap: 10px; +} + +.leaderboard__title { + margin: 0; + font-size: 20px; + line-height: 28px; + font-weight: 700; + color: #0f172a; +} + +.leaderboard__exportBtn { + cursor: pointer; + border: none; + padding: 8px 12px; + background-color: #fff; + box-shadow: 0px 0px 0px #fff inset, 0px 0px 0px 1px #cbd5e1 inset, 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + @include transition-fast; + + &:hover { + background-color: #f9fafb; + } + + svg { + width: 18px; + height: 18px; + color: #334155; + } +} + +.leaderboard__exportText { + font-size: 14px; + line-height: 20px; + font-weight: 600; + color: #334155; +} + +/* 主内容区域 */ +.leaderboard__content { + align-self: stretch; + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 24px; +} + +/* 榜单列表区域 */ +.leaderboard__boards { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; + min-width: 346px; +} + +/* 单个榜单卡片 */ +.leaderboard__board { + align-self: stretch; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 12px; + background-color: #fff; + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 24px; + gap: 16px; +} + +/* 榜单头部 */ +.leaderboard__boardHeader { + align-self: stretch; + border-bottom: 1px solid #e5e7eb; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + padding-bottom: 16px; + gap: 10px; +} + +.leaderboard__boardInfo { + display: flex; + align-items: center; + gap: 16px; +} + +.leaderboard__boardTitle { + margin: 0; + font-size: 18px; + line-height: 28px; + font-weight: 600; + color: #0f172a; +} + +.leaderboard__boardDesc { + font-size: 14px; + line-height: 20px; + color: #64748b; +} + +.leaderboard__boardToggle { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #64748b; +} + +.leaderboard__toggleLabel { + font-weight: 500; +} + +/* 排名表格 */ +.leaderboard__table { + align-self: stretch; + overflow-x: auto; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.leaderboard__tableHeader { + align-self: stretch; + display: flex; + align-items: flex-start; + min-width: 453px; +} + +.leaderboard__tableHeaderCell { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 12px 16px; + font-size: 14px; + line-height: 20px; + font-weight: 600; + color: #64748b; + + &--rank { + flex: 1; + min-width: 61px; + max-width: 96px; + } + + &--avatar { + width: 64px; + } + + &--nickname { + flex: 1; + min-width: 154px; + } + + &--count { + flex: 1; + min-width: 87px; + max-width: 128px; + } + + &--team { + flex: 1; + min-width: 87px; + max-width: 128px; + } +} + +.leaderboard__tableBody { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + min-width: 453px; +} + +.leaderboard__tableRow { + align-self: stretch; + display: flex; + align-items: center; + border-top: 1px solid #f3f4f6; + + &--gold { + background-color: #f3f4f6; + } + + &--silver { + background-color: #f8f8f8; + } + + &--bronze { + background-color: #f3f4f6; + } +} + +.leaderboard__tableCell { + display: flex; + align-items: center; + padding: 16px; + font-size: 14px; + line-height: 20px; + color: #475569; + + &--rank { + flex: 1; + min-width: 61px; + max-width: 96px; + } + + &--avatar { + width: 64px; + padding: 0; + } + + &--nickname { + flex: 1; + min-width: 154px; + gap: 8px; + color: #1e293b; + font-weight: 500; + } + + &--count { + flex: 1; + min-width: 87px; + max-width: 128px; + font-weight: 500; + } + + &--team { + flex: 1; + min-width: 87px; + max-width: 128px; + } +} + +/* 排名样式 */ +.leaderboard__rankMedal { + font-size: 16px; + line-height: 24px; + font-weight: 700; + + &--gold { + color: #d4af37; + } + + &--silver { + color: #a0a0a0; + } + + &--bronze { + color: #cd7f32; + } + + &--normal { + color: #64748b; + font-weight: 500; + } +} + +/* 头像 */ +.leaderboard__avatar { + width: 60px; + height: 60px; + border-radius: 9999px; + object-fit: cover; +} + +/* 虚拟标签 */ +.leaderboard__virtualTag { + border-radius: 9999px; + background-color: #e5e7eb; + padding: 2px 10px; + font-size: 12px; + line-height: 16px; + font-weight: 600; + color: #475569; +} + +/* 空状态 */ +.leaderboard__empty { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 64px 0; + gap: 16px; +} + +.leaderboard__emptyIcon { + width: 64px; + height: 64px; + border-radius: 9999px; + background-color: #f1f5f9; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 32px; + height: 32px; + color: #94a3b8; + } +} + +.leaderboard__emptyTitle { + font-size: 18px; + line-height: 28px; + font-weight: 600; + color: #334155; + margin: 0; +} + +.leaderboard__emptyStatus { + font-size: 14px; + line-height: 20px; + color: #64748b; +} + +/* 设置面板 */ +.leaderboard__settings { + flex: 0 0 auto; + width: 100%; + max-width: 384px; + min-width: 316px; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 12px; + background-color: #fff; + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 24px; + gap: 23px; +} + +.leaderboard__settingsTitle { + align-self: stretch; + font-size: 18px; + line-height: 28px; + font-weight: 600; + color: #0f172a; + margin: 0; +} + +.leaderboard__settingsContent { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 32px; +} + +.leaderboard__settingsSection { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 14px; +} + +.leaderboard__sectionTitle { + font-size: 16px; + line-height: 24px; + font-weight: 600; + color: #1e293b; + margin: 0; +} + +.leaderboard__settingsItem { + align-self: stretch; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.leaderboard__settingsLabel { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #334155; +} + +/* 数量输入和滑块 */ +.leaderboard__sliderGroup { + display: flex; + align-items: center; + gap: 16px; +} + +.leaderboard__numberInput { + width: 80px; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 6px; + background-color: #fff; + border: 1px solid #cbd5e1; + padding: 6px 12px; + font-size: 14px; + line-height: 20px; + color: #1e293b; + text-align: right; + + &:focus { + outline: none; + border-color: #005a9c; + } +} + +.leaderboard__slider { + flex: 1; + height: 8px; + border-radius: 12px; + background-color: #e2e8f0; + appearance: none; + cursor: pointer; + + &::-webkit-slider-thumb { + appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: #4f46e5; + cursor: pointer; + } + + &::-moz-range-thumb { + width: 20px; + height: 20px; + border-radius: 50%; + background-color: #4f46e5; + cursor: pointer; + border: none; + } +} + +/* 规则说明折叠 */ +.leaderboard__rulesToggle { + align-self: stretch; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + padding: 8px 0; + + span { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #334155; + } + + svg { + width: 24px; + height: 24px; + color: #64748b; + @include transition-fast; + } + + &--expanded svg { + transform: rotate(180deg); + } +} + +/* 实时预览 */ +.leaderboard__preview { + align-self: stretch; + display: flex; + flex-direction: column; + gap: 8px; +} + +.leaderboard__previewTitle { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #334155; +} + +.leaderboard__previewList { + align-self: stretch; + border-radius: 6px; + background-color: #f8fafc; + border: 1px dashed #cbd5e1; + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 12px; + gap: 8px; +} + +.leaderboard__previewItem { + align-self: stretch; + display: flex; + align-items: center; + gap: 12px; +} + +.leaderboard__previewAvatar { + width: 32px; + height: 32px; + border-radius: 9999px; + object-fit: cover; +} + +.leaderboard__previewName { + flex: 1; + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #1e293b; +} + +.leaderboard__previewTag { + border-radius: 9999px; + background-color: #e5e7eb; + padding: 2px 10px; + font-size: 12px; + line-height: 16px; + font-weight: 600; + color: #475569; +} + +/* 显示设置选择器 */ +.leaderboard__select { + align-self: stretch; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 6px; + background-color: #fff; + border: 1px solid #cbd5e1; + padding: 9px 12px; + font-size: 14px; + line-height: 20px; + color: #1e293b; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 12px center; + background-repeat: no-repeat; + background-size: 16px; + padding-right: 40px; +} + +/* 保存按钮区域 */ +.leaderboard__settingsFooter { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-end; + padding-top: 24px; + border-top: 1px solid #e5e7eb; +} + +.leaderboard__saveBtn { + cursor: pointer; + border: none; + padding: 10px 16px; + background-color: #4f46e5; + align-self: stretch; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + line-height: 20px; + font-weight: 600; + color: #fff; + @include transition-fast; + + &:hover { + background-color: #4338ca; + } +} + +/* Toggle 开关样式 */ +.leaderboard__toggle { + position: relative; + width: 44px; + height: 24px; + border-radius: 9999px; + cursor: pointer; + @include transition-fast; + + &--on { + background-color: #4f46e5; + } + + &--off { + background-color: #cbd5e1; + } +} + +.leaderboard__toggleHandle { + position: absolute; + top: 4px; + width: 16px; + height: 16px; + border-radius: 9999px; + background-color: #fff; + @include transition-fast; + + &--on { + left: 24px; + } + + &--off { + left: 4px; + } +} diff --git a/frontend/admin-web/src/app/(dashboard)/leaderboard/page.tsx b/frontend/admin-web/src/app/(dashboard)/leaderboard/page.tsx new file mode 100644 index 00000000..3182801e --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/leaderboard/page.tsx @@ -0,0 +1,358 @@ +'use client'; + +import { useState } from 'react'; +import Image from 'next/image'; +import { toast } from '@/components/common'; +import { PageContainer } from '@/components/layout'; +import { cn } from '@/utils/helpers'; +import { useLeaderboardStore } from '@/store/zustand/useLeaderboardStore'; +import styles from './leaderboard.module.scss'; + +/** + * 排名数据类型 + */ +interface RankingItem { + rank: number; + avatar: string; + nickname: string; + isVirtual: boolean; + adoptionCount: number; + teamData: string; +} + +/** + * 模拟排名数据 + */ +const dailyRankings: RankingItem[] = [ + { rank: 1, avatar: '/images/Data@2x.png', nickname: '用户昵称一', isVirtual: true, adoptionCount: 150, teamData: 'A队' }, + { rank: 2, avatar: '/images/Data1@2x.png', nickname: '用户昵称二', isVirtual: false, adoptionCount: 145, teamData: 'B队' }, + { rank: 3, avatar: '/images/Data2@2x.png', nickname: '用户昵称三', isVirtual: false, adoptionCount: 130, teamData: 'A队' }, + { rank: 4, avatar: '/images/Data3@2x.png', nickname: '用户昵称四', isVirtual: false, adoptionCount: 121, teamData: 'C队' }, +]; + +/** + * 虚拟用户预览数据 + */ +const virtualUsers = [ + { avatar: '/images/AB6AXuATdSdPxZTCZuKsRrRfrjo1cEn0u1vD6I1SdimH9qUtBih6fj9lJmAJPAHa57UwJb1EGDvHf04YODNVP2IOmDp0qirJN-p-F0UvUWm0ctNFxn0oCiuefeSgY4kDJSgN-A5SN0JxguQfNTdBXvo5uP2WNFDRKaPu1fnyOZMljP3J5eg0FHhLjqUcDr93KqiUcIOkFI-LXqS9fy9dcs17E-dCYbm6QcMVeMP-z472D-dTUv1DyqIbKdlsS208Le-Hh1-HFcsYrjOhjB4@2x.png', nickname: '用户昵称一' }, + { avatar: '/images/AB6AXuB0kRpHrVsNrZMQSzARbP4qj1R905TCYLOxsFKbEl3ZljGpFDr5WEUDOFq-mgudnfWCkVHfBxUKKy5xnonX-IONJm34ETzK1X-R4Ee-qW329XiGBZxdAGitzv-iJnIcOaN2j5ysVrOFb7Nm9-MAFXQ-zRcBJzPHQuI5i8R-U65SNjVfeo-KcMrO0gSMhn3tHzt-zKuC6MKShvkptqpnHMmdZ1cV2iKU-dd4TvFg2GIVj9Fc9-A22p-H4R4eG-cdV-JN0YTYXa9qWXI@2x.png', nickname: '用户昵称五' }, +]; + +/** + * 获取排名样式 + */ +const getRankStyle = (rank: number) => { + switch (rank) { + case 1: + return { medal: '🥇', color: 'gold', row: 'gold' }; + case 2: + return { medal: '🥈', color: 'silver', row: 'silver' }; + case 3: + return { medal: '🥉', color: 'bronze', row: 'bronze' }; + default: + return { medal: null, color: 'normal', row: '' }; + } +}; + +/** + * 龙虎榜管理页面 + * 基于 UIPro Figma 设计实现 + */ +export default function LeaderboardPage() { + const { virtualSettings, updateVirtualSettings, displaySettings, updateDisplaySettings } = useLeaderboardStore(); + + const [boardEnabled, setBoardEnabled] = useState({ + daily: true, + weekly: false, + monthly: false, + }); + + const [showRules, setShowRules] = useState(false); + + // 导出数据 + const handleExport = () => { + toast.success('导出功能开发中'); + }; + + // 保存设置 + const handleSave = () => { + toast.success('设置已保存'); + }; + + // 渲染切换开关 + const renderToggle = (checked: boolean, onChange: (checked: boolean) => void) => ( +
onChange(!checked)} + > +
+
+ ); + + // 渲染排名表格行 + const renderRankingRow = (item: RankingItem) => { + const rankStyle = getRankStyle(item.rank); + + return ( +
+
+ + {rankStyle.medal ? `${rankStyle.medal} ${item.rank}` : item.rank} + +
+
+ {item.nickname} +
+
+ {item.nickname} + {item.isVirtual && 虚拟} +
+
+ {item.adoptionCount} 棵 +
+
+ {item.teamData} +
+
+ ); + }; + + return ( + +
+ {/* 页面头部 */} +
+

龙虎榜管理

+ +
+ + {/* 主内容区域 */} +
+ {/* 榜单列表 */} +
+ {/* 日榜 */} +
+
+
+

日榜

+ 每日更新的排行榜 +
+
+ 关闭 + {renderToggle(boardEnabled.daily, (checked) => + setBoardEnabled({ ...boardEnabled, daily: checked }) + )} + 开启 +
+
+ + {boardEnabled.daily ? ( +
+
+
+ 排名 +
+
+ 头像 +
+
+ 昵称 +
+
+ 认种数量 +
+
+ 团队数据 +
+
+
+ {dailyRankings.map(renderRankingRow)} +
+
+ ) : ( +
+
+ 空状态 +
+

榜单未开启

+ 待激活 +
+ )} +
+ + {/* 周榜 */} +
+
+
+

周榜

+ 每周更新的排行榜 +
+
+ 关闭 + {renderToggle(boardEnabled.weekly, (checked) => + setBoardEnabled({ ...boardEnabled, weekly: checked }) + )} + 开启 +
+
+ +
+
+ 空状态 +
+

榜单未开启

+ 待激活 +
+
+ + {/* 月榜 */} +
+
+
+

月榜

+ 每月更新的排行榜 +
+
+ 关闭 + {renderToggle(boardEnabled.monthly, (checked) => + setBoardEnabled({ ...boardEnabled, monthly: checked }) + )} + 开启 +
+
+ +
+
+ 空状态 +
+

榜单未开启

+ 待激活 +
+
+
+ + {/* 设置面板 */} +
+

榜单设置

+ +
+ {/* 虚拟排名设置 */} +
+

虚拟排名设置

+ +
+ 启用虚拟排名 + {renderToggle(virtualSettings.enabled, (checked) => + updateVirtualSettings({ enabled: checked }) + )} +
+ +
+ 虚拟账户数量 +
+ + updateVirtualSettings({ virtualAccountCount: parseInt(e.target.value) || 0 }) + } + /> + + updateVirtualSettings({ virtualAccountCount: parseInt(e.target.value) }) + } + /> +
+
+ +
setShowRules(!showRules)} + > + 规则说明 + 展开 +
+ + {/* 实时预览 */} +
+ 实时预览 +
+ {virtualUsers.map((user, index) => ( +
+ {user.nickname} + {user.nickname} + 虚拟 +
+ ))} +
+
+
+ + {/* 显示设置 */} +
+

显示设置

+ +
+ 前端显示数量 +
+ +
+
+ + {/* 保存按钮 */} +
+ +
+
+
+
+
+ ); +} diff --git a/frontend/admin-web/src/app/(dashboard)/settings/page.tsx b/frontend/admin-web/src/app/(dashboard)/settings/page.tsx new file mode 100644 index 00000000..0cdbaad2 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/settings/page.tsx @@ -0,0 +1,548 @@ +'use client'; + +import { useState } from 'react'; +import { PageContainer } from '@/components/layout'; +import { cn } from '@/utils/helpers'; +import styles from './settings.module.scss'; + +/** + * 后台账号数据接口 + */ +interface AdminAccount { + id: string; + name: string; + role: string; + status: 'active' | 'disabled'; + lastLogin: string; +} + +/** + * 操作日志数据接口 + */ +interface OperationLog { + id: string; + time: string; + account: string; + type: string; + description: string; + result: 'pass' | 'reject' | 'pending'; +} + +/** + * 考核目标数据接口 + */ +interface AssessmentTarget { + month: number; + provinceMonthly: number; + provinceCumulative: number; + cityMonthly: number; + cityCumulative: number; +} + +// 模拟后台账号数据 +const mockAccounts: AdminAccount[] = [ + { id: '1', name: 'admin', role: '超级管理员', status: 'active', lastLogin: '2023-10-26 10:30' }, + { id: '2', name: 'operator01', role: '运营人员', status: 'active', lastLogin: '2023-10-26 09:15' }, +]; + +// 模拟操作日志数据 +const mockLogs: OperationLog[] = [ + { id: '1', time: '10-26 10:35', account: 'admin', type: '修改配置', description: '修改结算参数', result: 'pass' }, +]; + +// 模拟考核目标数据 +const mockTargets: AssessmentTarget[] = [ + { month: 1, provinceMonthly: 100, provinceCumulative: 100, cityMonthly: 20, cityCumulative: 20 }, + { month: 2, provinceMonthly: 120, provinceCumulative: 220, cityMonthly: 25, cityCumulative: 45 }, +]; + +export default function SettingsPage() { + // 结算参数设置 + const [settlementCurrencies, setSettlementCurrencies] = useState(['BNB', 'USDT']); + const [defaultCurrency, setDefaultCurrency] = useState(''); + + // 龙虎榜设置 + const [enableVirtualAccount, setEnableVirtualAccount] = useState(false); + const [virtualAccountCount, setVirtualAccountCount] = useState(''); + const [dailyRankEnabled, setDailyRankEnabled] = useState(true); + const [weeklyRankEnabled, setWeeklyRankEnabled] = useState(true); + const [monthlyRankEnabled, setMonthlyRankEnabled] = useState(true); + const [displayCount, setDisplayCount] = useState(''); + + // 认种限额设置 + const [singleAccountLimitEnabled, setSingleAccountLimitEnabled] = useState(false); + const [singleAccountDays, setSingleAccountDays] = useState(''); + const [singleAccountTrees, setSingleAccountTrees] = useState(''); + const [networkLimitEnabled, setNetworkLimitEnabled] = useState(false); + const [networkDays, setNetworkDays] = useState(''); + const [networkTrees, setNetworkTrees] = useState(''); + + // 考核规则设置 + const [localUserThreshold, setLocalUserThreshold] = useState('5'); + const [exemptionEnabled, setExemptionEnabled] = useState(false); + + // 前端展示设置 + const [allowNonAdopterView, setAllowNonAdopterView] = useState(false); + const [heatDisplayMode, setHeatDisplayMode] = useState<'count' | 'level'>('count'); + + // 后台账号与安全 + const [approvalCount, setApprovalCount] = useState('3'); + const [sensitiveOperations, setSensitiveOperations] = useState(['修改结算参数', '删除用户']); + const [logDate, setLogDate] = useState(''); + const [logSearch, setLogSearch] = useState(''); + + // 切换货币选择 + const toggleCurrency = (currency: string) => { + if (settlementCurrencies.includes(currency)) { + setSettlementCurrencies(settlementCurrencies.filter(c => c !== currency)); + } else { + setSettlementCurrencies([...settlementCurrencies, currency]); + } + }; + + // Toggle 组件 + const Toggle = ({ checked, onChange, size = 'normal' }: { checked: boolean; onChange: (v: boolean) => void; size?: 'normal' | 'small' }) => { + if (size === 'small') { + return ( +
onChange(!checked)} + > +
+
+ ); + } + return ( +
onChange(!checked)} + > +
+
+ ); + }; + + return ( + +
+ {/* 页面头部 */} +
+

系统设置

+

配置结算参数、龙虎榜规则、认种限额、考核规则及后台安全策略

+
+ 所有设置修改后请点击下方"保存设置"按钮生效 +
+
+ + {/* 结算参数设置 */} +
+
+

结算参数设置

+ +
+
+
+ +
+ {['BNB', 'OG', 'USDT', 'DST'].map(currency => ( + + ))} +
+
+
+ + + 提示:默认结算币种将用于用户点击"一键结算"时的自动兑换。 +
+
+
+ + {/* 龙虎榜设置 */} +
+
+

龙虎榜设置

+ +
+
+
+ 启用虚拟账户参与排名 + +
+
+ 虚拟账户数量 + setVirtualAccountCount(e.target.value)} + /> + +
+
+ +
+
+ 日榜开关 + +
+
+ 周榜开关 + +
+
+ 月榜开关 + +
+
+
+
+ + setDisplayCount(e.target.value)} + /> + 仅显示前 N 名的排行榜数据 +
+
+ 提示:虚拟账户仅用于展示效果,不影响真实收益结算。 +
+
+
+ + {/* 认种限额设置 */} +
+
+

认种限额设置

+ +
+
+ {/* 单账户认种限额 */} +
+
+ 单账户认种限额 + +
+
+ + setSingleAccountDays(e.target.value)} + /> + 天内,每个账户最多认种 + setSingleAccountTrees(e.target.value)} + /> + +
+ 示例:5 天内最多认种 1 棵 +
+ + {/* 全网总量限额 */} +
+
+ 全网总量限额 + +
+
+ + setNetworkDays(e.target.value)} + /> + 天内,全网总认种上限 + setNetworkTrees(e.target.value)} + /> + +
+ 示例:2 天内总量 100 棵 +
+
+
+ + {/* 考核规则设置 */} +
+
+

考核规则设置

+ +
+
+ {/* 本地用户占比阈值 */} +
+ +
+ setLocalUserThreshold(e.target.value)} + /> + % +
+ 当团队中本省/市用户占比低于此值时,将触发相应考核机制。 +
+ + {/* 豁免设置 */} +
+
+ 允许为单个账号设置"不考核自有团队本省/市占比"豁免 + 开启后,可在用户管理页面为特定账号设置豁免权。 +
+ +
+ + {/* 阶梯性考核目标表 */} +
+
+

省代 / 市代阶梯性考核目标表

+
+ + +
+
+
+
+
考核月
+
省代当月目标
+
省代累计目标
+
市代当月目标
+
市代累计目标
+
+
+ {mockTargets.map((target) => ( +
+
第{target.month}个月
+
{target.provinceMonthly}
+
{target.provinceCumulative}
+
{target.cityMonthly}
+
{target.cityCumulative}
+
+ ))} +
+
+ 此表格用于设定不同代理级别的月度和累计业绩目标。 +
+
+
+ + {/* 前端展示设置 */} +
+
+

前端展示设置

+ +
+
+
+ 允许未认种用户查看各省认种热度 + +
+
+ +
+ + +
+
+ 提示:仅影响前端展示,不影响任何收益计算及排名。 +
+
+ + {/* 后台账号与安全 */} +
+
+

后台账号与安全

+ +
+
+ {/* 左侧:账号管理 */} +
+
+

后台账号与角色

+ +
+
+
+
+
账号名
+
角色
+
状态
+
最近登录
+
操作
+
+
+ {mockAccounts.map((account) => ( +
+
{account.name}
+
{account.role}
+
+ + {account.status === 'active' ? '正常' : '禁用'} + +
+
{account.lastLogin}
+
+ + +
+
+ ))} +
+
+
+ 管理后台操作员账号及其权限角色。 +
+ + {/* 右侧:敏感操作和日志 */} +
+ {/* 敏感操作审批 */} +
+
+ 敏感操作审批人数 + setApprovalCount(e.target.value)} + /> + +
+
+ + +
+ {sensitiveOperations.map((op, index) => ( + + {op} + + + ))} +
+
+
+ + {/* 操作日志 */} +
+

操作日志

+
+ setLogDate(e.target.value)} + /> + setLogSearch(e.target.value)} + /> + +
+
+
+
+
时间
+
操作账号
+
类型
+
描述
+
审批结果
+
+
+ {mockLogs.map((log) => ( +
+
{log.time}
+
{log.account}
+
{log.type}
+
{log.description}
+
+ + {log.result === 'pass' ? '通过' : log.result === 'reject' ? '拒绝' : '待审'} + +
+
+ ))} +
+
+
+
+
+
+
+ + {/* 页面底部操作按钮 */} +
+ + +
+
+
+ ); +} diff --git a/frontend/admin-web/src/app/(dashboard)/settings/settings.module.scss b/frontend/admin-web/src/app/(dashboard)/settings/settings.module.scss new file mode 100644 index 00000000..522ff9b1 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/settings/settings.module.scss @@ -0,0 +1,1180 @@ +/* 系统设置页面样式 - 基于 UIPro Figma 设计 */ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.settings { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; + font-family: 'Noto Sans SC', $font-family-base; +} + +/* 页面头部 */ +.settings__header { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + max-width: 960px; +} + +.settings__title { + margin: 0; + font-size: 30px; + line-height: 36px; + font-weight: 700; + color: #0f172a; +} + +.settings__desc { + font-size: 16px; + line-height: 24px; + color: #6b7280; + padding-bottom: 12px; +} + +.settings__notice { + align-self: stretch; + border-radius: 8px; + background-color: #f8f8f8; + border: 1px solid #bfdbfe; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 12px; + font-size: 14px; + line-height: 20px; + color: #1d4ed8; +} + +/* 设置区块 */ +.settings__section { + width: 100%; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 8px; + background-color: #fff; + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 24px; + gap: 16px; + max-width: 960px; +} + +.settings__sectionHeader { + align-self: stretch; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; +} + +.settings__sectionTitle { + margin: 0; + font-size: 18px; + line-height: 28px; + font-weight: 700; + color: #0f172a; +} + +.settings__resetBtn { + cursor: pointer; + background: none; + border: none; + font-size: 14px; + line-height: 20px; + color: #0061a8; + padding: 0; + + &:hover { + text-decoration: underline; + } +} + +/* 设置内容区域 */ +.settings__content { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; +} + +/* 表单字段组 */ +.settings__fieldGroup { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; +} + +.settings__label { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #0f172a; +} + +/* 复选框组 */ +.settings__checkboxGroup { + align-self: stretch; + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 24px; + font-size: 16px; +} + +.settings__checkboxItem { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.settings__checkbox { + cursor: pointer; + margin: 0; + height: 16px; + width: 16px; + border-radius: 8px; + background-color: #fff; + border: 1px solid #6b7280; + appearance: none; + position: relative; + + &:checked { + background-color: #0061a8; + border-color: #0061a8; + + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #fff; + } + } +} + +.settings__checkboxLabel { + font-size: 16px; + line-height: 24px; + color: #0f172a; +} + +/* 下拉选择器 */ +.settings__selectWrapper { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; +} + +.settings__select { + width: 100%; + max-width: 320px; + border-radius: 6px; + background-color: #f3f4f6; + border: 1px solid #e5e7eb; + padding: 9px 12px; + font-size: 16px; + line-height: 24px; + color: #1e293b; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 12px center; + background-repeat: no-repeat; + background-size: 16px; + padding-right: 40px; + + &:focus { + outline: none; + border-color: #0061a8; + } +} + +.settings__hint { + font-size: 12px; + line-height: 16px; + color: #6b7280; +} + +/* 开关行 */ +.settings__toggleRow { + align-self: stretch; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.settings__toggleLabel { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #0f172a; +} + +/* Toggle 开关 */ +.settings__toggle { + position: relative; + width: 44px; + height: 24px; + border-radius: 9999px; + cursor: pointer; + @include transition-fast; + flex-shrink: 0; + + &--on { + background-color: #0061a8; + } + + &--off { + background-color: #e5e7eb; + } +} + +.settings__toggleHandle { + position: absolute; + top: 2px; + width: 20px; + height: 20px; + border-radius: 9999px; + background-color: #fff; + border: 1px solid #cbd5e1; + @include transition-fast; + + &--on { + left: 22px; + border-color: #fff; + } + + &--off { + left: 2px; + } +} + +/* 小型开关 */ +.settings__toggleSmall { + position: relative; + width: 36px; + height: 20px; + border-radius: 9999px; + cursor: pointer; + @include transition-fast; + flex-shrink: 0; + + &--on { + background-color: #0061a8; + } + + &--off { + background-color: #e5e7eb; + } +} + +.settings__toggleSmallHandle { + position: absolute; + top: 2px; + width: 16px; + height: 16px; + border-radius: 9999px; + background-color: #fff; + border: 1px solid #fff; + @include transition-fast; + + &--on { + left: 18px; + } + + &--off { + left: 2px; + border-color: #cbd5e1; + } +} + +/* 输入框行 */ +.settings__inputRow { + align-self: stretch; + display: flex; + align-items: center; + gap: 8px; +} + +.settings__input { + flex: 1; + border-radius: 6px; + background-color: #f3f4f6; + border: 1px solid #e5e7eb; + padding: 8px 12px; + font-size: 16px; + line-height: 24px; + color: #1e293b; + font-family: 'Noto Sans SC', $font-family-base; + + &::placeholder { + color: #6b7280; + } + + &:focus { + outline: none; + border-color: #0061a8; + } + + &--small { + width: 80px; + flex: none; + text-align: right; + } + + &--medium { + max-width: 320px; + } +} + +.settings__inputUnit { + font-size: 14px; + line-height: 20px; + color: #6b7280; +} + +/* 榜单开关组 */ +.settings__switchGroup { + align-self: stretch; + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 24px; +} + +.settings__switchItem { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.settings__switchLabel { + font-size: 14px; + line-height: 20px; + color: #0f172a; +} + +/* 认种限额边框卡片 */ +.settings__quotaCard { + align-self: stretch; + border-radius: 6px; + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 16px; + gap: 8px; +} + +.settings__quotaHeader { + align-self: stretch; + display: flex; + align-items: flex-start; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; +} + +.settings__quotaTitle { + font-size: 16px; + line-height: 24px; + font-weight: 500; + color: #0f172a; +} + +.settings__quotaInputRow { + align-self: stretch; + display: flex; + align-items: center; + flex-wrap: wrap; + padding-top: 4px; + gap: 8px; + font-size: 14px; + line-height: 20px; + color: #0f172a; +} + +.settings__quotaText { + flex-shrink: 0; +} + +/* 考核设置 */ +.settings__assessmentRow { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; +} + +.settings__assessmentLabel { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #0f172a; +} + +.settings__assessmentInputRow { + align-self: stretch; + display: flex; + align-items: center; + padding-top: 4px; + gap: 8px; + font-size: 16px; +} + +.settings__assessmentUnit { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #0f172a; +} + +/* 豁免设置行 */ +.settings__exemptionRow { + align-self: stretch; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 16px; +} + +.settings__exemptionInfo { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + min-width: 310px; + max-width: 576px; +} + +.settings__exemptionLabel { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #0f172a; +} + +.settings__exemptionHint { + font-size: 12px; + line-height: 16px; + color: #6b7280; +} + +/* 考核目标表格区域 */ +.settings__tableSection { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; +} + +.settings__tableHeader { + align-self: stretch; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; +} + +.settings__tableTitle { + font-size: 16px; + line-height: 24px; + font-weight: 500; + color: #0f172a; + margin: 0; +} + +.settings__tableBtns { + display: flex; + align-items: flex-end; + gap: 8px; +} + +.settings__tableBtn { + cursor: pointer; + border-radius: 6px; + background-color: #f3f4f6; + border: 1px solid #e5e7eb; + padding: 6px 12px; + font-size: 14px; + line-height: 20px; + color: #0f172a; + @include transition-fast; + + &:hover { + background-color: #e5e7eb; + } +} + +/* 表格样式 */ +.settings__table { + align-self: stretch; + overflow-x: auto; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.settings__tableHead { + align-self: stretch; + background-color: #f8fafc; + display: flex; + align-items: flex-start; + min-width: 490px; +} + +.settings__tableHeadCell { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 12px; + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #64748b; + + &--month { + flex: 0.8; + min-width: 82px; + } + + &--target { + flex: 1; + min-width: 108px; + } +} + +.settings__tableBody { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + min-width: 490px; +} + +.settings__tableRow { + align-self: stretch; + display: flex; + align-items: flex-start; + border-top: 1px solid #e5e7eb; + + &:first-child { + border-top: none; + } +} + +.settings__tableCell { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 12px; + font-size: 14px; + line-height: 20px; + color: #0f172a; + + &--month { + flex: 0.8; + min-width: 82px; + } + + &--target { + flex: 1; + min-width: 108px; + } +} + +/* 前端展示设置 */ +.settings__radioGroup { + align-self: stretch; + display: flex; + align-items: center; + gap: 24px; + font-size: 16px; +} + +.settings__radioItem { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.settings__radio { + height: 16px; + width: 16px; + border-radius: 16px; + background-color: #fff; + border: 1px solid #6b7280; + appearance: none; + cursor: pointer; + position: relative; + + &:checked { + border-color: #0061a8; + + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #0061a8; + } + } +} + +.settings__radioLabel { + font-size: 16px; + line-height: 24px; + color: #0f172a; +} + +/* 后台账号与安全 */ +.settings__securityContent { + align-self: stretch; + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 32px; +} + +.settings__securityLeft { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; + min-width: 310px; +} + +.settings__securityRight { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; + min-width: 310px; +} + +/* 账号表格区域 */ +.settings__accountHeader { + align-self: stretch; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; +} + +.settings__accountTitle { + font-size: 16px; + line-height: 24px; + font-weight: 500; + color: #0f172a; + margin: 0; +} + +.settings__addBtn { + cursor: pointer; + border-radius: 6px; + background-color: #0061a8; + border: none; + padding: 6px 12px; + font-size: 14px; + line-height: 20px; + color: #fff; + @include transition-fast; + + &:hover { + background-color: #005090; + } +} + +/* 账号表格 */ +.settings__accountTable { + align-self: stretch; + border-radius: 6px; + border: 1px solid #e5e7eb; + overflow: auto; +} + +.settings__accountTableInner { + min-width: 400px; + display: flex; + flex-direction: column; +} + +.settings__accountHead { + background-color: #f8fafc; + display: flex; + align-items: center; +} + +.settings__accountHeadCell { + padding: 12px; + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #64748b; + + &--name { + flex: 1; + min-width: 95px; + } + + &--role { + width: 80px; + } + + &--status { + width: 60px; + } + + &--login { + width: 90px; + } + + &--actions { + width: 86px; + text-align: center; + } +} + +.settings__accountBody { + display: flex; + flex-direction: column; +} + +.settings__accountRow { + display: flex; + align-items: center; + border-top: 1px solid #e5e7eb; + + &:first-child { + border-top: none; + } +} + +.settings__accountCell { + padding: 12px; + font-size: 14px; + line-height: 20px; + color: #0f172a; + + &--name { + flex: 1; + min-width: 95px; + } + + &--role { + width: 80px; + } + + &--status { + width: 60px; + } + + &--login { + width: 90px; + } + + &--actions { + width: 86px; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + } +} + +.settings__statusBadge { + display: inline-flex; + border-radius: 9999px; + background-color: #dcfce7; + padding: 2px 8px; + font-size: 12px; + line-height: 16px; + font-weight: 500; + color: #166534; + + &--disabled { + background-color: #fee2e2; + color: #dc2626; + } +} + +.settings__actionLink { + cursor: pointer; + background: none; + border: none; + padding: 0; + font-size: 12px; + line-height: 16px; + color: #0061a8; + + &:hover { + text-decoration: underline; + } + + &--danger { + color: #ef4444; + } +} + +/* 敏感操作区域 */ +.settings__sensitiveCard { + align-self: stretch; + border-radius: 6px; + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 16px; + gap: 16px; +} + +.settings__sensitiveRow { + align-self: stretch; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.settings__sensitiveLabel { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #0f172a; +} + +.settings__sensitiveFieldGroup { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; +} + +.settings__tagInput { + align-self: stretch; + border-radius: 6px; + background-color: #f3f4f6; + border: 1px solid #e5e7eb; + padding: 8px 12px; + font-size: 16px; + line-height: 24px; + color: #6b7280; + + &::placeholder { + color: #6b7280; + } + + &:focus { + outline: none; + border-color: #0061a8; + color: #1e293b; + } +} + +.settings__tagList { + align-self: stretch; + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 8px; +} + +.settings__tag { + border-radius: 9999px; + background-color: #dcfce7; + padding: 2px 10px; + font-size: 12px; + line-height: 16px; + font-weight: 500; + color: #1e40af; + display: flex; + align-items: center; + gap: 4px; +} + +.settings__tagRemove { + cursor: pointer; + background: none; + border: none; + padding: 0; + font-size: 14px; + line-height: 1; + color: #1e40af; + + &:hover { + color: #1e3a8a; + } +} + +/* 操作日志区域 */ +.settings__logSection { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; +} + +.settings__logTitle { + font-size: 16px; + line-height: 24px; + font-weight: 500; + color: #0f172a; + margin: 0; +} + +.settings__logFilters { + align-self: stretch; + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 8px; +} + +.settings__dateInput { + flex: 1; + min-width: 136px; + max-width: 160px; + border-radius: 6px; + background-color: #f3f4f6; + border: 1px solid #e5e7eb; + padding: 6px 12px; + font-size: 14px; + line-height: 20px; + color: #0f172a; + + &:focus { + outline: none; + border-color: #0061a8; + } +} + +.settings__searchInput { + flex: 1; + min-width: 143px; + border-radius: 6px; + background-color: #f3f4f6; + border: 1px solid #e5e7eb; + padding: 6px 12px; + font-size: 14px; + line-height: 20px; + color: #0f172a; + + &::placeholder { + color: #6b7280; + } + + &:focus { + outline: none; + border-color: #0061a8; + } +} + +.settings__exportBtn { + cursor: pointer; + border-radius: 6px; + background-color: #f3f4f6; + border: 1px solid #e5e7eb; + padding: 6px 12px; + font-size: 14px; + line-height: 20px; + color: #0f172a; + @include transition-fast; + + &:hover { + background-color: #e5e7eb; + } +} + +/* 日志表格 */ +.settings__logTable { + align-self: stretch; + border-radius: 6px; + border: 1px solid #e5e7eb; + overflow: auto; +} + +.settings__logTableInner { + min-width: 300px; + display: flex; + flex-direction: column; +} + +.settings__logHead { + background-color: #f8fafc; + display: flex; + align-items: center; +} + +.settings__logHeadCell { + padding: 12px; + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #64748b; + + &--time { + width: 70px; + } + + &--account { + width: 70px; + } + + &--type { + width: 60px; + } + + &--desc { + flex: 1; + min-width: 80px; + } + + &--result { + width: 60px; + } +} + +.settings__logBody { + display: flex; + flex-direction: column; +} + +.settings__logRow { + display: flex; + align-items: center; + border-top: 1px solid #e5e7eb; + + &:first-child { + border-top: none; + } +} + +.settings__logCell { + padding: 12px; + font-size: 14px; + line-height: 20px; + color: #0f172a; + + &--time { + width: 70px; + } + + &--account { + width: 70px; + } + + &--type { + width: 60px; + } + + &--desc { + flex: 1; + min-width: 80px; + } + + &--result { + width: 60px; + } +} + +.settings__logResult { + &--pass { + color: #16a34a; + } + + &--reject { + color: #ef4444; + } + + &--pending { + color: #f59e0b; + } +} + +/* 页面底部操作区 */ +.settings__footer { + width: 100%; + border-top: 1px solid #e5e7eb; + display: flex; + align-items: center; + justify-content: flex-end; + padding-top: 16px; + gap: 16px; + max-width: 960px; +} + +.settings__cancelBtn { + cursor: pointer; + border-radius: 6px; + background-color: #fff; + border: 1px solid #e5e7eb; + padding: 8px 24px; + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #0f172a; + @include transition-fast; + + &:hover { + background-color: #f9fafb; + } +} + +.settings__saveBtn { + cursor: pointer; + border-radius: 6px; + background-color: #0061a8; + border: none; + padding: 8px 24px; + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #fff; + @include transition-fast; + + &:hover { + background-color: #005090; + } + + &:disabled { + background-color: #94a3b8; + cursor: not-allowed; + } +} diff --git a/frontend/admin-web/src/app/(dashboard)/statistics/page.tsx b/frontend/admin-web/src/app/(dashboard)/statistics/page.tsx new file mode 100644 index 00000000..e6d9efee --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/statistics/page.tsx @@ -0,0 +1,359 @@ +'use client'; + +import { useState } from 'react'; +import { PageContainer } from '@/components/layout'; +import { cn } from '@/utils/helpers'; +import styles from './statistics.module.scss'; + +/** + * 排名数据接口 + */ +interface RankingItem { + rank: number; + account: string; + count: number; + province: string; + city: string; +} + +/** + * 区域数据接口 + */ +interface RegionItem { + region: string; + period: string; + count: number; + ratio: string; +} + +/** + * 运营数据接口 + */ +interface OperationItem { + type: string; + name: string; + month: string; + hashrate: string; + mining: string; + commission: string; +} + +/** + * 收益数据接口 + */ +interface RevenueItem { + time: string; + account: string; + source: string; + amount: string; + address: string; + txId: string; +} + +// 排名数据 +const rankingData: RankingItem[] = [ + { rank: 1, account: 'user_001', count: 58, province: '广东', city: '深圳' }, + { rank: 2, account: 'user_002', count: 55, province: '广东', city: '广州' }, + { rank: 3, account: 'user_003', count: 49, province: '湖南', city: '长沙' }, + { rank: 4, account: 'user_004', count: 42, province: '浙江', city: '杭州' }, + { rank: 5, account: 'user_005', count: 38, province: '江苏', city: '南京' }, +]; + +// 区域数据 +const regionData: RegionItem[] = [ + { region: '广东省', period: '2023-10', count: 2345, ratio: '25.8%' }, + { region: '湖南省', period: '2023-10', count: 1890, ratio: '20.8%' }, + { region: '浙江省', period: '2023-10', count: 1560, ratio: '17.1%' }, + { region: '江苏省', period: '2023-10', count: 1230, ratio: '13.5%' }, +]; + +// 运营数据 +const operationData: OperationItem[] = [ + { type: '省公司', name: '广东省公司', month: '2023-10', hashrate: '150 TH/s', mining: '0.8 BTC', commission: '¥2,500' }, + { type: '市公司', name: '深圳市公司', month: '2023-10', hashrate: '80 TH/s', mining: '0.45 BTC', commission: '¥1,800' }, + { type: '市公司', name: '广州市公司', month: '2023-10', hashrate: '70 TH/s', mining: '0.35 BTC', commission: '¥1,500' }, +]; + +// 收益数据 +const revenueData: RevenueItem[] = [ + { time: '2023-10-26 10:30', account: 'user_001', source: '挖矿收益', amount: '+ ¥50.00', address: 'bc1...xyz', txId: 'TXN1234567890' }, + { time: '2023-10-26 09:15', account: 'user_002', source: '认种提成', amount: '+ ¥120.00', address: 'bc1...abc', txId: 'TXN0987654321' }, + { time: '2023-10-25 18:00', account: 'user_003', source: '分润', amount: '+ ¥35.50', address: 'bc1...def', txId: 'TXN5432109876' }, +]; + +// 运营指标数据 +const metricsData = [ + { label: '每月算力', value: '1,200\nTH/s' }, + { label: '累计算力', value: '15,000\nTH/s' }, + { label: '每月挖矿量', value: '5.6 BTC' }, + { label: '累计挖矿量', value: '67.8 BTC' }, + { label: '每月佣金', value: '¥12,345' }, + { label: '累计佣金', value: '¥150,000' }, + { label: '每月累计认种\n提成', value: '¥8,900' }, +]; + +/** + * 数据统计页面 + * 基于 UIPro Figma 设计实现 + */ +export default function StatisticsPage() { + // 趋势图时间维度 + const [trendPeriod, setTrendPeriod] = useState<'day' | 'week' | 'month' | 'quarter' | 'year'>('day'); + // 龙虎榜时间维度 + const [rankingTab, setRankingTab] = useState<'daily' | 'weekly' | 'monthly'>('daily'); + // 区域统计维度 + const [regionType, setRegionType] = useState<'province' | 'city'>('province'); + + return ( + +
+ {/* 页面标题 */} +

数据统计

+ + {/* 统计概览卡片 */} +
+
+
榴莲树认种总量
+

12,345

+
+
+
今日认种数量
+

123

+
+
+
本月认种数量
+

1,456

+
+
+ + {/* 榴莲树认种数量趋势 */} +
+
+ 榴莲树认种数量趋势 +
+ {(['day', 'week', 'month', 'quarter', 'year'] as const).map((period) => ( + + ))} +
+
+
Chart Placeholder
+
+ + {/* 龙虎榜与排名统计 */} +
+

龙虎榜与排名统计

+
+ + + +
+
+ {/* 排名表格 */} +
+
+
+
排名
+
账户
+
认种数量
+
所属省
+
所属市
+
+ {rankingData.map((item) => ( +
+
{item.rank}
+
{item.account}
+
{item.count}
+
{item.province}
+
{item.city}
+
+ ))} +
+
+ + {/* 授权公司第一名 */} +
+
+ 授权省公司第一名 +

广东省公司

+
完成数据: 2,345
+
+
+ 授权市公司第一名 +

深圳市公司

+
完成数据: 1,120
+
+
+
+
+ + {/* 区域认种数据统计 */} +
+
+

区域认种数据统计

+
+ + +
+
+
+
+ 各省认购数量 +
Bar Chart Placeholder
+
+
+
+
+
区域
+
时间周期
+
认购数量
+
占比
+
+ {regionData.map((item, index) => ( +
+
{item.region}
+
{item.period}
+
{item.count.toLocaleString()}
+
{item.ratio}
+
+ ))} +
+
+
+
+ +
+
+ + {/* 省/市公司运营统计 */} +
+
+

省 / 市公司运营统计

+ +
+
+ {metricsData.map((metric) => ( +
+
{metric.label}
+ {metric.value} +
+ ))} +
+
+
+
账户类型(省公司 / 市公司)
+
账户名称
+
统计月份
+
算力
+
挖矿量
+
佣金
+
+ {operationData.map((item, index) => ( +
+
{item.type}
+
{item.name}
+
{item.month}
+
{item.hashrate}
+
{item.mining}
+
{item.commission}
+
+ ))} +
+
+ + {/* 收益明细与来源 */} +
+

收益明细与来源

+
+
+ + +
+
+ +
+
+ + + + +
+ +
+
+
+
+
+
时间
+
账户
+
收益来源
+
收益金额
+
相关地址
+
交易流水号
+
+ {revenueData.map((item, index) => ( +
+
{item.time}
+
{item.account}
+
{item.source}
+
{item.amount}
+
{item.address}
+
{item.txId}
+
+ ))} +
+
+ +
+
+
+
+ ); +} diff --git a/frontend/admin-web/src/app/(dashboard)/statistics/statistics.module.scss b/frontend/admin-web/src/app/(dashboard)/statistics/statistics.module.scss new file mode 100644 index 00000000..b43fe52b --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/statistics/statistics.module.scss @@ -0,0 +1,764 @@ +/* 数据统计页面样式 - 基于 UIPro Figma 设计 */ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.statistics { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 32px; + text-align: left; + font-family: 'Noto Sans SC', $font-family-base; +} + +/* 页面标题 */ +.statistics__title { + margin: 0; + font-size: 24px; + line-height: 32px; + font-weight: 700; + color: #0f172a; +} + +/* 统计概览卡片组 */ +.statistics__overview { + align-self: stretch; + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 24px; +} + +.statistics__overviewCard { + flex: 1; + min-width: 144px; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 1px 2px -1px rgba(0, 0, 0, 0.1); + border-radius: 8px; + background-color: #fff; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 24px; + gap: 7px; +} + +.statistics__overviewLabel { + align-self: stretch; + font-size: 16px; + line-height: 24px; + color: #6b7280; +} + +.statistics__overviewValue { + align-self: stretch; + margin: 0; + font-size: 30px; + line-height: 36px; + font-weight: 700; + color: #0f172a; +} + +/* 通用卡片样式 */ +.statistics__card { + align-self: stretch; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 1px 2px -1px rgba(0, 0, 0, 0.1); + border-radius: 8px; + background-color: #fff; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 24px; + gap: 16px; +} + +.statistics__cardHeader { + align-self: stretch; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; +} + +.statistics__cardTitle { + margin: 0; + font-size: 18px; + line-height: 28px; + font-weight: 700; + color: #0f172a; +} + +/* 时间周期切换按钮组 */ +.statistics__periodTabs { + border-radius: 8px; + background-color: #f8f8f8; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + gap: 4px; +} + +.statistics__periodTab { + cursor: pointer; + border: none; + padding: 4px 12px; + background-color: transparent; + border-radius: 6px; + font-size: 14px; + line-height: 20px; + font-family: inherit; + color: #4b5563; + @include transition-fast; + + &:hover { + background-color: rgba(255, 255, 255, 0.5); + } + + &--active { + background-color: #fff; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 1px 2px -1px rgba(0, 0, 0, 0.1); + font-weight: 700; + color: #475569; + } +} + +/* 图表占位区域 */ +.statistics__chartArea { + align-self: stretch; + height: 320px; + border-radius: 8px; + background-color: #f3f4f6; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + color: #94a3b8; +} + +/* 龙虎榜与排名统计 */ +.statistics__sectionTitle { + margin: 0; + font-size: 24px; + line-height: 32px; + font-weight: 700; + color: #0f172a; +} + +/* Tab 导航 */ +.statistics__tabs { + align-self: stretch; + border-bottom: 1px solid #e5e7eb; + display: flex; + align-items: flex-start; + gap: 4px; + padding-top: 8px; +} + +.statistics__tab { + cursor: pointer; + border: none; + padding: 8px 16px; + background-color: transparent; + font-size: 16px; + line-height: 24px; + font-family: inherit; + color: #6b7280; + @include transition-fast; + + &:hover { + color: #475569; + } + + &--active { + border-bottom: 2px solid #475569; + font-weight: 700; + color: #475569; + } +} + +/* 排名表格区域 */ +.statistics__rankingSection { + align-self: stretch; + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 24px; +} + +.statistics__rankingTable { + flex: 1; + min-width: 310px; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 1px 2px -1px rgba(0, 0, 0, 0.1); + border-radius: 8px; + background-color: #fff; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +/* 表格通用样式 */ +.statistics__table { + align-self: stretch; + overflow-x: auto; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.statistics__tableHeader { + align-self: stretch; + background-color: #f8fafc; + display: flex; + align-items: center; + min-width: 400px; +} + +.statistics__tableRow { + align-self: stretch; + background-color: #fff; + display: flex; + align-items: center; + min-width: 400px; + + &--bordered { + border-bottom: 1px solid #e5e7eb; + } +} + +.statistics__tableCell { + display: flex; + align-items: center; + padding: 12px 24px; + font-size: 14px; + line-height: 20px; + color: #6b7280; + + &--header { + font-size: 12px; + font-weight: 700; + color: #334155; + text-transform: uppercase; + } + + &--rank { + flex: 0.5; + min-width: 72px; + } + + &--account { + flex: 0.7; + min-width: 117px; + font-weight: 500; + color: #0f172a; + } + + &--count { + flex: 0.65; + min-width: 96px; + } + + &--province { + flex: 0.65; + min-width: 84px; + } + + &--city { + flex: 1; + min-width: 36px; + } +} + +/* 排名高亮卡片 */ +.statistics__highlightCards { + flex: 1; + min-width: 175px; + max-width: 304px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; +} + +.statistics__highlightCard { + align-self: stretch; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 1px 2px -1px rgba(0, 0, 0, 0.1); + border-radius: 8px; + background-color: #fff; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 24px; +} + +.statistics__highlightTitle { + font-size: 16px; + line-height: 24px; + font-weight: 700; + color: #4b5563; + padding: 12px 0; +} + +.statistics__highlightName { + margin: 0; + font-size: 20px; + line-height: 28px; + font-weight: 700; + color: #475569; +} + +.statistics__highlightData { + font-size: 14px; + line-height: 20px; + color: #6b7280; +} + +/* 区域统计卡片 */ +.statistics__regionCard { + align-self: stretch; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 1px 2px -1px rgba(0, 0, 0, 0.1); + border-radius: 8px; + background-color: #fff; + overflow-y: auto; + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: center; + padding: 24px; + gap: 16px; +} + +.statistics__regionHeader { + align-self: stretch; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; +} + +.statistics__regionTitle { + margin: 0; + font-size: 20px; + line-height: 28px; + font-weight: 700; + color: #0f172a; +} + +/* 区域统计切换按钮 */ +.statistics__regionTabs { + flex: 1; + max-width: 200px; + border-radius: 8px; + background-color: #f8f8f8; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + gap: 4px; +} + +.statistics__regionTab { + flex: 1; + cursor: pointer; + border: none; + padding: 6px 16px; + background-color: transparent; + border-radius: 6px; + font-size: 14px; + line-height: 20px; + font-family: inherit; + color: #4b5563; + text-align: center; + @include transition-fast; + + &--active { + background-color: #fff; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 1px 2px -1px rgba(0, 0, 0, 0.1); + font-weight: 700; + color: #475569; + } +} + +/* 区域统计内容 */ +.statistics__regionContent { + align-self: stretch; + display: flex; + align-items: flex-start; + gap: 24px; + + @media (max-width: 768px) { + flex-direction: column; + } +} + +.statistics__regionChart { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 11px; +} + +.statistics__regionChartTitle { + font-size: 16px; + line-height: 24px; + font-weight: 700; + color: #334155; +} + +.statistics__regionChartArea { + align-self: stretch; + border-radius: 8px; + background-color: #f3f4f6; + display: flex; + align-items: center; + justify-content: center; + padding: 148px 0; + font-size: 16px; + color: #94a3b8; +} + +.statistics__regionTableWrapper { + flex: 1; + overflow: auto; + display: flex; + flex-direction: column; + align-items: center; + padding: 36px 0; +} + +/* 区域表格单元格 */ +.statistics__regionCell { + &--region { + flex: 0.55; + min-width: 98px; + font-weight: 500; + color: #0f172a; + } + + &--period { + flex: 0.6; + min-width: 107px; + } + + &--count { + flex: 0.65; + min-width: 96px; + } + + &--ratio { + flex: 1; + min-width: 40px; + } +} + +/* 社区明细链接 */ +.statistics__viewDetail { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-end; + text-align: right; +} + +.statistics__viewDetailLink { + cursor: pointer; + border: none; + padding: 0; + background-color: transparent; + font-size: 14px; + line-height: 20px; + font-family: inherit; + color: #475569; + @include transition-fast; + + &:hover { + text-decoration: underline; + } +} + +/* 省/市公司运营统计 */ +.statistics__operationCard { + align-self: stretch; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 1px 2px -1px rgba(0, 0, 0, 0.1); + border-radius: 8px; + background-color: #fff; + overflow-y: auto; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 24px; + gap: 24px; +} + +.statistics__operationHeader { + align-self: stretch; + display: flex; + align-items: flex-start; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; +} + +.statistics__operationTitle { + margin: 0; + flex: 1; + font-size: 20px; + line-height: 28px; + font-weight: 700; + color: #0f172a; +} + +.statistics__exportBtn { + cursor: pointer; + border: none; + border-radius: 8px; + background-color: rgba(12, 102, 164, 0.1); + display: flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + gap: 8px; + font-size: 14px; + line-height: 20px; + font-family: inherit; + color: #475569; + @include transition-fast; + + &:hover { + background-color: rgba(12, 102, 164, 0.15); + } + + svg { + width: 16px; + height: 24px; + } +} + +/* 运营指标卡片组 */ +.statistics__metricsGrid { + align-self: stretch; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 16px; +} + +.statistics__metricCard { + flex: 1; + min-width: 116px; + border-radius: 8px; + background-color: #f8f8f8; + display: flex; + flex-direction: column; + align-items: center; + padding: 16px; + text-align: center; +} + +.statistics__metricLabel { + font-size: 14px; + line-height: 20px; + color: #6b7280; +} + +.statistics__metricValue { + font-size: 18px; + line-height: 28px; + font-weight: 700; + color: #0f172a; +} + +/* 运营表格单元格 */ +.statistics__operationCell { + &--type { + flex: 1; + min-width: 203px; + } + + &--name { + flex: 1; + min-width: 126px; + font-weight: 500; + color: #0f172a; + } + + &--month { + flex: 1; + min-width: 107px; + } + + &--hashrate { + flex: 1; + min-width: 105px; + } + + &--mining { + flex: 1; + min-width: 104px; + } + + &--commission { + flex: 1; + min-width: 91px; + } +} + +/* 收益明细与来源 */ +.statistics__revenueCard { + align-self: stretch; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 1px 2px -1px rgba(0, 0, 0, 0.1); + border-radius: 8px; + background-color: #fff; + overflow-y: auto; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 24px; + gap: 15px; +} + +.statistics__revenueTitle { + margin: 0; + font-size: 20px; + line-height: 28px; + font-weight: 700; + color: #0f172a; +} + +/* 筛选区域 */ +.statistics__filters { + align-self: stretch; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 16px; +} + +.statistics__filterGroup { + flex: 1; + min-width: 250px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; +} + +.statistics__filterLabel { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #334155; +} + +.statistics__filterSelect { + align-self: stretch; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 6px; + background-color: #fff; + border: 1px solid #cbd5e1; + padding: 9px 13px; + font-size: 16px; + line-height: 24px; + color: #1e293b; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 8px center; + background-repeat: no-repeat; + background-size: 16px; +} + +.statistics__filterInputWrapper { + align-self: stretch; + display: flex; + align-items: flex-start; +} + +.statistics__filterIcon { + height: 37px; + width: 36px; + display: flex; + align-items: center; + justify-content: center; + color: #6b7280; + + svg { + width: 20px; + height: 20px; + } +} + +.statistics__filterInput { + flex: 1; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 6px; + background-color: #fff; + border: 1px solid #cbd5e1; + padding: 8px 12px; + font-size: 16px; + color: #1e293b; + outline: none; + font-family: inherit; + + &::placeholder { + color: #6b7280; + } +} + +/* 收益表格单元格 */ +.statistics__revenueCell { + &--time { + flex: 1; + min-width: 165px; + } + + &--account { + flex: 1; + min-width: 117px; + font-weight: 500; + color: #0f172a; + } + + &--source { + flex: 1; + min-width: 104px; + } + + &--amount { + flex: 1; + min-width: 114px; + font-weight: 700; + color: #16a34a; + } + + &--address { + flex: 1; + min-width: 111px; + } + + &--txId { + flex: 1; + min-width: 151px; + } +} + +/* 查看时间轴链接 */ +.statistics__timelineLink { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-end; + text-align: right; +} diff --git a/frontend/admin-web/src/app/(dashboard)/users/page.tsx b/frontend/admin-web/src/app/(dashboard)/users/page.tsx new file mode 100644 index 00000000..ffed46a2 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/users/page.tsx @@ -0,0 +1,584 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import Image from 'next/image'; +import { Modal, toast } from '@/components/common'; +import { PageContainer } from '@/components/layout'; +import { cn } from '@/utils/helpers'; +import { formatNumber, formatRanking } from '@/utils/formatters'; +import styles from './users.module.scss'; + +/** + * 用户数据类型 + */ +interface UserItem { + accountId: string; + avatar: string; + nickname: string; + personalAdoptions: number; + teamAddresses: number; + teamAdoptions: number; + provincialAdoptions: { count: number; percentage: number }; + cityAdoptions: { count: number; percentage: number }; + referrerId: string; + ranking: number | null; + status: 'online' | 'offline' | 'busy'; +} + +/** + * 模拟用户数据 + */ +const mockUsers: UserItem[] = [ + { + accountId: '1001', + avatar: '/images/Data@2x.png', + nickname: '春风', + personalAdoptions: 150, + teamAddresses: 3200, + teamAdoptions: 8500, + provincialAdoptions: { count: 2125, percentage: 25 }, + cityAdoptions: { count: 850, percentage: 10 }, + referrerId: '988', + ranking: 1, + status: 'busy', + }, + { + accountId: '1002', + avatar: '/images/Data1@2x.png', + nickname: '夏雨', + personalAdoptions: 120, + teamAddresses: 2800, + teamAdoptions: 7200, + provincialAdoptions: { count: 1800, percentage: 25 }, + cityAdoptions: { count: 1080, percentage: 15 }, + referrerId: '1001', + ranking: 31, + status: 'online', + }, + { + accountId: '1003', + avatar: '/images/Data1@2x.png', + nickname: '秋叶', + personalAdoptions: 95, + teamAddresses: 1500, + teamAdoptions: 4100, + provincialAdoptions: { count: 1025, percentage: 25 }, + cityAdoptions: { count: 410, percentage: 10 }, + referrerId: '1002', + ranking: null, + status: 'offline', + }, + ...Array.from({ length: 47 }, (_, i) => ({ + accountId: String(1004 + i), + avatar: '/images/Data@2x.png', + nickname: `用户${i + 4}`, + personalAdoptions: Math.floor(Math.random() * 100) + 50, + teamAddresses: Math.floor(Math.random() * 2000) + 500, + teamAdoptions: Math.floor(Math.random() * 5000) + 1000, + provincialAdoptions: { + count: Math.floor(Math.random() * 1000) + 200, + percentage: Math.floor(Math.random() * 30) + 10, + }, + cityAdoptions: { + count: Math.floor(Math.random() * 500) + 100, + percentage: Math.floor(Math.random() * 20) + 5, + }, + referrerId: String(Math.floor(Math.random() * 1000) + 1), + ranking: Math.random() > 0.3 ? Math.floor(Math.random() * 100) + 1 : null, + status: (['online', 'offline', 'busy'] as const)[Math.floor(Math.random() * 3)], + })), +]; + +/** + * 用户管理页面 + * 基于 UIPro Figma 设计实现 + */ +export default function UsersPage() { + const [keyword, setKeyword] = useState(''); + const [showFilters, setShowFilters] = useState(false); + const [selectedRows, setSelectedRows] = useState([]); + const [detailModal, setDetailModal] = useState(null); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 10, + }); + + // 过滤后的数据 + const filteredData = useMemo(() => { + if (!keyword) return mockUsers; + return mockUsers.filter( + (user) => + user.accountId.includes(keyword) || + user.nickname.toLowerCase().includes(keyword.toLowerCase()) + ); + }, [keyword]); + + // 分页数据 + const paginatedData = useMemo(() => { + const start = (pagination.current - 1) * pagination.pageSize; + return filteredData.slice(start, start + pagination.pageSize); + }, [filteredData, pagination]); + + // 总页数 + const totalPages = Math.ceil(filteredData.length / pagination.pageSize); + + // 全选处理 + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedRows(paginatedData.map((user) => user.accountId)); + } else { + setSelectedRows([]); + } + }; + + // 单选处理 + const handleSelectRow = (accountId: string, checked: boolean) => { + if (checked) { + setSelectedRows((prev) => [...prev, accountId]); + } else { + setSelectedRows((prev) => prev.filter((id) => id !== accountId)); + } + }; + + // 导出 Excel + const handleExport = () => { + toast.success('导出功能开发中'); + }; + + // 批量编辑 + const handleBatchEdit = () => { + if (selectedRows.length === 0) { + toast.warning('请先选择用户'); + return; + } + toast.success(`已选择 ${selectedRows.length} 位用户`); + }; + + // 生成分页按钮 + const renderPaginationButtons = () => { + const buttons = []; + const maxVisible = 5; + + // 首页按钮 + buttons.push( + + ); + + // 上一页按钮 + buttons.push( + + ); + + // 页码按钮 + let startPage = Math.max(1, pagination.current - Math.floor(maxVisible / 2)); + const endPage = Math.min(totalPages, startPage + maxVisible - 1); + if (endPage - startPage < maxVisible - 1) { + startPage = Math.max(1, endPage - maxVisible + 1); + } + + if (startPage > 1) { + buttons.push( + + ); + if (startPage > 2) { + buttons.push( + + ... + + ); + } + } + + for (let i = startPage; i <= endPage; i++) { + buttons.push( + + ); + } + + if (endPage < totalPages) { + if (endPage < totalPages - 1) { + buttons.push( + + ... + + ); + } + buttons.push( + + ); + } + + // 下一页按钮 + buttons.push( + + ); + + // 末页按钮 + buttons.push( + + ); + + return buttons; + }; + + return ( + +
+ {/* 页面标题和操作按钮 */} +
+

用户管理

+
+ + +
+
+ + {/* 主内容卡片 */} +
+ {/* 搜索和筛选区域 */} +
+ {/* 搜索框 */} +
+
+ 搜索 +
+ setKeyword(e.target.value)} + /> +
+ + {/* 高级筛选面板 */} +
+
setShowFilters(!showFilters)} + > + 高级筛选 +
+ 展开 +
+
+ {showFilters && ( +
+ + + +
+ )} +
+
+ + {/* 表格区域 */} +
+ {/* 表格头部 */} +
+
+ 0} + onChange={(e) => handleSelectAll(e.target.checked)} + /> +
+
+ 账户序号 +
+
+ 头像 +
+
+ 昵称 +
+
+ 账户认种量 +
+
+ 团队总注册地址量 +
+
+ 团队总认种量 +
+
+ 团队本省认种量及占比 +
+
+ 团队本市认种量及占比 +
+
+ 推荐人序列号 +
+
+ 龙虎榜排名 +
+
+ 操作 +
+
+ + {/* 表格内容 */} +
+ {paginatedData.map((user) => ( +
+ {/* 复选框 */} +
+ handleSelectRow(user.accountId, e.target.checked)} + /> +
+ + {/* 账户序号 */} +
+ {user.accountId} +
+ + {/* 头像 */} +
+
+
+
+
+ + {/* 昵称 */} +
+ {user.nickname} +
+ + {/* 账户认种量 */} +
+ {formatNumber(user.personalAdoptions)} +
+ + {/* 团队总注册地址量 */} +
+ {formatNumber(user.teamAddresses)} +
+ + {/* 团队总认种量 */} +
+ {formatNumber(user.teamAdoptions)} +
+ + {/* 团队本省认种量及占比 */} +
+ {formatNumber(user.provincialAdoptions.count)} + + ({user.provincialAdoptions.percentage}%) + +
+ + {/* 团队本市认种量及占比 */} +
+ {formatNumber(user.cityAdoptions.count)} + + ({user.cityAdoptions.percentage}%) + +
+ + {/* 推荐人序列号 */} +
+ {user.referrerId} +
+ + {/* 龙虎榜排名 */} +
+ {user.ranking ? formatRanking(user.ranking) : '-'} +
+ + {/* 操作 */} +
+ + +
+
+ ))} +
+
+ + {/* 分页区域 */} +
+
+ 每页显示: + + + 共 {filteredData.length} 条记录 + +
+
{renderPaginationButtons()}
+
+
+ + {/* 用户详情弹窗 */} + setDetailModal(null)} + footer={null} + width={600} + > + {detailModal && ( +
+
+
+
+

{detailModal.nickname}

+

账户ID: {detailModal.accountId}

+
+
+
+
+ 个人认种量 + + {formatNumber(detailModal.personalAdoptions)} + +
+
+ 团队认种量 + + {formatNumber(detailModal.teamAdoptions)} + +
+
+ 龙虎榜排名 + + {formatRanking(detailModal.ranking)} + +
+
+
+ )} + +
+ + ); +} diff --git a/frontend/admin-web/src/app/(dashboard)/users/users.module.scss b/frontend/admin-web/src/app/(dashboard)/users/users.module.scss new file mode 100644 index 00000000..e3947667 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/users/users.module.scss @@ -0,0 +1,634 @@ +/* 用户管理页面样式 - 基于 UIPro Figma 设计 */ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.users { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; + text-align: left; + font-family: 'Noto Sans SC', $font-family-base; +} + +/* 页面标题区域 */ +.users__header { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; +} + +.users__title { + margin: 0; + position: relative; + font-size: 30px; + line-height: 36px; + font-weight: 700; + color: #000; +} + +/* 操作按钮组 */ +.users__actions { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.users__actionBtn { + cursor: pointer; + border: 1px solid #e5e7eb; + padding: 8px 16px; + background-color: #fff; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + @include transition-fast; + + &:hover { + background-color: #f9fafb; + border-color: #d1d5db; + } + + svg { + width: 16px; + height: 16px; + color: #333; + } +} + +.users__actionText { + position: relative; + font-size: 14px; + line-height: 20px; + font-weight: 500; + font-family: 'Noto Sans SC', $font-family-base; + color: #333; + text-align: left; +} + +/* 主内容卡片区域 */ +.users__card { + align-self: stretch; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 12px; + background-color: #fff; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 24px; + gap: 16px; +} + +/* 搜索和筛选区域 */ +.users__filters { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; +} + +.users__searchBar { + align-self: stretch; + border-radius: 8px; + background-color: #f3f4f6; + display: flex; + align-items: center; +} + +.users__searchIcon { + height: 48px; + width: 40px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + svg { + width: 20px; + height: 20px; + color: #6b7280; + } +} + +.users__searchInput { + width: calc(100% - 40px); + border: none; + outline: none; + background-color: transparent; + flex: 1; + border-radius: 8px; + overflow: hidden; + display: flex; + align-items: center; + padding: 14px 8px 15px; + box-sizing: border-box; + font-family: 'Noto Sans SC', $font-family-base; + font-size: 16px; + color: #333; + + &::placeholder { + color: #6b7280; + } +} + +/* 高级筛选折叠面板 */ +.users__filterPanel { + align-self: stretch; + border-radius: 8px; + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + align-items: flex-start; + overflow: hidden; +} + +.users__filterHeader { + align-self: stretch; + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + cursor: pointer; + @include transition-fast; + + &:hover { + background-color: #f9fafb; + } +} + +.users__filterTitle { + position: relative; + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #333; +} + +.users__filterArrow { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + @include transition-fast; + + &--expanded { + transform: rotate(180deg); + } + + svg { + width: 16px; + height: 16px; + color: #6b7280; + } +} + +.users__filterContent { + align-self: stretch; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 16px; + padding: 16px; + border-top: 1px solid #e5e7eb; + background-color: #fafafa; +} + +/* 表格区域 */ +.users__table { + align-self: stretch; + overflow-x: auto; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +/* 表格头部 */ +.users__tableHeader { + align-self: stretch; + min-width: 900px; + background-color: #f3f4f6; + display: flex; + align-items: center; + gap: 8px; + padding: 0 16px; +} + +.users__tableHeaderCell { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 16px 8px; + font-size: 12px; + line-height: 16px; + font-weight: 700; + color: #6b7280; + text-transform: uppercase; + + &--checkbox { + width: 48px; + flex-shrink: 0; + } + + &--id { + width: 70px; + flex-shrink: 0; + } + + &--avatar { + width: 50px; + flex-shrink: 0; + } + + &--nickname { + width: 60px; + flex-shrink: 0; + } + + &--adoptions { + width: 70px; + flex-shrink: 0; + } + + &--teamAddress { + width: 90px; + flex-shrink: 0; + } + + &--teamTotal { + width: 80px; + flex-shrink: 0; + } + + &--province { + width: 100px; + flex-shrink: 0; + } + + &--city { + width: 100px; + flex-shrink: 0; + } + + &--referrer { + width: 80px; + flex-shrink: 0; + } + + &--ranking { + width: 50px; + flex-shrink: 0; + text-align: center; + } + + &--actions { + width: 80px; + flex-shrink: 0; + } +} + +/* 表格行 */ +.users__tableBody { + align-self: stretch; + min-width: 900px; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.users__tableRow { + align-self: stretch; + display: flex; + align-items: center; + gap: 8px; + padding: 0 16px; + border-bottom: 1px solid #e5e7eb; + @include transition-fast; + + &:hover { + background-color: #f9fafb; + } + + &--selected { + background-color: #eff6ff; + + &:hover { + background-color: #dbeafe; + } + } +} + +.users__tableCell { + display: flex; + align-items: center; + padding: 16px 8px; + font-size: 14px; + line-height: 20px; + color: #333; + + &--checkbox { + width: 48px; + flex-shrink: 0; + justify-content: center; + } + + &--id { + width: 70px; + flex-shrink: 0; + font-family: 'Consolas', monospace; + } + + &--avatar { + width: 50px; + flex-shrink: 0; + justify-content: center; + } + + &--nickname { + width: 60px; + flex-shrink: 0; + font-weight: 500; + } + + &--adoptions { + width: 70px; + flex-shrink: 0; + } + + &--teamAddress { + width: 90px; + flex-shrink: 0; + } + + &--teamTotal { + width: 80px; + flex-shrink: 0; + } + + &--province { + width: 100px; + flex-shrink: 0; + flex-direction: column; + align-items: flex-start; + } + + &--city { + width: 100px; + flex-shrink: 0; + flex-direction: column; + align-items: flex-start; + } + + &--referrer { + width: 80px; + flex-shrink: 0; + font-family: 'Consolas', monospace; + } + + &--ranking { + width: 50px; + flex-shrink: 0; + justify-content: center; + font-weight: 500; + + &.users__tableCell--gold { + color: #333; + } + + &.users__tableCell--normal { + color: #6b7280; + } + } + + &--actions { + width: 80px; + flex-shrink: 0; + justify-content: center; + gap: 8px; + } +} + +.users__percentage { + font-size: 14px; + line-height: 20px; + color: #6b7280; +} + +/* 用户头像 */ +.users__avatar { + width: 40px; + height: 40px; + border-radius: 9999px; + background-size: cover; + background-repeat: no-repeat; + background-position: center; + position: relative; + flex-shrink: 0; +} + +.users__avatarStatus { + width: 12px; + height: 12px; + position: absolute; + right: 0; + bottom: 0; + box-shadow: 0px 0px 0px 2px #fff; + border-radius: 9999px; + + &--online { + background-color: #22c55e; + } + + &--offline { + background-color: #94a3b8; + } + + &--busy { + background-color: #ef4444; + } +} + +/* 操作按钮 */ +.users__rowAction { + cursor: pointer; + border: none; + padding: 0; + background-color: transparent; + font-size: 12px; + line-height: 16px; + font-family: 'Noto Sans SC', $font-family-base; + color: #005a9c; + text-align: center; + @include transition-fast; + + &:hover { + text-decoration: underline; + } +} + +/* 复选框 */ +.users__checkbox { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: #005a9c; +} + +/* 分页区域 */ +.users__pagination { + align-self: stretch; + display: flex; + align-items: flex-end; + justify-content: space-between; + flex-wrap: wrap; + padding: 16px 0 0; + gap: 16px; +} + +.users__paginationInfo { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + line-height: 20px; + color: #333; +} + +.users__paginationLabel { + color: #6b7280; +} + +.users__paginationSelect { + border-radius: 8px; + background-color: #f3f4f6; + border: 1px solid #cbd5e1; + padding: 7px 30px 7px 12px; + font-size: 14px; + line-height: 20px; + color: #333; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 8px center; + background-repeat: no-repeat; + background-size: 16px; +} + +.users__paginationTotal { + color: #6b7280; + + span { + font-weight: 600; + color: #333; + } +} + +/* 分页按钮列表 */ +.users__paginationList { + display: flex; + align-items: flex-start; + flex-wrap: wrap; +} + +.users__pageBtn { + cursor: pointer; + border: 1px solid #cbd5e1; + padding: 6px 12px; + background-color: transparent; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + line-height: 20px; + font-family: 'Noto Sans SC', $font-family-base; + color: #333; + margin-left: -1px; + @include transition-fast; + + &:first-child { + border-radius: 8px 0 0 8px; + margin-left: 0; + } + + &:last-child { + border-radius: 0 8px 8px 0; + } + + &:hover:not(&--active):not(&--disabled) { + background-color: #f3f4f6; + } + + &--active { + background-color: #005a9c; + border-color: #005a9c; + color: #fff; + z-index: 1; + } + + &--disabled { + color: #9ca3af; + cursor: not-allowed; + } + + &--ellipsis { + cursor: default; + } +} + +/* 用户详情弹窗 */ +.userDetail { + &__header { + @include flex-start; + gap: $spacing-lg; + margin-bottom: $spacing-xl; + padding-bottom: $spacing-lg; + border-bottom: 1px solid $border-color; + + h3 { + font-size: $font-size-lg; + font-weight: $font-weight-semibold; + margin: 0 0 $spacing-xs; + } + + p { + font-size: $font-size-sm; + color: $text-secondary; + margin: 0; + } + } + + &__stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: $spacing-lg; + } + + &__statItem { + @include flex-column; + align-items: center; + padding: $spacing-base; + background-color: $background-color; + border-radius: $border-radius-base; + } + + &__statLabel { + font-size: $font-size-xs; + color: $text-secondary; + margin-bottom: $spacing-xs; + } + + &__statValue { + font-family: $font-family-number; + font-size: $font-size-xl; + font-weight: $font-weight-semibold; + color: $text-primary; + } +} diff --git a/frontend/admin-web/src/app/globals.scss b/frontend/admin-web/src/app/globals.scss new file mode 100644 index 00000000..51df908e --- /dev/null +++ b/frontend/admin-web/src/app/globals.scss @@ -0,0 +1,101 @@ +@use '../styles/variables' as vars; +@import '../styles/reset.scss'; +@import '../styles/typography.scss'; +@import '../styles/animations.scss'; + +// CSS 变量(用于运行时主题切换) +:root { + // 主色调 + --color-primary: #{vars.$primary-color}; + --color-primary-light: #{vars.$primary-light}; + --color-primary-dark: #{vars.$primary-dark}; + + // 辅助色 + --color-success: #{vars.$success-color}; + --color-warning: #{vars.$warning-color}; + --color-error: #{vars.$error-color}; + --color-info: #{vars.$info-color}; + + // 中性色 + --color-text-primary: #{vars.$text-primary}; + --color-text-secondary: #{vars.$text-secondary}; + --color-text-disabled: #{vars.$text-disabled}; + --color-border: #{vars.$border-color}; + --color-background: #{vars.$background-color}; + --color-card: #{vars.$card-background}; + --color-sidebar: #{vars.$sidebar-background}; + + // 语义色 + --color-increase: #{vars.$increase-color}; + --color-decrease: #{vars.$decrease-color}; + + // 字体 + --font-family-base: #{vars.$font-family-base}; + --font-family-number: #{vars.$font-family-number}; + + // 布局尺寸 + --sidebar-width: #{vars.$sidebar-width}; + --sidebar-collapsed-width: #{vars.$sidebar-collapsed-width}; + --header-height: #{vars.$header-height}; + --page-padding: #{vars.$page-padding}; +} + +// 基础样式 +* { + box-sizing: border-box; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--font-family-base); + font-size: vars.$font-size-base; + line-height: vars.$line-height-base; + color: var(--color-text-primary); + background-color: var(--color-background); +} + +// 自定义滚动条 +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background-color: vars.$border-color; + border-radius: 4px; + + &:hover { + background-color: vars.$text-secondary; + } +} + +// 选中文本颜色 +::selection { + background-color: rgba(vars.$primary-color, 0.2); + color: vars.$text-primary; +} + +// 焦点样式 +:focus-visible { + outline: 2px solid vars.$primary-color; + outline-offset: 2px; +} + +// 链接样式 +a { + color: vars.$primary-color; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} diff --git a/frontend/admin-web/src/app/layout.tsx b/frontend/admin-web/src/app/layout.tsx new file mode 100644 index 00000000..51f0ae4e --- /dev/null +++ b/frontend/admin-web/src/app/layout.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from 'next'; +import { Providers } from './providers'; +import './globals.scss'; + +export const metadata: Metadata = { + title: '榴莲认种管理后台 - RWADurian Admin', + description: '榴莲认种业务管理系统', + icons: { + icon: '/favicon.ico', + }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/frontend/admin-web/src/app/page.tsx b/frontend/admin-web/src/app/page.tsx new file mode 100644 index 00000000..f889cb61 --- /dev/null +++ b/frontend/admin-web/src/app/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation'; + +export default function Home() { + redirect('/dashboard'); +} diff --git a/frontend/admin-web/src/app/providers.tsx b/frontend/admin-web/src/app/providers.tsx new file mode 100644 index 00000000..f88dbea6 --- /dev/null +++ b/frontend/admin-web/src/app/providers.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { Provider } from 'react-redux'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { useState } from 'react'; +import { store } from '@/store/redux/store'; + +export function Providers({ children }: { children: React.ReactNode }) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 5 * 60 * 1000, // 5 minutes + refetchOnWindowFocus: false, + }, + }, + }) + ); + + return ( + + {children} + + ); +} diff --git a/frontend/admin-web/src/components/common/Avatar/Avatar.module.scss b/frontend/admin-web/src/components/common/Avatar/Avatar.module.scss new file mode 100644 index 00000000..6ae787f4 --- /dev/null +++ b/frontend/admin-web/src/components/common/Avatar/Avatar.module.scss @@ -0,0 +1,94 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.avatar { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + background-color: $background-color; + overflow: hidden; + flex-shrink: 0; + + &--xs { + width: 24px; + height: 24px; + font-size: 10px; + } + + &--sm { + width: 32px; + height: 32px; + font-size: $font-size-xs; + } + + &--md { + width: 40px; + height: 40px; + font-size: $font-size-sm; + } + + &--lg { + width: 48px; + height: 48px; + font-size: $font-size-base; + } + + &--xl { + width: 64px; + height: 64px; + font-size: $font-size-lg; + } + + &--circle { + border-radius: $border-radius-round; + } + + &--square { + border-radius: $border-radius-base; + } + + &__image { + width: 100%; + height: 100%; + object-fit: cover; + } + + &__initials { + font-weight: $font-weight-medium; + color: $text-secondary; + text-transform: uppercase; + } + + &__placeholder { + @include flex-center; + width: 60%; + height: 60%; + color: $text-disabled; + + svg { + width: 100%; + height: 100%; + } + } + + &__status { + position: absolute; + bottom: 0; + right: 0; + width: 25%; + height: 25%; + min-width: 8px; + min-height: 8px; + border-radius: 50%; + border: 2px solid $card-background; + + &--online { + background-color: $success-color; + } + + &--offline { + background-color: $text-disabled; + } + } +} diff --git a/frontend/admin-web/src/components/common/Avatar/Avatar.tsx b/frontend/admin-web/src/components/common/Avatar/Avatar.tsx new file mode 100644 index 00000000..23b74e1f --- /dev/null +++ b/frontend/admin-web/src/components/common/Avatar/Avatar.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { FC } from 'react'; +import Image from 'next/image'; +import { cn } from '@/utils/helpers'; +import styles from './Avatar.module.scss'; + +export interface AvatarProps { + src?: string; + alt?: string; + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + shape?: 'circle' | 'square'; + name?: string; + online?: boolean; + className?: string; +} + +const sizeMap = { + xs: 24, + sm: 32, + md: 40, + lg: 48, + xl: 64, +}; + +export const Avatar: FC = ({ + src, + alt = 'avatar', + size = 'md', + shape = 'circle', + name, + online, + className, +}) => { + const getInitials = (name: string): string => { + return name + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + .slice(0, 2); + }; + + const pixelSize = sizeMap[size]; + + return ( +
+ {src ? ( + {alt} + ) : name ? ( + {getInitials(name)} + ) : ( + + + + + + )} + {online !== undefined && ( + + )} +
+ ); +}; diff --git a/frontend/admin-web/src/components/common/Avatar/index.ts b/frontend/admin-web/src/components/common/Avatar/index.ts new file mode 100644 index 00000000..4a3dd72a --- /dev/null +++ b/frontend/admin-web/src/components/common/Avatar/index.ts @@ -0,0 +1,2 @@ +export { Avatar } from './Avatar'; +export type { AvatarProps } from './Avatar'; diff --git a/frontend/admin-web/src/components/common/Badge/Badge.module.scss b/frontend/admin-web/src/components/common/Badge/Badge.module.scss new file mode 100644 index 00000000..2aa9a42f --- /dev/null +++ b/frontend/admin-web/src/components/common/Badge/Badge.module.scss @@ -0,0 +1,142 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.badge { + position: relative; + display: inline-flex; + + &__dot { + position: absolute; + top: 0; + right: 0; + transform: translate(50%, -50%); + width: 8px; + height: 8px; + border-radius: 50%; + border: 2px solid $card-background; + + &--default { + background-color: $text-secondary; + } + + &--primary { + background-color: $primary-color; + } + + &--success { + background-color: $success-color; + } + + &--warning { + background-color: $warning-color; + } + + &--error { + background-color: $error-color; + } + + &--info { + background-color: $info-color; + } + } + + &__count { + position: absolute; + top: 0; + right: 0; + transform: translate(50%, -50%); + @include flex-center; + min-width: 18px; + height: 18px; + padding: 0 $spacing-xs; + border-radius: $border-radius-pill; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + color: #ffffff; + border: 2px solid $card-background; + + &--sm { + min-width: 16px; + height: 16px; + font-size: 10px; + } + + &--md { + min-width: 18px; + height: 18px; + font-size: $font-size-xs; + } + + &--default { + background-color: $text-secondary; + } + + &--primary { + background-color: $primary-color; + } + + &--success { + background-color: $success-color; + } + + &--warning { + background-color: $warning-color; + } + + &--error { + background-color: $error-color; + } + + &--info { + background-color: $info-color; + } + } +} + +.badgeTag { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: $border-radius-sm; + font-weight: $font-weight-medium; + + &--sm { + padding: 2px $spacing-sm; + font-size: $font-size-xs; + } + + &--md { + padding: $spacing-xs $spacing-sm; + font-size: $font-size-xs; + } + + &--default { + background-color: $background-color; + color: $text-secondary; + } + + &--primary { + background-color: rgba($primary-color, 0.1); + color: $primary-color; + } + + &--success { + background-color: rgba($success-color, 0.1); + color: $success-color; + } + + &--warning { + background-color: rgba($warning-color, 0.1); + color: darken($warning-color, 10%); + } + + &--error { + background-color: rgba($error-color, 0.1); + color: $error-color; + } + + &--info { + background-color: rgba($info-color, 0.1); + color: $info-color; + } +} diff --git a/frontend/admin-web/src/components/common/Badge/Badge.tsx b/frontend/admin-web/src/components/common/Badge/Badge.tsx new file mode 100644 index 00000000..ca4b8b17 --- /dev/null +++ b/frontend/admin-web/src/components/common/Badge/Badge.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { FC, ReactNode } from 'react'; +import { cn } from '@/utils/helpers'; +import styles from './Badge.module.scss'; + +export interface BadgeProps { + variant?: 'default' | 'primary' | 'success' | 'warning' | 'error' | 'info'; + size?: 'sm' | 'md'; + dot?: boolean; + count?: number; + maxCount?: number; + children?: ReactNode; + className?: string; +} + +export const Badge: FC = ({ + variant = 'default', + size = 'md', + dot = false, + count, + maxCount = 99, + children, + className, +}) => { + const displayCount = count && count > maxCount ? `${maxCount}+` : count; + + // 如果只是显示徽章标签(没有children) + if (!children) { + return ( + + {displayCount} + + ); + } + + // 如果是包裹children的角标 + return ( + + {children} + {dot ? ( + + ) : count !== undefined && count > 0 ? ( + + {displayCount} + + ) : null} + + ); +}; diff --git a/frontend/admin-web/src/components/common/Badge/index.ts b/frontend/admin-web/src/components/common/Badge/index.ts new file mode 100644 index 00000000..cd3531df --- /dev/null +++ b/frontend/admin-web/src/components/common/Badge/index.ts @@ -0,0 +1,2 @@ +export { Badge } from './Badge'; +export type { BadgeProps } from './Badge'; diff --git a/frontend/admin-web/src/components/common/Button/Button.module.scss b/frontend/admin-web/src/components/common/Button/Button.module.scss new file mode 100644 index 00000000..812741ac --- /dev/null +++ b/frontend/admin-web/src/components/common/Button/Button.module.scss @@ -0,0 +1,127 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.button { + @include button-base; + position: relative; + + &--primary { + background-color: $primary-color; + color: #ffffff; + + &:hover:not(:disabled) { + background-color: $primary-light; + } + + &:active:not(:disabled) { + background-color: $primary-dark; + } + } + + &--secondary { + background-color: #ffffff; + color: $primary-color; + border: 1px solid $primary-color; + + &:hover:not(:disabled) { + background-color: rgba($primary-color, 0.05); + } + + &:active:not(:disabled) { + background-color: rgba($primary-color, 0.1); + } + } + + &--outline { + background-color: transparent; + color: $text-primary; + border: 1px solid $border-color; + + &:hover:not(:disabled) { + border-color: $text-secondary; + background-color: $background-color; + } + } + + &--ghost { + background-color: transparent; + color: $text-primary; + + &:hover:not(:disabled) { + background-color: $background-color; + } + } + + &--danger { + background-color: $error-color; + color: #ffffff; + + &:hover:not(:disabled) { + background-color: darken($error-color, 5%); + } + + &:active:not(:disabled) { + background-color: darken($error-color, 10%); + } + } + + &--sm { + padding: $spacing-sm $spacing-base; + font-size: $font-size-sm; + border-radius: $border-radius-sm; + } + + &--md { + padding: $spacing-md $spacing-lg; + font-size: $font-size-base; + } + + &--lg { + padding: $spacing-base $spacing-xl; + font-size: $font-size-md; + } + + &--fullWidth { + width: 100%; + } + + &--loading { + pointer-events: none; + } + + &__spinner { + @include flex-center; + width: 16px; + height: 16px; + + svg { + width: 100%; + height: 100%; + animation: spin 1s linear infinite; + } + } + + &__icon { + @include flex-center; + width: 16px; + height: 16px; + + svg { + width: 100%; + height: 100%; + } + } + + &__text { + flex: 1; + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/frontend/admin-web/src/components/common/Button/Button.tsx b/frontend/admin-web/src/components/common/Button/Button.tsx new file mode 100644 index 00000000..37a4bd77 --- /dev/null +++ b/frontend/admin-web/src/components/common/Button/Button.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { FC, ButtonHTMLAttributes, ReactNode } from 'react'; +import { cn } from '@/utils/helpers'; +import styles from './Button.module.scss'; + +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'; + size?: 'sm' | 'md' | 'lg'; + icon?: ReactNode; + iconPosition?: 'left' | 'right'; + loading?: boolean; + fullWidth?: boolean; + children?: ReactNode; +} + +export const Button: FC = ({ + variant = 'primary', + size = 'md', + icon, + iconPosition = 'left', + loading = false, + fullWidth = false, + disabled, + className, + children, + ...props +}) => { + const isDisabled = disabled || loading; + + return ( + + ); +}; diff --git a/frontend/admin-web/src/components/common/Button/index.ts b/frontend/admin-web/src/components/common/Button/index.ts new file mode 100644 index 00000000..fa3c8a51 --- /dev/null +++ b/frontend/admin-web/src/components/common/Button/index.ts @@ -0,0 +1,2 @@ +export { Button } from './Button'; +export type { ButtonProps } from './Button'; diff --git a/frontend/admin-web/src/components/common/Card/Card.module.scss b/frontend/admin-web/src/components/common/Card/Card.module.scss new file mode 100644 index 00000000..fc0ff740 --- /dev/null +++ b/frontend/admin-web/src/components/common/Card/Card.module.scss @@ -0,0 +1,40 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.card { + @include card-base; + + &--noPadding { + .card__body { + padding: 0; + } + } + + &--noShadow { + box-shadow: none; + border: 1px solid $border-color; + } + + &__header { + @include flex-between; + padding: $padding-card; + padding-bottom: $spacing-base; + border-bottom: 1px solid $border-color; + } + + &__title { + font-size: $font-size-md; + font-weight: $font-weight-semibold; + color: $text-primary; + margin: 0; + } + + &__extra { + @include flex-start; + gap: $spacing-sm; + } + + &__body { + padding: $padding-card; + } +} diff --git a/frontend/admin-web/src/components/common/Card/Card.tsx b/frontend/admin-web/src/components/common/Card/Card.tsx new file mode 100644 index 00000000..62983524 --- /dev/null +++ b/frontend/admin-web/src/components/common/Card/Card.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { FC, ReactNode } from 'react'; +import { cn } from '@/utils/helpers'; +import styles from './Card.module.scss'; + +export interface CardProps { + title?: string; + extra?: ReactNode; + padding?: boolean; + shadow?: boolean; + className?: string; + children: ReactNode; +} + +export const Card: FC = ({ + title, + extra, + padding = true, + shadow = true, + className, + children, +}) => { + return ( +
+ {(title || extra) && ( +
+ {title &&

{title}

} + {extra &&
{extra}
} +
+ )} +
{children}
+
+ ); +}; diff --git a/frontend/admin-web/src/components/common/Card/index.ts b/frontend/admin-web/src/components/common/Card/index.ts new file mode 100644 index 00000000..050a95d7 --- /dev/null +++ b/frontend/admin-web/src/components/common/Card/index.ts @@ -0,0 +1,2 @@ +export { Card } from './Card'; +export type { CardProps } from './Card'; diff --git a/frontend/admin-web/src/components/common/Input/Input.module.scss b/frontend/admin-web/src/components/common/Input/Input.module.scss new file mode 100644 index 00000000..49acfdf2 --- /dev/null +++ b/frontend/admin-web/src/components/common/Input/Input.module.scss @@ -0,0 +1,106 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.inputWrapper { + display: flex; + flex-direction: column; + gap: $spacing-xs; + + &__label { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-primary; + } + + &__helper { + font-size: $font-size-xs; + color: $text-secondary; + + &--error { + color: $error-color; + } + } +} + +.input { + display: flex; + align-items: center; + gap: $spacing-sm; + border: 1px solid $border-color; + border-radius: $border-radius-base; + background-color: $card-background; + @include transition-fast; + + &:focus-within { + border-color: $primary-color; + box-shadow: 0 0 0 3px rgba($primary-color, 0.1); + } + + &--sm { + padding: $spacing-sm $spacing-md; + + .input__field { + font-size: $font-size-sm; + } + } + + &--md { + padding: $spacing-md $spacing-base; + + .input__field { + font-size: $font-size-base; + } + } + + &--lg { + padding: $spacing-base $spacing-lg; + + .input__field { + font-size: $font-size-md; + } + } + + &--error { + border-color: $error-color; + + &:focus-within { + border-color: $error-color; + box-shadow: 0 0 0 3px rgba($error-color, 0.1); + } + } + + &--disabled { + background-color: $background-color; + cursor: not-allowed; + + .input__field { + cursor: not-allowed; + } + } + + &__prefix, + &__suffix { + @include flex-center; + color: $text-secondary; + flex-shrink: 0; + + svg { + width: 16px; + height: 16px; + } + } + + &__field { + flex: 1; + border: none; + outline: none; + background: transparent; + color: $text-primary; + font-family: $font-family-base; + min-width: 0; + + &::placeholder { + color: $text-disabled; + } + } +} diff --git a/frontend/admin-web/src/components/common/Input/Input.tsx b/frontend/admin-web/src/components/common/Input/Input.tsx new file mode 100644 index 00000000..0dea2924 --- /dev/null +++ b/frontend/admin-web/src/components/common/Input/Input.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { CSSProperties, InputHTMLAttributes, ReactNode, forwardRef } from 'react'; +import { cn } from '@/utils/helpers'; +import styles from './Input.module.scss'; + +export interface InputProps extends Omit, 'size' | 'prefix'> { + size?: 'sm' | 'md' | 'lg'; + prefix?: ReactNode; + suffix?: ReactNode; + error?: string; + label?: string; + helperText?: string; + wrapperStyle?: CSSProperties; +} + +export const Input = forwardRef( + ( + { + size = 'md', + prefix, + suffix, + error, + label, + helperText, + disabled, + className, + id, + style, + wrapperStyle, + ...props + }, + ref + ) => { + const inputId = id || `input-${Math.random().toString(36).substring(7)}`; + + return ( +
+ {label && ( + + )} +
+ {prefix && {prefix}} + + {suffix && {suffix}} +
+ {(error || helperText) && ( + + {error || helperText} + + )} +
+ ); + } +); + +Input.displayName = 'Input'; diff --git a/frontend/admin-web/src/components/common/Input/index.ts b/frontend/admin-web/src/components/common/Input/index.ts new file mode 100644 index 00000000..785490f4 --- /dev/null +++ b/frontend/admin-web/src/components/common/Input/index.ts @@ -0,0 +1,2 @@ +export { Input } from './Input'; +export type { InputProps } from './Input'; diff --git a/frontend/admin-web/src/components/common/Loading/Loading.module.scss b/frontend/admin-web/src/components/common/Loading/Loading.module.scss new file mode 100644 index 00000000..f7de4714 --- /dev/null +++ b/frontend/admin-web/src/components/common/Loading/Loading.module.scss @@ -0,0 +1,91 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.loading { + @include flex-column-center; + gap: $spacing-sm; + color: $primary-color; + + &--sm { + .loading__spinner { + width: 20px; + height: 20px; + } + + .loading__tip { + font-size: $font-size-xs; + } + } + + &--md { + .loading__spinner { + width: 32px; + height: 32px; + } + + .loading__tip { + font-size: $font-size-sm; + } + } + + &--lg { + .loading__spinner { + width: 48px; + height: 48px; + } + + .loading__tip { + font-size: $font-size-base; + } + } + + &__spinner { + animation: spin 1s linear infinite; + + svg { + width: 100%; + height: 100%; + } + + circle { + stroke-dasharray: 80, 200; + stroke-dashoffset: 0; + animation: dash 1.5s ease-in-out infinite; + } + } + + &__tip { + color: $text-secondary; + } + + &__fullScreen { + @include fixed-fill; + @include flex-center; + background-color: rgba($card-background, 0.8); + z-index: $z-index-modal; + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@keyframes dash { + 0% { + stroke-dasharray: 1, 200; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 90, 200; + stroke-dashoffset: -35; + } + 100% { + stroke-dasharray: 90, 200; + stroke-dashoffset: -125; + } +} diff --git a/frontend/admin-web/src/components/common/Loading/Loading.tsx b/frontend/admin-web/src/components/common/Loading/Loading.tsx new file mode 100644 index 00000000..4b19fd4d --- /dev/null +++ b/frontend/admin-web/src/components/common/Loading/Loading.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { FC } from 'react'; +import { cn } from '@/utils/helpers'; +import styles from './Loading.module.scss'; + +export interface LoadingProps { + size?: 'sm' | 'md' | 'lg'; + fullScreen?: boolean; + tip?: string; + className?: string; +} + +export const Loading: FC = ({ size = 'md', fullScreen = false, tip, className }) => { + const content = ( +
+
+ + + +
+ {tip && {tip}} +
+ ); + + if (fullScreen) { + return
{content}
; + } + + return content; +}; diff --git a/frontend/admin-web/src/components/common/Loading/index.ts b/frontend/admin-web/src/components/common/Loading/index.ts new file mode 100644 index 00000000..516de1f4 --- /dev/null +++ b/frontend/admin-web/src/components/common/Loading/index.ts @@ -0,0 +1,2 @@ +export { Loading } from './Loading'; +export type { LoadingProps } from './Loading'; diff --git a/frontend/admin-web/src/components/common/Modal/Modal.module.scss b/frontend/admin-web/src/components/common/Modal/Modal.module.scss new file mode 100644 index 00000000..62c656b3 --- /dev/null +++ b/frontend/admin-web/src/components/common/Modal/Modal.module.scss @@ -0,0 +1,101 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.modal { + @include fixed-fill; + z-index: $z-index-modal; + @include flex-center; + + &__mask { + @include absolute-fill; + background-color: rgba(0, 0, 0, 0.45); + animation: fadeIn $duration-fast $ease-out; + } + + &__wrapper { + position: relative; + max-height: calc(100vh - 100px); + max-width: calc(100vw - 32px); + } + + &__content { + background-color: $card-background; + border-radius: $border-radius-lg; + box-shadow: $shadow-xl; + animation: scaleIn $duration-base $ease-out; + display: flex; + flex-direction: column; + max-height: calc(100vh - 100px); + } + + &__header { + @include flex-between; + padding: $padding-modal; + padding-bottom: $spacing-base; + border-bottom: 1px solid $border-color; + flex-shrink: 0; + } + + &__title { + font-size: $font-size-lg; + font-weight: $font-weight-semibold; + color: $text-primary; + margin: 0; + } + + &__close { + @include flex-center; + width: 32px; + height: 32px; + border-radius: $border-radius-sm; + color: $text-secondary; + @include transition-fast; + + &:hover { + background-color: $background-color; + color: $text-primary; + } + + svg { + width: 20px; + height: 20px; + } + } + + &__body { + padding: $padding-modal; + overflow-y: auto; + flex: 1; + @include custom-scrollbar; + } + + &__footer { + @include flex-start; + justify-content: flex-end; + gap: $spacing-sm; + padding: $padding-modal; + padding-top: $spacing-base; + border-top: 1px solid $border-color; + flex-shrink: 0; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} diff --git a/frontend/admin-web/src/components/common/Modal/Modal.tsx b/frontend/admin-web/src/components/common/Modal/Modal.tsx new file mode 100644 index 00000000..10651e0f --- /dev/null +++ b/frontend/admin-web/src/components/common/Modal/Modal.tsx @@ -0,0 +1,111 @@ +'use client'; + +import { FC, ReactNode, useEffect, useCallback } from 'react'; +import { createPortal } from 'react-dom'; +import { cn } from '@/utils/helpers'; +import { Button } from '../Button'; +import styles from './Modal.module.scss'; + +export interface ModalProps { + visible: boolean; + title: string; + width?: number; + onClose: () => void; + onConfirm?: () => void; + confirmText?: string; + cancelText?: string; + loading?: boolean; + footer?: ReactNode | null; + closable?: boolean; + maskClosable?: boolean; + children: ReactNode; +} + +export const Modal: FC = ({ + visible, + title, + width = 520, + onClose, + onConfirm, + confirmText = '确定', + cancelText = '取消', + loading = false, + footer, + closable = true, + maskClosable = true, + children, +}) => { + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape' && closable) { + onClose(); + } + }, + [closable, onClose] + ); + + useEffect(() => { + if (visible) { + document.addEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'hidden'; + } + + return () => { + document.removeEventListener('keydown', handleKeyDown); + document.body.style.overflow = ''; + }; + }, [visible, handleKeyDown]); + + const handleMaskClick = () => { + if (maskClosable) { + onClose(); + } + }; + + const renderFooter = () => { + if (footer === null) return null; + + if (footer) return footer; + + return ( +
+ + {onConfirm && ( + + )} +
+ ); + }; + + if (!visible) return null; + + const modalContent = ( +
+
+
+
e.stopPropagation()}> +
+

{title}

+ {closable && ( + + )} +
+
{children}
+ {renderFooter()} +
+
+
+ ); + + if (typeof document === 'undefined') return null; + + return createPortal(modalContent, document.body); +}; diff --git a/frontend/admin-web/src/components/common/Modal/index.ts b/frontend/admin-web/src/components/common/Modal/index.ts new file mode 100644 index 00000000..05844a90 --- /dev/null +++ b/frontend/admin-web/src/components/common/Modal/index.ts @@ -0,0 +1,2 @@ +export { Modal } from './Modal'; +export type { ModalProps } from './Modal'; diff --git a/frontend/admin-web/src/components/common/Pagination/Pagination.module.scss b/frontend/admin-web/src/components/common/Pagination/Pagination.module.scss new file mode 100644 index 00000000..2bd6e4c1 --- /dev/null +++ b/frontend/admin-web/src/components/common/Pagination/Pagination.module.scss @@ -0,0 +1,85 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.pagination { + @include flex-between; + gap: $spacing-base; + flex-wrap: wrap; + + &__total { + font-size: $font-size-sm; + color: $text-secondary; + + strong { + color: $text-primary; + font-weight: $font-weight-medium; + } + } + + &__controls { + @include flex-start; + gap: $spacing-xs; + } + + &__item { + @include flex-center; + min-width: 32px; + height: 32px; + padding: 0 $spacing-sm; + border: 1px solid $border-color; + border-radius: $border-radius-sm; + background-color: $card-background; + color: $text-primary; + font-size: $font-size-sm; + @include transition-fast; + + &:hover:not(&--active) { + border-color: $primary-color; + color: $primary-color; + } + + &--active { + background-color: $primary-color; + border-color: $primary-color; + color: #ffffff; + } + } + + &__arrow { + @include flex-center; + width: 32px; + height: 32px; + border: 1px solid $border-color; + border-radius: $border-radius-sm; + background-color: $card-background; + color: $text-primary; + @include transition-fast; + + svg { + width: 16px; + height: 16px; + } + + &:hover:not(&--disabled) { + border-color: $primary-color; + color: $primary-color; + } + + &--disabled { + color: $text-disabled; + cursor: not-allowed; + } + } + + &__ellipsis { + @include flex-center; + width: 32px; + height: 32px; + color: $text-secondary; + font-size: $font-size-sm; + } + + &__sizeChanger { + min-width: 110px; + } +} diff --git a/frontend/admin-web/src/components/common/Pagination/Pagination.tsx b/frontend/admin-web/src/components/common/Pagination/Pagination.tsx new file mode 100644 index 00000000..3b43044e --- /dev/null +++ b/frontend/admin-web/src/components/common/Pagination/Pagination.tsx @@ -0,0 +1,145 @@ +'use client'; + +import { FC } from 'react'; +import { cn } from '@/utils/helpers'; +import { Select } from '../Select'; +import styles from './Pagination.module.scss'; + +export interface PaginationProps { + current: number; + pageSize: number; + total: number; + pageSizeOptions?: number[]; + onChange: (page: number, pageSize: number) => void; + showSizeChanger?: boolean; + showTotal?: boolean; + className?: string; +} + +export const Pagination: FC = ({ + current, + pageSize, + total, + pageSizeOptions = [10, 20, 50, 100], + onChange, + showSizeChanger = true, + showTotal = true, + className, +}) => { + const totalPages = Math.ceil(total / pageSize); + + const handlePageChange = (page: number) => { + if (page < 1 || page > totalPages) return; + onChange(page, pageSize); + }; + + const handlePageSizeChange = (size: number) => { + onChange(1, size); + }; + + const renderPageNumbers = () => { + const pages: (number | string)[] = []; + const showEllipsis = totalPages > 7; + + if (!showEllipsis) { + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + if (current <= 4) { + for (let i = 1; i <= 5; i++) { + pages.push(i); + } + pages.push('...'); + pages.push(totalPages); + } else if (current >= totalPages - 3) { + pages.push(1); + pages.push('...'); + for (let i = totalPages - 4; i <= totalPages; i++) { + pages.push(i); + } + } else { + pages.push(1); + pages.push('...'); + for (let i = current - 1; i <= current + 1; i++) { + pages.push(i); + } + pages.push('...'); + pages.push(totalPages); + } + } + + return pages.map((page, index) => { + if (page === '...') { + return ( + + ... + + ); + } + + return ( + + ); + }); + }; + + if (total === 0) return null; + + return ( +
+ {showTotal && ( + + 共 {total} 条 + + )} + +
+ + + {renderPageNumbers()} + + +
+ + {showSizeChanger && ( +
+ setSearchTerm(e.target.value)} + placeholder={selectedOption?.label || placeholder} + onClick={(e) => e.stopPropagation()} + autoFocus + /> + ) : ( + + {selectedOption?.label || placeholder} + + )} +
+ + + + + +
+ {isOpen && ( +
+ {filteredOptions.length === 0 ? ( +
无匹配选项
+ ) : ( + filteredOptions.map((option) => ( +
handleSelect(option)} + > + {option.label} +
+ )) + )} +
+ )} + {error && {error}} +
+ ); +} diff --git a/frontend/admin-web/src/components/common/Select/index.ts b/frontend/admin-web/src/components/common/Select/index.ts new file mode 100644 index 00000000..e398b02f --- /dev/null +++ b/frontend/admin-web/src/components/common/Select/index.ts @@ -0,0 +1,2 @@ +export { Select } from './Select'; +export type { SelectProps, SelectOption } from './Select'; diff --git a/frontend/admin-web/src/components/common/Table/Table.module.scss b/frontend/admin-web/src/components/common/Table/Table.module.scss new file mode 100644 index 00000000..445267bc --- /dev/null +++ b/frontend/admin-web/src/components/common/Table/Table.module.scss @@ -0,0 +1,100 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.tableWrapper { + width: 100%; + overflow-x: auto; + @include custom-scrollbar; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: $font-size-sm; + + &__head { + background-color: $background-color; + } + + &__th { + padding: $spacing-md $spacing-base; + text-align: left; + font-weight: $font-weight-medium; + color: $text-secondary; + white-space: nowrap; + border-bottom: 1px solid $border-color; + + &--left { + text-align: left; + } + + &--center { + text-align: center; + } + + &--right { + text-align: right; + } + } + + &__body { + background-color: $card-background; + } + + &__tr { + @include transition-fast; + + &:hover { + background-color: rgba($primary-color, 0.02); + } + + &--selected { + background-color: rgba($primary-color, 0.05); + + &:hover { + background-color: rgba($primary-color, 0.08); + } + } + + &--clickable { + cursor: pointer; + } + } + + &__td { + padding: $spacing-md $spacing-base; + color: $text-primary; + border-bottom: 1px solid $border-color; + vertical-align: middle; + + &--left { + text-align: left; + } + + &--center { + text-align: center; + } + + &--right { + text-align: right; + } + } + + &__checkbox { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: $primary-color; + } + + &__loading, + &__empty { + text-align: center; + padding: $spacing-xxxl $spacing-base; + color: $text-secondary; + } + + &__loading { + @include flex-center; + } +} diff --git a/frontend/admin-web/src/components/common/Table/Table.tsx b/frontend/admin-web/src/components/common/Table/Table.tsx new file mode 100644 index 00000000..b60eb080 --- /dev/null +++ b/frontend/admin-web/src/components/common/Table/Table.tsx @@ -0,0 +1,167 @@ +'use client'; + +import { ReactNode } from 'react'; +import { cn } from '@/utils/helpers'; +import { Loading } from '../Loading'; +import styles from './Table.module.scss'; + +export interface TableColumn { + key: string; + title: string; + width?: number | string; + sortable?: boolean; + align?: 'left' | 'center' | 'right'; + render?: (value: unknown, record: T, index: number) => ReactNode; +} + +export interface TableProps { + columns: TableColumn[]; + data: T[]; + loading?: boolean; + rowKey?: string | ((record: T) => string); + rowSelection?: { + selectedRowKeys: string[]; + onChange: (keys: string[]) => void; + }; + onRowClick?: (record: T) => void; + emptyText?: string; + className?: string; +} + +export function Table({ + columns, + data, + loading = false, + rowKey = 'id', + rowSelection, + onRowClick, + emptyText = '暂无数据', + className, +}: TableProps) { + const getRowKey = (record: T, index: number): string => { + if (typeof rowKey === 'function') { + return rowKey(record); + } + return String((record as Record)[rowKey] ?? index); + }; + + const handleSelectAll = (checked: boolean) => { + if (!rowSelection) return; + if (checked) { + rowSelection.onChange(data.map((record, index) => getRowKey(record, index))); + } else { + rowSelection.onChange([]); + } + }; + + const handleSelectRow = (key: string, checked: boolean) => { + if (!rowSelection) return; + if (checked) { + rowSelection.onChange([...rowSelection.selectedRowKeys, key]); + } else { + rowSelection.onChange(rowSelection.selectedRowKeys.filter((k) => k !== key)); + } + }; + + const isAllSelected = data.length > 0 && rowSelection?.selectedRowKeys.length === data.length; + const isIndeterminate = + rowSelection && + rowSelection.selectedRowKeys.length > 0 && + rowSelection.selectedRowKeys.length < data.length; + + return ( +
+ + + + {rowSelection && ( + + )} + {columns.map((column) => ( + + ))} + + + + {loading ? ( + + + + ) : data.length === 0 ? ( + + + + ) : ( + data.map((record, index) => { + const key = getRowKey(record, index); + const isSelected = rowSelection?.selectedRowKeys.includes(key); + + return ( + onRowClick?.(record)} + > + {rowSelection && ( + + )} + {columns.map((column) => ( + + ))} + + ); + }) + )} + +
+ { + if (el) el.indeterminate = isIndeterminate ?? false; + }} + onChange={(e) => handleSelectAll(e.target.checked)} + className={styles.table__checkbox} + /> + + {column.title} +
+ +
+ {emptyText} +
+ { + e.stopPropagation(); + handleSelectRow(key, e.target.checked); + }} + onClick={(e) => e.stopPropagation()} + className={styles.table__checkbox} + /> + + {column.render + ? column.render((record as Record)[column.key], record, index) + : ((record as Record)[column.key] as ReactNode)} +
+
+ ); +} diff --git a/frontend/admin-web/src/components/common/Table/index.ts b/frontend/admin-web/src/components/common/Table/index.ts new file mode 100644 index 00000000..7ffffa30 --- /dev/null +++ b/frontend/admin-web/src/components/common/Table/index.ts @@ -0,0 +1,2 @@ +export { Table } from './Table'; +export type { TableProps, TableColumn } from './Table'; diff --git a/frontend/admin-web/src/components/common/Toast/Toast.module.scss b/frontend/admin-web/src/components/common/Toast/Toast.module.scss new file mode 100644 index 00000000..9decaa78 --- /dev/null +++ b/frontend/admin-web/src/components/common/Toast/Toast.module.scss @@ -0,0 +1,111 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.toastContainer { + position: fixed; + top: $spacing-xl; + right: $spacing-xl; + z-index: $z-index-toast; + display: flex; + flex-direction: column; + gap: $spacing-sm; +} + +.toast { + @include flex-start; + gap: $spacing-sm; + padding: $spacing-md $spacing-base; + background-color: $card-background; + border-radius: $border-radius-base; + box-shadow: $shadow-lg; + min-width: 300px; + max-width: 400px; + animation: slideInRight $duration-base $ease-out; + + &--exiting { + animation: slideOutRight $duration-fast $ease-in forwards; + } + + &--success { + .toast__icon { + color: $success-color; + } + } + + &--error { + .toast__icon { + color: $error-color; + } + } + + &--warning { + .toast__icon { + color: $warning-color; + } + } + + &--info { + .toast__icon { + color: $info-color; + } + } + + &__icon { + @include flex-center; + width: 20px; + height: 20px; + flex-shrink: 0; + + svg { + width: 100%; + height: 100%; + } + } + + &__message { + flex: 1; + font-size: $font-size-sm; + color: $text-primary; + line-height: $line-height-base; + } + + &__close { + @include flex-center; + width: 20px; + height: 20px; + color: $text-secondary; + flex-shrink: 0; + @include transition-fast; + + &:hover { + color: $text-primary; + } + + svg { + width: 14px; + height: 14px; + } + } +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideOutRight { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(100%); + } +} diff --git a/frontend/admin-web/src/components/common/Toast/Toast.tsx b/frontend/admin-web/src/components/common/Toast/Toast.tsx new file mode 100644 index 00000000..0737cc5f --- /dev/null +++ b/frontend/admin-web/src/components/common/Toast/Toast.tsx @@ -0,0 +1,166 @@ +'use client'; + +import { FC, useEffect, useState, useCallback } from 'react'; +import { createPortal } from 'react-dom'; +import { cn } from '@/utils/helpers'; +import styles from './Toast.module.scss'; + +export type ToastType = 'success' | 'error' | 'warning' | 'info'; + +export interface ToastItem { + id: string; + type: ToastType; + message: string; + duration?: number; +} + +interface ToastProps { + toast: ToastItem; + onClose: (id: string) => void; +} + +const ToastIcon: FC<{ type: ToastType }> = ({ type }) => { + switch (type) { + case 'success': + return ( + + + + + ); + case 'error': + return ( + + + + + + ); + case 'warning': + return ( + + + + + + ); + case 'info': + default: + return ( + + + + + + ); + } +}; + +const ToastItem: FC = ({ toast, onClose }) => { + const [isExiting, setIsExiting] = useState(false); + + useEffect(() => { + const duration = toast.duration ?? 3000; + if (duration > 0) { + const timer = setTimeout(() => { + setIsExiting(true); + setTimeout(() => onClose(toast.id), 200); + }, duration); + return () => clearTimeout(timer); + } + }, [toast, onClose]); + + const handleClose = () => { + setIsExiting(true); + setTimeout(() => onClose(toast.id), 200); + }; + + return ( +
+ + + + {toast.message} + +
+ ); +}; + +// Toast Container +let toastContainer: HTMLDivElement | null = null; +let toasts: ToastItem[] = []; +let listeners: Array<(toasts: ToastItem[]) => void> = []; + +const getContainer = () => { + if (typeof document === 'undefined') return null; + if (!toastContainer) { + toastContainer = document.createElement('div'); + toastContainer.id = 'toast-container'; + document.body.appendChild(toastContainer); + } + return toastContainer; +}; + +const notify = () => { + listeners.forEach((listener) => listener([...toasts])); +}; + +export const toast = { + show: (message: string, type: ToastType = 'info', duration = 3000) => { + const id = Date.now().toString(); + toasts = [...toasts, { id, message, type, duration }]; + notify(); + return id; + }, + success: (message: string, duration?: number) => toast.show(message, 'success', duration), + error: (message: string, duration?: number) => toast.show(message, 'error', duration), + warning: (message: string, duration?: number) => toast.show(message, 'warning', duration), + info: (message: string, duration?: number) => toast.show(message, 'info', duration), + dismiss: (id: string) => { + toasts = toasts.filter((t) => t.id !== id); + notify(); + }, + dismissAll: () => { + toasts = []; + notify(); + }, +}; + +export const ToastContainer: FC = () => { + const [items, setItems] = useState([]); + + useEffect(() => { + const listener = (newToasts: ToastItem[]) => setItems(newToasts); + listeners.push(listener); + return () => { + listeners = listeners.filter((l) => l !== listener); + }; + }, []); + + const handleClose = useCallback((id: string) => { + toast.dismiss(id); + }, []); + + const container = getContainer(); + if (!container || items.length === 0) return null; + + return createPortal( +
+ {items.map((item) => ( + + ))} +
, + container + ); +}; diff --git a/frontend/admin-web/src/components/common/Toast/index.ts b/frontend/admin-web/src/components/common/Toast/index.ts new file mode 100644 index 00000000..1b914c9a --- /dev/null +++ b/frontend/admin-web/src/components/common/Toast/index.ts @@ -0,0 +1,2 @@ +export { toast, ToastContainer } from './Toast'; +export type { ToastItem, ToastType } from './Toast'; diff --git a/frontend/admin-web/src/components/common/Toggle/Toggle.module.scss b/frontend/admin-web/src/components/common/Toggle/Toggle.module.scss new file mode 100644 index 00000000..d2a7e65f --- /dev/null +++ b/frontend/admin-web/src/components/common/Toggle/Toggle.module.scss @@ -0,0 +1,71 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.toggle { + @include flex-start; + gap: $spacing-sm; + cursor: pointer; + + &--disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &--sm { + .toggle__switch { + width: 32px; + height: 18px; + } + + .toggle__thumb { + width: 14px; + height: 14px; + } + + .toggle__switch--checked .toggle__thumb { + transform: translateX(14px); + } + } + + &--md { + .toggle__switch { + width: 44px; + height: 24px; + } + + .toggle__thumb { + width: 20px; + height: 20px; + } + + .toggle__switch--checked .toggle__thumb { + transform: translateX(20px); + } + } + + &__switch { + position: relative; + border-radius: $border-radius-pill; + background-color: $border-color; + padding: 2px; + @include transition-base; + + &--checked { + background-color: $primary-color; + } + } + + &__thumb { + display: block; + border-radius: 50%; + background-color: #ffffff; + box-shadow: $shadow-sm; + @include transition-base; + } + + &__label { + font-size: $font-size-sm; + color: $text-primary; + user-select: none; + } +} diff --git a/frontend/admin-web/src/components/common/Toggle/Toggle.tsx b/frontend/admin-web/src/components/common/Toggle/Toggle.tsx new file mode 100644 index 00000000..0eab16c8 --- /dev/null +++ b/frontend/admin-web/src/components/common/Toggle/Toggle.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { FC } from 'react'; +import { cn } from '@/utils/helpers'; +import styles from './Toggle.module.scss'; + +export interface ToggleProps { + checked: boolean; + onChange: (checked: boolean) => void; + disabled?: boolean; + size?: 'sm' | 'md'; + label?: string; + className?: string; +} + +export const Toggle: FC = ({ + checked, + onChange, + disabled = false, + size = 'md', + label, + className, +}) => { + const handleClick = () => { + if (!disabled) { + onChange(!checked); + } + }; + + return ( + + ); +}; diff --git a/frontend/admin-web/src/components/common/Toggle/index.ts b/frontend/admin-web/src/components/common/Toggle/index.ts new file mode 100644 index 00000000..e005a382 --- /dev/null +++ b/frontend/admin-web/src/components/common/Toggle/index.ts @@ -0,0 +1,2 @@ +export { Toggle } from './Toggle'; +export type { ToggleProps } from './Toggle'; diff --git a/frontend/admin-web/src/components/common/index.ts b/frontend/admin-web/src/components/common/index.ts new file mode 100644 index 00000000..53b8266e --- /dev/null +++ b/frontend/admin-web/src/components/common/index.ts @@ -0,0 +1,37 @@ +// 通用组件统一导出 + +export { Button } from './Button'; +export type { ButtonProps } from './Button'; + +export { Input } from './Input'; +export type { InputProps } from './Input'; + +export { Select } from './Select'; +export type { SelectProps, SelectOption } from './Select'; + +export { Table } from './Table'; +export type { TableProps, TableColumn } from './Table'; + +export { Modal } from './Modal'; +export type { ModalProps } from './Modal'; + +export { Card } from './Card'; +export type { CardProps } from './Card'; + +export { Badge } from './Badge'; +export type { BadgeProps } from './Badge'; + +export { Avatar } from './Avatar'; +export type { AvatarProps } from './Avatar'; + +export { Pagination } from './Pagination'; +export type { PaginationProps } from './Pagination'; + +export { Toggle } from './Toggle'; +export type { ToggleProps } from './Toggle'; + +export { Loading } from './Loading'; +export type { LoadingProps } from './Loading'; + +export { toast, ToastContainer } from './Toast'; +export type { ToastItem, ToastType } from './Toast'; diff --git a/frontend/admin-web/src/components/features/dashboard/RecentActivity/RecentActivity.module.scss b/frontend/admin-web/src/components/features/dashboard/RecentActivity/RecentActivity.module.scss new file mode 100644 index 00000000..77c4e51c --- /dev/null +++ b/frontend/admin-web/src/components/features/dashboard/RecentActivity/RecentActivity.module.scss @@ -0,0 +1,62 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.recentActivity { + height: 100%; + + &__list { + display: flex; + flex-direction: column; + } + + &__item { + @include flex-start; + gap: $spacing-md; + padding: $spacing-md 0; + border-bottom: 1px solid $border-color; + + &:last-child { + border-bottom: none; + padding-bottom: 0; + } + + &:first-child { + padding-top: 0; + } + } + + &__icon { + @include flex-center; + width: 36px; + height: 36px; + border-radius: $border-radius-base; + background-color: $background-color; + font-size: 16px; + flex-shrink: 0; + } + + &__content { + flex: 1; + min-width: 0; + } + + &__title { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-primary; + margin-bottom: 2px; + } + + &__description { + font-size: $font-size-xs; + color: $text-secondary; + @include text-ellipsis; + } + + &__time { + font-size: $font-size-xs; + color: $text-disabled; + white-space: nowrap; + flex-shrink: 0; + } +} diff --git a/frontend/admin-web/src/components/features/dashboard/RecentActivity/RecentActivity.tsx b/frontend/admin-web/src/components/features/dashboard/RecentActivity/RecentActivity.tsx new file mode 100644 index 00000000..7ecfba6a --- /dev/null +++ b/frontend/admin-web/src/components/features/dashboard/RecentActivity/RecentActivity.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { FC } from 'react'; +import { Card } from '@/components/common'; +import styles from './RecentActivity.module.scss'; + +interface ActivityItem { + id: string; + type: 'user_register' | 'company_activity' | 'system_update' | 'report_generated'; + icon: string; + title: string; + description: string; + timestamp: string; +} + +export interface RecentActivityProps { + activities: ActivityItem[]; +} + +export const RecentActivity: FC = ({ activities }) => { + return ( + +
+ {activities.map((activity) => ( +
+
{activity.icon}
+
+
{activity.title}
+
{activity.description}
+
+
{activity.timestamp}
+
+ ))} +
+
+ ); +}; diff --git a/frontend/admin-web/src/components/features/dashboard/RecentActivity/index.ts b/frontend/admin-web/src/components/features/dashboard/RecentActivity/index.ts new file mode 100644 index 00000000..00f887fd --- /dev/null +++ b/frontend/admin-web/src/components/features/dashboard/RecentActivity/index.ts @@ -0,0 +1,2 @@ +export { RecentActivity } from './RecentActivity'; +export type { RecentActivityProps } from './RecentActivity'; diff --git a/frontend/admin-web/src/components/features/dashboard/RegionDistribution/RegionDistribution.module.scss b/frontend/admin-web/src/components/features/dashboard/RegionDistribution/RegionDistribution.module.scss new file mode 100644 index 00000000..6876d588 --- /dev/null +++ b/frontend/admin-web/src/components/features/dashboard/RegionDistribution/RegionDistribution.module.scss @@ -0,0 +1,53 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.regionDistribution { + height: 100%; + + &__content { + display: flex; + align-items: center; + gap: $spacing-xxl; + + @include respond-below(md) { + flex-direction: column; + } + } + + &__chart { + flex: 1; + min-width: 250px; + } + + &__legend { + display: flex; + flex-direction: column; + gap: $spacing-md; + min-width: 200px; + } + + &__legendItem { + @include flex-start; + gap: $spacing-sm; + } + + &__legendColor { + width: 12px; + height: 12px; + border-radius: $border-radius-sm; + flex-shrink: 0; + } + + &__legendLabel { + flex: 1; + font-size: $font-size-sm; + color: $text-primary; + } + + &__legendValue { + font-family: $font-family-number; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-primary; + } +} diff --git a/frontend/admin-web/src/components/features/dashboard/RegionDistribution/RegionDistribution.tsx b/frontend/admin-web/src/components/features/dashboard/RegionDistribution/RegionDistribution.tsx new file mode 100644 index 00000000..c5921687 --- /dev/null +++ b/frontend/admin-web/src/components/features/dashboard/RegionDistribution/RegionDistribution.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { FC } from 'react'; +import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts'; +import { Card } from '@/components/common'; +import styles from './RegionDistribution.module.scss'; + +interface RegionData { + region: string; + percentage: number; + color: string; +} + +export interface RegionDistributionProps { + data: RegionData[]; +} + +export const RegionDistribution: FC = ({ data }) => { + return ( + +
+
+ + + + {data.map((entry, index) => ( + + ))} + + [`${value}%`, '占比']} + contentStyle={{ + backgroundColor: '#fff', + border: '1px solid #E0E0E0', + borderRadius: 8, + boxShadow: '0 4px 12px rgba(0,0,0,0.1)', + }} + /> + + +
+
+ {data.map((item, index) => ( +
+ + {item.region} + {item.percentage}% +
+ ))} +
+
+
+ ); +}; diff --git a/frontend/admin-web/src/components/features/dashboard/RegionDistribution/index.ts b/frontend/admin-web/src/components/features/dashboard/RegionDistribution/index.ts new file mode 100644 index 00000000..a84d710a --- /dev/null +++ b/frontend/admin-web/src/components/features/dashboard/RegionDistribution/index.ts @@ -0,0 +1,2 @@ +export { RegionDistribution } from './RegionDistribution'; +export type { RegionDistributionProps } from './RegionDistribution'; diff --git a/frontend/admin-web/src/components/features/dashboard/StatCard/StatCard.module.scss b/frontend/admin-web/src/components/features/dashboard/StatCard/StatCard.module.scss new file mode 100644 index 00000000..669b7509 --- /dev/null +++ b/frontend/admin-web/src/components/features/dashboard/StatCard/StatCard.module.scss @@ -0,0 +1,63 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.statCard { + &__header { + @include flex-between; + margin-bottom: $spacing-base; + } + + &__title { + font-size: $font-size-sm; + color: $text-secondary; + } + + &__indicator { + width: 8px; + height: 8px; + border-radius: 50%; + } + + &__value { + @include flex-start; + align-items: baseline; + gap: $spacing-xs; + margin-bottom: $spacing-sm; + } + + &__number { + font-family: $font-family-number; + font-size: $font-size-display; + font-weight: $font-weight-bold; + color: $text-primary; + line-height: 1; + } + + &__suffix { + font-size: $font-size-sm; + color: $text-secondary; + } + + &__change { + @include flex-start; + gap: $spacing-xs; + font-size: $font-size-xs; + + &--up { + color: $increase-color; + } + + &--down { + color: $decrease-color; + } + } + + &__arrow { + font-weight: $font-weight-bold; + } + + &__period { + color: $text-disabled; + margin-left: $spacing-xs; + } +} diff --git a/frontend/admin-web/src/components/features/dashboard/StatCard/StatCard.tsx b/frontend/admin-web/src/components/features/dashboard/StatCard/StatCard.tsx new file mode 100644 index 00000000..c8292fbc --- /dev/null +++ b/frontend/admin-web/src/components/features/dashboard/StatCard/StatCard.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { FC } from 'react'; +import { Card } from '@/components/common'; +import { formatNumber, formatPercentage } from '@/utils/formatters'; +import styles from './StatCard.module.scss'; + +export interface StatCardProps { + title: string; + value: number | string; + suffix?: string; + change?: { + value: number; + trend: 'up' | 'down'; + }; + color?: string; +} + +export const StatCard: FC = ({ title, value, suffix = '', change, color }) => { + return ( + +
+ {title} + {color && ( + + )} +
+
+ + {typeof value === 'number' ? formatNumber(value) : value} + + {suffix && {suffix}} +
+ {change && ( +
+ + {change.trend === 'up' ? '↑' : '↓'} + + {formatPercentage(change.value)} + 较上月 +
+ )} +
+ ); +}; diff --git a/frontend/admin-web/src/components/features/dashboard/StatCard/index.ts b/frontend/admin-web/src/components/features/dashboard/StatCard/index.ts new file mode 100644 index 00000000..7b02dd6f --- /dev/null +++ b/frontend/admin-web/src/components/features/dashboard/StatCard/index.ts @@ -0,0 +1,2 @@ +export { StatCard } from './StatCard'; +export type { StatCardProps } from './StatCard'; diff --git a/frontend/admin-web/src/components/features/dashboard/TrendChart/TrendChart.module.scss b/frontend/admin-web/src/components/features/dashboard/TrendChart/TrendChart.module.scss new file mode 100644 index 00000000..58cf659d --- /dev/null +++ b/frontend/admin-web/src/components/features/dashboard/TrendChart/TrendChart.module.scss @@ -0,0 +1,16 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.trendChart { + height: 100%; + + &__ranges { + @include flex-start; + gap: $spacing-xs; + } + + &__chart { + width: 100%; + height: 300px; + } +} diff --git a/frontend/admin-web/src/components/features/dashboard/TrendChart/TrendChart.tsx b/frontend/admin-web/src/components/features/dashboard/TrendChart/TrendChart.tsx new file mode 100644 index 00000000..1ec840b4 --- /dev/null +++ b/frontend/admin-web/src/components/features/dashboard/TrendChart/TrendChart.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { FC, useState } from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart } from 'recharts'; +import { Card, Button } from '@/components/common'; +import styles from './TrendChart.module.scss'; + +interface TrendData { + date: string; + value: number; +} + +export interface TrendChartProps { + title: string; + data: TrendData[]; +} + +const timeRanges = [ + { label: '7天', value: '7d' }, + { label: '30天', value: '30d' }, + { label: '90天', value: '90d' }, +]; + +export const TrendChart: FC = ({ title, data }) => { + const [activeRange, setActiveRange] = useState('7d'); + + return ( + + {timeRanges.map((range) => ( + + ))} +
+ } + className={styles.trendChart} + > +
+ + + + + + + + + + + + + + + +
+ + ); +}; diff --git a/frontend/admin-web/src/components/features/dashboard/TrendChart/index.ts b/frontend/admin-web/src/components/features/dashboard/TrendChart/index.ts new file mode 100644 index 00000000..9e38ab3b --- /dev/null +++ b/frontend/admin-web/src/components/features/dashboard/TrendChart/index.ts @@ -0,0 +1,2 @@ +export { TrendChart } from './TrendChart'; +export type { TrendChartProps } from './TrendChart'; diff --git a/frontend/admin-web/src/components/layout/Header/Header.module.scss b/frontend/admin-web/src/components/layout/Header/Header.module.scss new file mode 100644 index 00000000..c583a5c5 --- /dev/null +++ b/frontend/admin-web/src/components/layout/Header/Header.module.scss @@ -0,0 +1,165 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.header { + @include flex-between; + height: $header-height; + padding: 0 $page-padding; + background-color: $card-background; + border-bottom: 1px solid $border-color; + + &__left { + @include flex-column; + gap: $spacing-xs; + } + + &__breadcrumb { + @include flex-start; + font-size: $font-size-sm; + } + + &__breadcrumbItem { + @include flex-start; + gap: $spacing-sm; + } + + &__breadcrumbLink { + color: $text-secondary; + @include transition-fast; + + &:hover { + color: $primary-color; + } + } + + &__breadcrumbCurrent { + color: $text-primary; + } + + &__breadcrumbSeparator { + color: $text-disabled; + } + + &__title { + font-size: $font-size-lg; + font-weight: $font-weight-semibold; + color: $text-primary; + margin: 0; + } + + &__right { + @include flex-start; + gap: $spacing-lg; + } + + &__actions { + @include flex-start; + gap: $spacing-sm; + } + + &__iconButton { + @include flex-center; + width: 40px; + height: 40px; + border-radius: $border-radius-base; + color: $text-secondary; + @include transition-fast; + + &:hover { + background-color: $background-color; + color: $text-primary; + } + + svg { + width: 20px; + height: 20px; + } + } + + &__user { + position: relative; + @include flex-start; + gap: $spacing-sm; + padding: $spacing-sm $spacing-md; + border-radius: $border-radius-base; + cursor: pointer; + @include transition-fast; + + &:hover { + background-color: $background-color; + + .header__dropdown { + opacity: 1; + visibility: visible; + transform: translateY(0); + } + } + } + + &__userInfo { + @include flex-column; + gap: 2px; + } + + &__userName { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-primary; + } + + &__userRole { + font-size: $font-size-xs; + color: $text-secondary; + } + + &__dropdown { + position: absolute; + top: 100%; + right: 0; + min-width: 180px; + margin-top: $spacing-xs; + padding: $spacing-sm; + background-color: $card-background; + border: 1px solid $border-color; + border-radius: $border-radius-base; + box-shadow: $shadow-lg; + opacity: 0; + visibility: hidden; + transform: translateY(-8px); + @include transition-fast; + z-index: $z-index-dropdown; + } + + &__dropdownItem { + @include flex-start; + gap: $spacing-sm; + width: 100%; + padding: $spacing-sm $spacing-md; + border-radius: $border-radius-sm; + font-size: $font-size-sm; + color: $text-primary; + @include transition-fast; + + &:hover { + background-color: $background-color; + } + + &--danger { + color: $error-color; + + &:hover { + background-color: rgba($error-color, 0.05); + } + } + + svg { + width: 16px; + height: 16px; + } + } + + &__dropdownDivider { + margin: $spacing-sm 0; + border-top: 1px solid $border-color; + } +} diff --git a/frontend/admin-web/src/components/layout/Header/Header.tsx b/frontend/admin-web/src/components/layout/Header/Header.tsx new file mode 100644 index 00000000..cd42cbc8 --- /dev/null +++ b/frontend/admin-web/src/components/layout/Header/Header.tsx @@ -0,0 +1,108 @@ +'use client'; + +import { FC, ReactNode } from 'react'; +import Link from 'next/link'; +import { cn } from '@/utils/helpers'; +import { useAppSelector, useAppDispatch } from '@/store/redux/hooks'; +import { logout } from '@/store/redux/slices/authSlice'; +import { Avatar, Badge } from '@/components/common'; +import styles from './Header.module.scss'; + +interface BreadcrumbItem { + label: string; + path?: string; +} + +export interface HeaderProps { + title?: string; + breadcrumb?: BreadcrumbItem[]; + actions?: ReactNode; +} + +export const Header: FC = ({ title, breadcrumb = [], actions }) => { + const dispatch = useAppDispatch(); + const user = useAppSelector((state) => state.auth.user); + const unreadCount = useAppSelector((state) => state.notification.unreadCount); + + const handleLogout = () => { + dispatch(logout()); + window.location.href = '/login'; + }; + + return ( +
+
+ {breadcrumb.length > 0 && ( + + )} + {title &&

{title}

} +
+ +
+ {actions &&
{actions}
} + + + +
+ +
+ {user?.nickname || '管理员'} + {user?.role || '超级管理员'} +
+
+ + +
+ +
+
+
+
+ ); +}; diff --git a/frontend/admin-web/src/components/layout/Header/index.ts b/frontend/admin-web/src/components/layout/Header/index.ts new file mode 100644 index 00000000..2479ea89 --- /dev/null +++ b/frontend/admin-web/src/components/layout/Header/index.ts @@ -0,0 +1,2 @@ +export { Header } from './Header'; +export type { HeaderProps } from './Header'; diff --git a/frontend/admin-web/src/components/layout/PageContainer/PageContainer.module.scss b/frontend/admin-web/src/components/layout/PageContainer/PageContainer.module.scss new file mode 100644 index 00000000..a7395404 --- /dev/null +++ b/frontend/admin-web/src/components/layout/PageContainer/PageContainer.module.scss @@ -0,0 +1,26 @@ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.pageContainer { + min-height: 100vh; + background-color: $background-color; + + &__main { + margin-left: $sidebar-width; + min-height: 100vh; + @include transition-base; + + &--collapsed { + margin-left: $sidebar-collapsed-width; + } + } + + &__content { + padding: $page-padding; + min-height: calc(100vh - $header-height); + + &--fullWidth { + padding: 0; + } + } +} diff --git a/frontend/admin-web/src/components/layout/PageContainer/PageContainer.tsx b/frontend/admin-web/src/components/layout/PageContainer/PageContainer.tsx new file mode 100644 index 00000000..a5b0e520 --- /dev/null +++ b/frontend/admin-web/src/components/layout/PageContainer/PageContainer.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { FC, ReactNode } from 'react'; +import { cn } from '@/utils/helpers'; +import { useAppSelector } from '@/store/redux/hooks'; +import { Sidebar } from '../Sidebar'; +import { Header, HeaderProps } from '../Header'; +import styles from './PageContainer.module.scss'; + +export interface PageContainerProps extends HeaderProps { + children: ReactNode; + className?: string; + fullWidth?: boolean; +} + +export const PageContainer: FC = ({ + children, + className, + fullWidth = false, + ...headerProps +}) => { + const collapsed = useAppSelector((state) => state.settings.sidebarCollapsed); + + return ( +
+ +
+
+
+ {children} +
+
+
+ ); +}; diff --git a/frontend/admin-web/src/components/layout/PageContainer/index.ts b/frontend/admin-web/src/components/layout/PageContainer/index.ts new file mode 100644 index 00000000..fdc4f878 --- /dev/null +++ b/frontend/admin-web/src/components/layout/PageContainer/index.ts @@ -0,0 +1,2 @@ +export { PageContainer } from './PageContainer'; +export type { PageContainerProps } from './PageContainer'; diff --git a/frontend/admin-web/src/components/layout/Sidebar/Sidebar.module.scss b/frontend/admin-web/src/components/layout/Sidebar/Sidebar.module.scss new file mode 100644 index 00000000..0c34e4b7 --- /dev/null +++ b/frontend/admin-web/src/components/layout/Sidebar/Sidebar.module.scss @@ -0,0 +1,222 @@ +/* 侧边栏样式 - 基于 UIPro Figma 设计 */ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.sidebar { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: $sidebar-width; + background-color: #005a9c; + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 13.9px 0; + box-sizing: border-box; + gap: 14px; + z-index: $z-index-fixed; + font-family: 'Noto Sans SC', $font-family-base; + @include transition-base; + + &--collapsed { + width: $sidebar-collapsed-width; + + .sidebar__header { + justify-content: center; + padding: 0 $spacing-sm; + } + + .sidebar__logoText { + display: none; + } + + .sidebar__toggle { + position: static; + } + + .sidebar__item { + justify-content: center; + padding: 8px; + } + + .sidebar__label { + display: none; + } + } + + &__header { + align-self: stretch; + display: flex; + align-items: center; + padding: 0 24px; + gap: 12px; + flex-shrink: 0; + } + + &__logo { + display: flex; + align-items: center; + gap: 12px; + } + + &__logoIcon { + height: 36px; + width: 30px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + + img { + width: 100%; + height: 100%; + object-fit: contain; + } + } + + &__logoText { + margin: 0; + position: relative; + font-size: 20px; + line-height: 28px; + font-weight: 700; + color: $white; + } + + &__navWrapper { + width: $sidebar-width; + position: relative; + flex-shrink: 0; + flex: 1; + display: flex; + flex-direction: column; + } + + &__nav { + width: 244px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 16px 4px 16px 16px; + box-sizing: border-box; + gap: 8px; + flex-shrink: 0; + } + + &__toggle { + position: absolute; + right: -12px; + top: 50%; + transform: translateY(-50%); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); + border-radius: 9999px; + width: 24px; + height: 24px; + background-color: $white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border: none; + @include transition-fast; + + &:hover { + background-color: #f3f4f6; + } + + svg { + width: 14px; + height: 14px; + color: #6b7280; + } + } + + &__item { + cursor: pointer; + border: none; + padding: 8px 16px; + background-color: transparent; + align-self: stretch; + border-radius: 8px; + display: flex; + align-items: center; + gap: 12px; + text-decoration: none; + @include transition-fast; + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } + + &--active { + background-color: rgba(255, 255, 255, 0.2); + + .sidebar__label { + font-weight: 700; + } + } + } + + &__icon { + height: 28px; + width: 24px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + img, svg { + width: 100%; + height: 100%; + color: $white; + } + } + + &__label { + position: relative; + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: $white; + text-align: left; + } + + &__bottomNav { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 16px; + flex-shrink: 0; + margin-top: auto; + } + + &__bottomMenu { + align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + &__logoutItem { + cursor: pointer; + border: none; + padding: 8px 16px; + background-color: transparent; + align-self: stretch; + border-radius: 8px; + display: flex; + align-items: center; + gap: 12px; + @include transition-fast; + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } + } +} diff --git a/frontend/admin-web/src/components/layout/Sidebar/Sidebar.tsx b/frontend/admin-web/src/components/layout/Sidebar/Sidebar.tsx new file mode 100644 index 00000000..c4e37607 --- /dev/null +++ b/frontend/admin-web/src/components/layout/Sidebar/Sidebar.tsx @@ -0,0 +1,136 @@ +'use client'; + +import { FC } from 'react'; +import Link from 'next/link'; +import Image from 'next/image'; +import { usePathname, useRouter } from 'next/navigation'; +import { cn } from '@/utils/helpers'; +import { useAppSelector, useAppDispatch } from '@/store/redux/hooks'; +import { toggleSidebar } from '@/store/redux/slices/settingsSlice'; +import { logoutService } from '@/services/authService'; +import { toast } from '@/components/common'; +import styles from './Sidebar.module.scss'; + +/** + * 侧边栏菜单项配置 + */ +interface MenuItem { + key: string; + icon: string; + label: string; + path: string; +} + +// 主导航菜单 +const topMenuItems: MenuItem[] = [ + { key: 'dashboard', icon: '/images/Container1.svg', label: '仪表板', path: '/dashboard' }, + { key: 'users', icon: '/images/Container2.svg', label: '用户管理', path: '/users' }, + { key: 'leaderboard', icon: '/images/Container3.svg', label: '龙虎榜', path: '/leaderboard' }, + { key: 'authorization', icon: '/images/Container4.svg', label: '授权管理', path: '/authorization' }, + { key: 'statistics', icon: '/images/Container5.svg', label: '数据统计', path: '/statistics' }, + { key: 'settings', icon: '/images/Container6.svg', label: '系统设置', path: '/settings' }, +]; + +// 底部导航菜单 +const bottomMenuItems: MenuItem[] = [ + { key: 'help', icon: '/images/Container7.svg', label: '帮助中心', path: '/help' }, +]; + +/** + * 侧边栏组件 + * 基于 UIPro Figma 设计实现 + */ +export const Sidebar: FC = () => { + const pathname = usePathname(); + const router = useRouter(); + const dispatch = useAppDispatch(); + const collapsed = useAppSelector((state) => state.settings.sidebarCollapsed); + + // 切换侧边栏展开/收起 + const handleToggle = () => { + dispatch(toggleSidebar()); + }; + + // 退出登录 + const handleLogout = () => { + // 清除所有上下文和存储 + logoutService.logout(); + toast.success('已安全退出登录'); + // 跳转到登录页 + router.push('/login'); + }; + + // 渲染菜单项 + const renderMenuItem = (item: MenuItem) => { + const isActive = pathname === item.path || pathname.startsWith(`${item.path}/`); + + return ( + + + {item.label} + + {!collapsed && {item.label}} + + ); + }; + + return ( + + ); +}; diff --git a/frontend/admin-web/src/components/layout/Sidebar/index.ts b/frontend/admin-web/src/components/layout/Sidebar/index.ts new file mode 100644 index 00000000..d440597f --- /dev/null +++ b/frontend/admin-web/src/components/layout/Sidebar/index.ts @@ -0,0 +1 @@ +export { Sidebar } from './Sidebar'; diff --git a/frontend/admin-web/src/components/layout/index.ts b/frontend/admin-web/src/components/layout/index.ts new file mode 100644 index 00000000..5c860c7d --- /dev/null +++ b/frontend/admin-web/src/components/layout/index.ts @@ -0,0 +1,7 @@ +// 布局组件统一导出 + +export { Sidebar } from './Sidebar'; +export { Header } from './Header'; +export type { HeaderProps } from './Header'; +export { PageContainer } from './PageContainer'; +export type { PageContainerProps } from './PageContainer'; diff --git a/frontend/admin-web/src/infrastructure/api/client.ts b/frontend/admin-web/src/infrastructure/api/client.ts new file mode 100644 index 00000000..8327bc12 --- /dev/null +++ b/frontend/admin-web/src/infrastructure/api/client.ts @@ -0,0 +1,42 @@ +import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'; +import { store } from '@/store/redux/store'; +import { logout } from '@/store/redux/slices/authSlice'; + +const apiClient = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// 请求拦截器 +apiClient.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + const token = store.getState().auth.token; + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// 响应拦截器 +apiClient.interceptors.response.use( + (response) => response.data, + (error: AxiosError) => { + if (error.response?.status === 401) { + store.dispatch(logout()); + // 在客户端环境中重定向 + if (typeof window !== 'undefined') { + window.location.href = '/login'; + } + } + return Promise.reject(error); + } +); + +export default apiClient; diff --git a/frontend/admin-web/src/infrastructure/api/endpoints.ts b/frontend/admin-web/src/infrastructure/api/endpoints.ts new file mode 100644 index 00000000..6e5d2340 --- /dev/null +++ b/frontend/admin-web/src/infrastructure/api/endpoints.ts @@ -0,0 +1,88 @@ +// API 端点定义 + +export const API_ENDPOINTS = { + // 认证 + AUTH: { + LOGIN: '/auth/login', + LOGOUT: '/auth/logout', + REFRESH: '/auth/refresh', + FORGOT_PASSWORD: '/auth/forgot-password', + REGISTER: '/auth/register', + }, + + // 用户管理 + USERS: { + LIST: '/users', + DETAIL: (id: string) => `/users/${id}`, + UPDATE: (id: string) => `/users/${id}`, + DELETE: (id: string) => `/users/${id}`, + EXPORT: '/users/export', + BATCH_UPDATE: '/users/batch', + }, + + // 龙虎榜 + LEADERBOARD: { + RANKINGS: '/leaderboard/rankings', + DAILY: '/leaderboard/daily', + WEEKLY: '/leaderboard/weekly', + MONTHLY: '/leaderboard/monthly', + SETTINGS: '/leaderboard/settings', + EXPORT: '/leaderboard/export', + }, + + // 授权管理 + AUTHORIZATION: { + PROVINCE_COMPANIES: '/authorization/province-companies', + CITY_COMPANIES: '/authorization/city-companies', + PROVINCE_RULES: '/authorization/province-rules', + CITY_RULES: '/authorization/city-rules', + ASSESSMENT_RULES: '/authorization/assessment-rules', + LADDER_TARGETS: '/authorization/ladder-targets', + LIMITS: '/authorization/limits', + AUTHORIZE: (id: string) => `/authorization/${id}/authorize`, + REVOKE: (id: string) => `/authorization/${id}/revoke`, + }, + + // 数据统计 + STATISTICS: { + OVERVIEW: '/statistics/overview', + TREND: '/statistics/trend', + REGION: '/statistics/region', + REGION_PROVINCE: '/statistics/region/province', + REGION_CITY: '/statistics/region/city', + LEADERBOARD: '/statistics/leaderboard', + OPERATIONS: '/statistics/operations', + REVENUE: '/statistics/revenue', + EXPORT: '/statistics/export', + }, + + // 系统设置 + SETTINGS: { + ALL: '/settings', + SETTLEMENT: '/settings/settlement', + LEADERBOARD: '/settings/leaderboard', + QUOTA: '/settings/quota', + ASSESSMENT: '/settings/assessment', + DISPLAY: '/settings/display', + SECURITY: '/settings/security', + ACCOUNTS: '/settings/accounts', + OPERATION_LOGS: '/settings/operation-logs', + }, + + // 帮助中心 + HELP: { + DOCUMENTS: '/help/documents', + DOCUMENT_DETAIL: (id: string) => `/help/documents/${id}`, + FAQ: '/help/faq', + SEARCH: '/help/search', + FEEDBACK: '/help/feedback', + }, + + // 仪表板 + DASHBOARD: { + OVERVIEW: '/dashboard/overview', + STATS: '/dashboard/stats', + ACTIVITIES: '/dashboard/activities', + CHARTS: '/dashboard/charts', + }, +} as const; diff --git a/frontend/admin-web/src/infrastructure/external/exportService.ts b/frontend/admin-web/src/infrastructure/external/exportService.ts new file mode 100644 index 00000000..92c9b739 --- /dev/null +++ b/frontend/admin-web/src/infrastructure/external/exportService.ts @@ -0,0 +1,85 @@ +import * as XLSX from 'xlsx'; + +interface ExportColumn { + key: string; + title: string; + width?: number; +} + +export const exportService = { + /** + * 导出数据为 Excel 文件 + */ + exportToExcel>( + data: T[], + columns: ExportColumn[], + filename: string + ): void { + // 转换数据格式 + const exportData = data.map((item) => { + const row: Record = {}; + columns.forEach((col) => { + row[col.title] = item[col.key]; + }); + return row; + }); + + // 创建工作簿 + const ws = XLSX.utils.json_to_sheet(exportData); + + // 设置列宽 + const colWidths = columns.map((col) => ({ + wch: col.width || 15, + })); + ws['!cols'] = colWidths; + + // 创建工作簿并添加工作表 + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, 'Sheet1'); + + // 导出文件 + XLSX.writeFile(wb, `${filename}.xlsx`); + }, + + /** + * 导出数据为 CSV 文件 + */ + exportToCSV>( + data: T[], + columns: ExportColumn[], + filename: string + ): void { + // 转换数据格式 + const exportData = data.map((item) => { + const row: Record = {}; + columns.forEach((col) => { + row[col.title] = item[col.key]; + }); + return row; + }); + + // 创建工作表 + const ws = XLSX.utils.json_to_sheet(exportData); + + // 转换为 CSV 字符串 + const csv = XLSX.utils.sheet_to_csv(ws); + + // 创建 Blob 并下载 + const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `${filename}.csv`; + link.click(); + URL.revokeObjectURL(link.href); + }, + + /** + * 下载文件 + */ + downloadFile(url: string, filename: string): void { + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + }, +}; diff --git a/frontend/admin-web/src/infrastructure/storage/localStorage.ts b/frontend/admin-web/src/infrastructure/storage/localStorage.ts new file mode 100644 index 00000000..c6a820f2 --- /dev/null +++ b/frontend/admin-web/src/infrastructure/storage/localStorage.ts @@ -0,0 +1,46 @@ +// localStorage 操作封装 + +const PREFIX = 'rwadurian_admin_'; + +export const localStorageService = { + get(key: string): T | null { + if (typeof window === 'undefined') return null; + try { + const item = localStorage.getItem(PREFIX + key); + return item ? JSON.parse(item) : null; + } catch { + return null; + } + }, + + set(key: string, value: T): void { + if (typeof window === 'undefined') return; + try { + localStorage.setItem(PREFIX + key, JSON.stringify(value)); + } catch (error) { + console.error('localStorage set error:', error); + } + }, + + remove(key: string): void { + if (typeof window === 'undefined') return; + localStorage.removeItem(PREFIX + key); + }, + + clear(): void { + if (typeof window === 'undefined') return; + Object.keys(localStorage) + .filter((key) => key.startsWith(PREFIX)) + .forEach((key) => localStorage.removeItem(key)); + }, +}; + +// 常用 key +export const STORAGE_KEYS = { + TOKEN: 'token', + USER: 'user', + SETTINGS: 'settings', + SIDEBAR_COLLAPSED: 'sidebar_collapsed', + THEME: 'theme', + LANGUAGE: 'language', +} as const; diff --git a/frontend/admin-web/src/infrastructure/storage/sessionStorage.ts b/frontend/admin-web/src/infrastructure/storage/sessionStorage.ts new file mode 100644 index 00000000..06a8a736 --- /dev/null +++ b/frontend/admin-web/src/infrastructure/storage/sessionStorage.ts @@ -0,0 +1,43 @@ +// sessionStorage 操作封装 + +const PREFIX = 'rwadurian_admin_'; + +export const sessionStorageService = { + get(key: string): T | null { + if (typeof window === 'undefined') return null; + try { + const item = sessionStorage.getItem(PREFIX + key); + return item ? JSON.parse(item) : null; + } catch { + return null; + } + }, + + set(key: string, value: T): void { + if (typeof window === 'undefined') return; + try { + sessionStorage.setItem(PREFIX + key, JSON.stringify(value)); + } catch (error) { + console.error('sessionStorage set error:', error); + } + }, + + remove(key: string): void { + if (typeof window === 'undefined') return; + sessionStorage.removeItem(PREFIX + key); + }, + + clear(): void { + if (typeof window === 'undefined') return; + Object.keys(sessionStorage) + .filter((key) => key.startsWith(PREFIX)) + .forEach((key) => sessionStorage.removeItem(key)); + }, +}; + +// 常用 key +export const SESSION_KEYS = { + CURRENT_PAGE: 'current_page', + FILTERS: 'filters', + SCROLL_POSITION: 'scroll_position', +} as const; diff --git a/frontend/admin-web/src/services/authService.ts b/frontend/admin-web/src/services/authService.ts new file mode 100644 index 00000000..4bf2b7c7 --- /dev/null +++ b/frontend/admin-web/src/services/authService.ts @@ -0,0 +1,65 @@ +/** + * 认证服务 + * 处理登录、登出等认证相关操作 + */ + +import { localStorageService, STORAGE_KEYS } from '@/infrastructure/storage/localStorage'; +import { sessionStorageService } from '@/infrastructure/storage/sessionStorage'; +import { store } from '@/store/redux/store'; +import { logout as logoutAction } from '@/store/redux/slices/authSlice'; +import { useUserFiltersStore } from '@/store/zustand/useUserFiltersStore'; +import { useLeaderboardStore } from '@/store/zustand/useLeaderboardStore'; +import { useModalStore } from '@/store/zustand/useModalStore'; + +/** + * 安全退出登录 + * 清除所有用户相关的上下文和存储 + */ +export const logoutService = { + /** + * 执行完整的退出登录流程 + */ + logout: () => { + // 1. 清除 Redux 状态 + store.dispatch(logoutAction()); + + // 2. 清除 Zustand stores + useUserFiltersStore.getState().resetFilters(); + useLeaderboardStore.setState({ + activeBoard: 'daily', + virtualSettings: { + enabled: false, + virtualAccountCount: 0, + ruleDescription: '', + }, + displaySettings: { + frontendDisplayCount: 10, + }, + }); + useModalStore.getState().closeModal(); + + // 3. 清除 localStorage 中的认证信息 + localStorageService.remove(STORAGE_KEYS.TOKEN); + localStorageService.remove(STORAGE_KEYS.USER); + + // 4. 清除 sessionStorage 中的会话数据 + sessionStorageService.clear(); + }, + + /** + * 检查是否已登录 + */ + isAuthenticated: (): boolean => { + const token = localStorageService.get(STORAGE_KEYS.TOKEN); + return !!token; + }, + + /** + * 获取当前 token + */ + getToken: (): string | null => { + return localStorageService.get(STORAGE_KEYS.TOKEN); + }, +}; + +export default logoutService; diff --git a/frontend/admin-web/src/store/redux/hooks.ts b/frontend/admin-web/src/store/redux/hooks.ts new file mode 100644 index 00000000..f4f700f3 --- /dev/null +++ b/frontend/admin-web/src/store/redux/hooks.ts @@ -0,0 +1,6 @@ +import { useDispatch, useSelector, type TypedUseSelectorHook } from 'react-redux'; +import type { RootState, AppDispatch } from './store'; + +// 使用类型化的 hooks +export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/frontend/admin-web/src/store/redux/slices/authSlice.ts b/frontend/admin-web/src/store/redux/slices/authSlice.ts new file mode 100644 index 00000000..6100b229 --- /dev/null +++ b/frontend/admin-web/src/store/redux/slices/authSlice.ts @@ -0,0 +1,53 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { User } from '@/types/user.types'; + +interface AuthState { + user: User | null; + token: string | null; + isAuthenticated: boolean; + permissions: string[]; + loading: boolean; +} + +const initialState: AuthState = { + user: null, + token: null, + isAuthenticated: false, + permissions: [], + loading: true, +}; + +const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + setCredentials: ( + state, + action: PayloadAction<{ user: User; token: string; permissions?: string[] }> + ) => { + state.user = action.payload.user; + state.token = action.payload.token; + state.permissions = action.payload.permissions || []; + state.isAuthenticated = true; + state.loading = false; + }, + logout: (state) => { + state.user = null; + state.token = null; + state.permissions = []; + state.isAuthenticated = false; + state.loading = false; + }, + setLoading: (state, action: PayloadAction) => { + state.loading = action.payload; + }, + updateUser: (state, action: PayloadAction>) => { + if (state.user) { + state.user = { ...state.user, ...action.payload }; + } + }, + }, +}); + +export const { setCredentials, logout, setLoading, updateUser } = authSlice.actions; +export default authSlice.reducer; diff --git a/frontend/admin-web/src/store/redux/slices/notificationSlice.ts b/frontend/admin-web/src/store/redux/slices/notificationSlice.ts new file mode 100644 index 00000000..557d133c --- /dev/null +++ b/frontend/admin-web/src/store/redux/slices/notificationSlice.ts @@ -0,0 +1,71 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface Notification { + id: string; + type: 'info' | 'success' | 'warning' | 'error'; + title: string; + message: string; + read: boolean; + timestamp: string; +} + +interface NotificationState { + unreadCount: number; + notifications: Notification[]; +} + +const initialState: NotificationState = { + unreadCount: 0, + notifications: [], +}; + +const notificationSlice = createSlice({ + name: 'notification', + initialState, + reducers: { + addNotification: (state, action: PayloadAction>) => { + const newNotification: Notification = { + ...action.payload, + id: Date.now().toString(), + read: false, + }; + state.notifications.unshift(newNotification); + state.unreadCount += 1; + }, + markAsRead: (state, action: PayloadAction) => { + const notification = state.notifications.find((n) => n.id === action.payload); + if (notification && !notification.read) { + notification.read = true; + state.unreadCount = Math.max(0, state.unreadCount - 1); + } + }, + markAllAsRead: (state) => { + state.notifications.forEach((n) => { + n.read = true; + }); + state.unreadCount = 0; + }, + clearNotifications: (state) => { + state.notifications = []; + state.unreadCount = 0; + }, + removeNotification: (state, action: PayloadAction) => { + const index = state.notifications.findIndex((n) => n.id === action.payload); + if (index !== -1) { + if (!state.notifications[index].read) { + state.unreadCount = Math.max(0, state.unreadCount - 1); + } + state.notifications.splice(index, 1); + } + }, + }, +}); + +export const { + addNotification, + markAsRead, + markAllAsRead, + clearNotifications, + removeNotification, +} = notificationSlice.actions; +export default notificationSlice.reducer; diff --git a/frontend/admin-web/src/store/redux/slices/settingsSlice.ts b/frontend/admin-web/src/store/redux/slices/settingsSlice.ts new file mode 100644 index 00000000..23a8fb85 --- /dev/null +++ b/frontend/admin-web/src/store/redux/slices/settingsSlice.ts @@ -0,0 +1,35 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface SettingsState { + sidebarCollapsed: boolean; + theme: 'light' | 'dark'; + language: 'zh-CN' | 'en-US'; +} + +const initialState: SettingsState = { + sidebarCollapsed: false, + theme: 'light', + language: 'zh-CN', +}; + +const settingsSlice = createSlice({ + name: 'settings', + initialState, + reducers: { + toggleSidebar: (state) => { + state.sidebarCollapsed = !state.sidebarCollapsed; + }, + setSidebarCollapsed: (state, action: PayloadAction) => { + state.sidebarCollapsed = action.payload; + }, + setTheme: (state, action: PayloadAction<'light' | 'dark'>) => { + state.theme = action.payload; + }, + setLanguage: (state, action: PayloadAction<'zh-CN' | 'en-US'>) => { + state.language = action.payload; + }, + }, +}); + +export const { toggleSidebar, setSidebarCollapsed, setTheme, setLanguage } = settingsSlice.actions; +export default settingsSlice.reducer; diff --git a/frontend/admin-web/src/store/redux/store.ts b/frontend/admin-web/src/store/redux/store.ts new file mode 100644 index 00000000..3c3133a5 --- /dev/null +++ b/frontend/admin-web/src/store/redux/store.ts @@ -0,0 +1,16 @@ +import { configureStore } from '@reduxjs/toolkit'; +import authReducer from './slices/authSlice'; +import settingsReducer from './slices/settingsSlice'; +import notificationReducer from './slices/notificationSlice'; + +export const store = configureStore({ + reducer: { + auth: authReducer, + settings: settingsReducer, + notification: notificationReducer, + }, + devTools: process.env.NODE_ENV !== 'production', +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; diff --git a/frontend/admin-web/src/store/zustand/useLeaderboardStore.ts b/frontend/admin-web/src/store/zustand/useLeaderboardStore.ts new file mode 100644 index 00000000..5ee989b7 --- /dev/null +++ b/frontend/admin-web/src/store/zustand/useLeaderboardStore.ts @@ -0,0 +1,41 @@ +import { create } from 'zustand'; + +interface VirtualSettings { + enabled: boolean; + virtualAccountCount: number; + ruleDescription: string; +} + +interface DisplaySettings { + frontendDisplayCount: 10 | 20 | 50; +} + +interface LeaderboardStore { + activeBoard: 'daily' | 'weekly' | 'monthly'; + virtualSettings: VirtualSettings; + displaySettings: DisplaySettings; + setActiveBoard: (board: 'daily' | 'weekly' | 'monthly') => void; + updateVirtualSettings: (settings: Partial) => void; + updateDisplaySettings: (settings: Partial) => void; +} + +export const useLeaderboardStore = create((set) => ({ + activeBoard: 'daily', + virtualSettings: { + enabled: false, + virtualAccountCount: 0, + ruleDescription: '', + }, + displaySettings: { + frontendDisplayCount: 10, + }, + setActiveBoard: (board) => set({ activeBoard: board }), + updateVirtualSettings: (settings) => + set((state) => ({ + virtualSettings: { ...state.virtualSettings, ...settings }, + })), + updateDisplaySettings: (settings) => + set((state) => ({ + displaySettings: { ...state.displaySettings, ...settings }, + })), +})); diff --git a/frontend/admin-web/src/store/zustand/useModalStore.ts b/frontend/admin-web/src/store/zustand/useModalStore.ts new file mode 100644 index 00000000..e9fef18e --- /dev/null +++ b/frontend/admin-web/src/store/zustand/useModalStore.ts @@ -0,0 +1,23 @@ +import { create } from 'zustand'; + +interface ModalStore { + activeModal: string | null; + modalData: Record | null; + openModal: (name: string, data?: Record) => void; + closeModal: () => void; +} + +export const useModalStore = create((set) => ({ + activeModal: null, + modalData: null, + openModal: (name, data) => + set({ + activeModal: name, + modalData: data ?? null, + }), + closeModal: () => + set({ + activeModal: null, + modalData: null, + }), +})); diff --git a/frontend/admin-web/src/store/zustand/useUserFiltersStore.ts b/frontend/admin-web/src/store/zustand/useUserFiltersStore.ts new file mode 100644 index 00000000..47068853 --- /dev/null +++ b/frontend/admin-web/src/store/zustand/useUserFiltersStore.ts @@ -0,0 +1,39 @@ +import { create } from 'zustand'; + +interface UserFilters { + keyword: string; + province: string; + city: string; + status: string; + rankRange: [number, number] | null; + adoptionRange: [number, number] | null; +} + +interface UserFiltersStore { + filters: UserFilters; + setKeyword: (keyword: string) => void; + setFilters: (filters: Partial) => void; + resetFilters: () => void; +} + +const initialFilters: UserFilters = { + keyword: '', + province: '', + city: '', + status: '', + rankRange: null, + adoptionRange: null, +}; + +export const useUserFiltersStore = create((set) => ({ + filters: initialFilters, + setKeyword: (keyword) => + set((state) => ({ + filters: { ...state.filters, keyword }, + })), + setFilters: (newFilters) => + set((state) => ({ + filters: { ...state.filters, ...newFilters }, + })), + resetFilters: () => set({ filters: initialFilters }), +})); diff --git a/frontend/admin-web/src/styles/animations.scss b/frontend/admin-web/src/styles/animations.scss new file mode 100644 index 00000000..aa5bea5b --- /dev/null +++ b/frontend/admin-web/src/styles/animations.scss @@ -0,0 +1,202 @@ +// ============================================ +// 榴莲认种管理后台 - 动画定义 +// ============================================ + +@use 'variables' as *; + +// 淡入动画 +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +// 淡出动画 +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +// 向上滑入 +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +// 向下滑入 +@keyframes slideInDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +// 向左滑入 +@keyframes slideInLeft { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +// 向右滑入 +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +// 缩放进入 +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +// 缩放退出 +@keyframes scaleOut { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.95); + } +} + +// 弹跳动画 +@keyframes bounce { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-5px); + } +} + +// 脉冲动画 +@keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } +} + +// 旋转动画 +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +// 摇晃动画 +@keyframes shake { + 0%, 100% { + transform: translateX(0); + } + 10%, 30%, 50%, 70%, 90% { + transform: translateX(-5px); + } + 20%, 40%, 60%, 80% { + transform: translateX(5px); + } +} + +// 骨架屏闪烁 +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +// 动画类 +.animate-fadeIn { + animation: fadeIn $duration-base $ease-out; +} + +.animate-fadeOut { + animation: fadeOut $duration-base $ease-in; +} + +.animate-slideInUp { + animation: slideInUp $duration-base $ease-out; +} + +.animate-slideInDown { + animation: slideInDown $duration-base $ease-out; +} + +.animate-slideInLeft { + animation: slideInLeft $duration-base $ease-out; +} + +.animate-slideInRight { + animation: slideInRight $duration-base $ease-out; +} + +.animate-scaleIn { + animation: scaleIn $duration-fast $ease-out; +} + +.animate-scaleOut { + animation: scaleOut $duration-fast $ease-in; +} + +.animate-bounce { + animation: bounce 1s infinite; +} + +.animate-pulse { + animation: pulse 2s infinite; +} + +.animate-spin { + animation: spin 1s linear infinite; +} + +.animate-shake { + animation: shake 0.5s ease-in-out; +} diff --git a/frontend/admin-web/src/styles/mixins.scss b/frontend/admin-web/src/styles/mixins.scss new file mode 100644 index 00000000..f62babad --- /dev/null +++ b/frontend/admin-web/src/styles/mixins.scss @@ -0,0 +1,256 @@ +// ============================================ +// 榴莲认种管理后台 - SCSS Mixins +// ============================================ + +@use 'variables' as *; + +// 过渡动画 +@mixin transition-base { + transition: all $duration-base $ease-in-out; +} + +@mixin transition-fast { + transition: all $duration-fast $ease-in-out; +} + +@mixin transition-slow { + transition: all $duration-slow $ease-in-out; +} + +// 悬停抬起效果 +@mixin hover-lift { + transition: transform $duration-fast $ease-out, box-shadow $duration-fast $ease-out; + &:hover { + transform: translateY(-2px); + box-shadow: $shadow-md; + } +} + +// Flexbox 布局 +@mixin flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +@mixin flex-between { + display: flex; + align-items: center; + justify-content: space-between; +} + +@mixin flex-start { + display: flex; + align-items: center; + justify-content: flex-start; +} + +@mixin flex-end { + display: flex; + align-items: center; + justify-content: flex-end; +} + +@mixin flex-column { + display: flex; + flex-direction: column; +} + +@mixin flex-column-center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +// 文本溢出省略 +@mixin text-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@mixin text-ellipsis-multiline($lines: 2) { + display: -webkit-box; + -webkit-line-clamp: $lines; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +// 滚动条样式 +@mixin custom-scrollbar($width: 6px) { + &::-webkit-scrollbar { + width: $width; + height: $width; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: $border-color; + border-radius: $border-radius-pill; + + &:hover { + background-color: $text-disabled; + } + } +} + +// 卡片样式 +@mixin card-base { + background: $card-background; + border-radius: $border-radius-base; + box-shadow: $shadow-base; +} + +// 按钮基础样式 +@mixin button-base { + display: inline-flex; + align-items: center; + justify-content: center; + gap: $spacing-sm; + padding: $padding-button; + border: none; + border-radius: $border-radius-base; + font-family: $font-family-base; + font-size: $font-size-base; + font-weight: $font-weight-medium; + cursor: pointer; + @include transition-fast; + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } +} + +// 输入框基础样式 +@mixin input-base { + width: 100%; + padding: $padding-input; + border: 1px solid $border-color; + border-radius: $border-radius-base; + font-family: $font-family-base; + font-size: $font-size-base; + color: $text-primary; + background: $card-background; + @include transition-fast; + + &::placeholder { + color: $text-disabled; + } + + &:focus { + outline: none; + border-color: $primary-color; + box-shadow: 0 0 0 3px rgba($primary-color, 0.1); + } + + &:disabled { + background: $background-color; + cursor: not-allowed; + } +} + +// 绝对定位填充 +@mixin absolute-fill { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +// 固定定位填充 +@mixin fixed-fill { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +// 响应式断点 +$breakpoints: ( + sm: 640px, + md: 768px, + lg: 1024px, + xl: 1280px, + 2xl: 1536px +); + +@mixin respond-to($breakpoint) { + @if map-has-key($breakpoints, $breakpoint) { + @media (min-width: map-get($breakpoints, $breakpoint)) { + @content; + } + } @else { + @warn "Unknown breakpoint: #{$breakpoint}"; + } +} + +@mixin respond-below($breakpoint) { + @if map-has-key($breakpoints, $breakpoint) { + @media (max-width: map-get($breakpoints, $breakpoint) - 1px) { + @content; + } + } @else { + @warn "Unknown breakpoint: #{$breakpoint}"; + } +} + +// 隐藏滚动条但保持滚动功能 +@mixin hide-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } +} + +// 可点击区域扩大 +@mixin clickable-area($size: 8px) { + position: relative; + + &::before { + content: ''; + position: absolute; + top: -$size; + left: -$size; + right: -$size; + bottom: -$size; + } +} + +// 骨架屏动画 +@mixin skeleton-loading { + background: linear-gradient( + 90deg, + $background-color 25%, + lighten($background-color, 5%) 50%, + $background-color 75% + ); + background-size: 200% 100%; + animation: skeleton-loading 1.5s infinite; + + @keyframes skeleton-loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } + } +} + +// 聚焦环 +@mixin focus-ring { + &:focus-visible { + outline: 2px solid $primary-color; + outline-offset: 2px; + } +} diff --git a/frontend/admin-web/src/styles/reset.scss b/frontend/admin-web/src/styles/reset.scss new file mode 100644 index 00000000..89381b3c --- /dev/null +++ b/frontend/admin-web/src/styles/reset.scss @@ -0,0 +1,96 @@ +// ============================================ +// 榴莲认种管理后台 - 样式重置 +// ============================================ + +@use 'variables' as *; + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: $font-family-base; + font-size: $font-size-base; + line-height: $line-height-base; + color: $text-primary; + background-color: $background-color; +} + +a { + color: inherit; + text-decoration: none; +} + +ul, ol { + list-style: none; +} + +img, svg { + display: block; + max-width: 100%; + height: auto; +} + +button { + font-family: inherit; + cursor: pointer; + background: none; + border: none; +} + +input, textarea, select { + font-family: inherit; + font-size: inherit; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: $font-weight-semibold; +} + +// 去除 Chrome 自动填充背景色 +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus { + -webkit-box-shadow: 0 0 0 1000px $card-background inset; + transition: background-color 5000s ease-in-out 0s; +} + +// 去除数字输入框的箭头 +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; +} + +// 去除搜索框的清除按钮 +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-results-button, +input[type="search"]::-webkit-search-results-decoration { + -webkit-appearance: none; +} + +// 选择文本颜色 +::selection { + background-color: rgba($primary-color, 0.2); + color: $text-primary; +} diff --git a/frontend/admin-web/src/styles/typography.scss b/frontend/admin-web/src/styles/typography.scss new file mode 100644 index 00000000..49672e9b --- /dev/null +++ b/frontend/admin-web/src/styles/typography.scss @@ -0,0 +1,146 @@ +// ============================================ +// 榴莲认种管理后台 - 字体样式 +// ============================================ + +@use 'variables' as *; + +// 标题样式 +.heading-display { + font-family: $font-family-number; + font-size: $font-size-display; + font-weight: $font-weight-bold; + line-height: $line-height-tight; + color: $text-primary; +} + +.heading-xxl { + font-family: $font-family-base; + font-size: $font-size-xxl; + font-weight: $font-weight-semibold; + line-height: $line-height-tight; + color: $text-primary; +} + +.heading-xl { + font-family: $font-family-base; + font-size: $font-size-xl; + font-weight: $font-weight-semibold; + line-height: $line-height-tight; + color: $text-primary; +} + +.heading-lg { + font-family: $font-family-base; + font-size: $font-size-lg; + font-weight: $font-weight-semibold; + line-height: $line-height-base; + color: $text-primary; +} + +.heading-md { + font-family: $font-family-base; + font-size: $font-size-md; + font-weight: $font-weight-medium; + line-height: $line-height-base; + color: $text-primary; +} + +// 正文样式 +.text-base { + font-family: $font-family-base; + font-size: $font-size-base; + font-weight: $font-weight-regular; + line-height: $line-height-base; + color: $text-primary; +} + +.text-sm { + font-family: $font-family-base; + font-size: $font-size-sm; + font-weight: $font-weight-regular; + line-height: $line-height-base; + color: $text-secondary; +} + +.text-xs { + font-family: $font-family-base; + font-size: $font-size-xs; + font-weight: $font-weight-regular; + line-height: $line-height-base; + color: $text-secondary; +} + +// 数字样式 +.number-display { + font-family: $font-family-number; + font-size: $font-size-display; + font-weight: $font-weight-bold; + line-height: 1; + color: $text-primary; +} + +.number-lg { + font-family: $font-family-number; + font-size: $font-size-xxl; + font-weight: $font-weight-semibold; + line-height: 1; + color: $text-primary; +} + +.number-base { + font-family: $font-family-number; + font-size: $font-size-base; + font-weight: $font-weight-medium; + line-height: 1; + color: $text-primary; +} + +// 文本颜色 +.text-primary { + color: $text-primary; +} + +.text-secondary { + color: $text-secondary; +} + +.text-disabled { + color: $text-disabled; +} + +.text-success { + color: $success-color; +} + +.text-warning { + color: $warning-color; +} + +.text-error { + color: $error-color; +} + +.text-info { + color: $info-color; +} + +.text-increase { + color: $increase-color; +} + +.text-decrease { + color: $decrease-color; +} + +// 链接样式 +.link { + color: $primary-color; + text-decoration: none; + cursor: pointer; + transition: color $duration-fast $ease-in-out; + + &:hover { + color: $primary-light; + text-decoration: underline; + } +} diff --git a/frontend/admin-web/src/styles/variables.scss b/frontend/admin-web/src/styles/variables.scss new file mode 100644 index 00000000..dde1d029 --- /dev/null +++ b/frontend/admin-web/src/styles/variables.scss @@ -0,0 +1,119 @@ +// ============================================ +// 榴莲认种管理后台 - CSS 变量定义 +// ============================================ + +// 主色调 +$primary-color: #1565C0; +$primary-light: #1E88E5; +$primary-dark: #0D47A1; + +// 辅助色 +$success-color: #4CAF50; +$warning-color: #F5A623; +$error-color: #E53935; +$info-color: #2196F3; + +// 中性色 +$text-primary: #212121; +$text-secondary: #757575; +$text-disabled: #BDBDBD; +$border-color: #E0E0E0; +$background-color: #F5F5F5; +$card-background: #FFFFFF; +$sidebar-background: #1565C0; + +// 语义色 +$increase-color: #4CAF50; +$decrease-color: #E53935; + +// 字体族 +$font-family-base: 'PingFang SC', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, sans-serif; +$font-family-number: 'DIN Alternate', 'Roboto', $font-family-base; + +// 字号 +$font-size-xs: 12px; +$font-size-sm: 13px; +$font-size-base: 14px; +$font-size-md: 16px; +$font-size-lg: 18px; +$font-size-xl: 20px; +$font-size-xxl: 24px; +$font-size-display: 32px; + +// 行高 +$line-height-tight: 1.25; +$line-height-base: 1.5; +$line-height-loose: 1.75; + +// 字重 +$font-weight-regular: 400; +$font-weight-medium: 500; +$font-weight-semibold: 600; +$font-weight-bold: 700; + +// 间距 +$spacing-xs: 4px; +$spacing-sm: 8px; +$spacing-md: 12px; +$spacing-base: 16px; +$spacing-lg: 20px; +$spacing-xl: 24px; +$spacing-xxl: 32px; +$spacing-xxxl: 48px; + +// 组件内间距 +$padding-card: 24px; +$padding-modal: 24px; +$padding-input: 12px 16px; +$padding-button: 10px 24px; + +// 页面边距 +$page-padding: 24px; +$sidebar-width: 256px; +$sidebar-collapsed-width: 64px; +$header-height: 64px; + +// 圆角 +$border-radius-sm: 4px; +$border-radius-base: 8px; +$border-radius-lg: 12px; +$border-radius-xl: 16px; +$border-radius-round: 50%; +$border-radius-pill: 999px; + +// 阴影 +$shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); +$shadow-base: 0 2px 8px rgba(0, 0, 0, 0.08); +$shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1); +$shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); +$shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.15); + +// 动画时长 +$duration-fast: 150ms; +$duration-base: 250ms; +$duration-slow: 350ms; + +// 缓动函数 +$ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); +$ease-out: cubic-bezier(0, 0, 0.2, 1); +$ease-in: cubic-bezier(0.4, 0, 1, 1); + +// z-index 层级 +$z-index-dropdown: 1000; +$z-index-sticky: 1020; +$z-index-fixed: 1030; +$z-index-modal-backdrop: 1040; +$z-index-modal: 1050; +$z-index-popover: 1060; +$z-index-tooltip: 1070; +$z-index-toast: 1080; + +// 基础颜色 +$white: #FFFFFF; +$black: #000000; + +// 边框颜色变体 +$border-color-light: #F0F0F0; + +// 文本颜色 +$text-placeholder: #9E9E9E; diff --git a/frontend/admin-web/src/types/api.types.ts b/frontend/admin-web/src/types/api.types.ts new file mode 100644 index 00000000..79c894b7 --- /dev/null +++ b/frontend/admin-web/src/types/api.types.ts @@ -0,0 +1,33 @@ +// API 通用响应类型 + +export interface ApiResponse { + code: number; + message: string; + data: T; +} + +export interface PaginatedResponse { + code: number; + message: string; + data: { + list: T[]; + pagination: PaginationInfo; + }; +} + +export interface PaginationInfo { + current: number; + pageSize: number; + total: number; +} + +export interface PaginationParams { + page: number; + pageSize: number; +} + +export interface ApiError { + code: number; + message: string; + details?: Record; +} diff --git a/frontend/admin-web/src/types/common.types.ts b/frontend/admin-web/src/types/common.types.ts new file mode 100644 index 00000000..5379ffbd --- /dev/null +++ b/frontend/admin-web/src/types/common.types.ts @@ -0,0 +1,43 @@ +// 通用类型定义 + +export interface SelectOption { + label: string; + value: T; +} + +export interface MenuItem { + key: string; + icon: React.ReactNode; + label: string; + path: string; + badge?: number; + position?: 'top' | 'bottom'; +} + +export interface BreadcrumbItem { + label: string; + path?: string; +} + +export interface TableColumn { + key: string; + title: string; + width?: number | string; + sortable?: boolean; + align?: 'left' | 'center' | 'right'; + render?: (value: unknown, record: T, index: number) => React.ReactNode; +} + +export interface SortConfig { + key: string; + direction: 'asc' | 'desc'; +} + +export type ToastType = 'success' | 'error' | 'warning' | 'info'; + +export interface Toast { + id: string; + type: ToastType; + message: string; + duration?: number; +} diff --git a/frontend/admin-web/src/types/company.types.ts b/frontend/admin-web/src/types/company.types.ts new file mode 100644 index 00000000..b9b2dab4 --- /dev/null +++ b/frontend/admin-web/src/types/company.types.ts @@ -0,0 +1,43 @@ +// 公司相关类型定义 + +export type CompanyType = 'province' | 'city'; +export type AuthStatus = 'authorized' | 'pending' | 'unauthorized'; + +export interface Company { + id: string; + avatar: string; + nickname: string; + accountId: string; + type: CompanyType; + province: string; + city?: string; + teamAdoptions: number; + authStatus: AuthStatus; + authorizedAt?: string; +} + +export interface AssessmentRules { + firstTriggerThreshold: number; + stageUnitBenefit: number; + incrementalBenefit: number; + assessmentCycle: 'monthly' | 'quarterly'; + failureResetEnabled: boolean; +} + +export interface AuthorizationLimits { + oneProvinceOneCompany: boolean; + oneCityOneCompany: boolean; + topRankingRequired: number; + specialCityRules: { + beijing: boolean; + shanghai: boolean; + }; +} + +export interface LadderTarget { + assessmentMonth: number; + provinceMonthlyTarget: number; + provinceCumulativeTarget: number; + cityMonthlyTarget: number; + cityCumulativeTarget: number; +} diff --git a/frontend/admin-web/src/types/index.ts b/frontend/admin-web/src/types/index.ts new file mode 100644 index 00000000..515417b9 --- /dev/null +++ b/frontend/admin-web/src/types/index.ts @@ -0,0 +1,7 @@ +// 类型定义统一导出 + +export * from './api.types'; +export * from './user.types'; +export * from './company.types'; +export * from './statistics.types'; +export * from './common.types'; diff --git a/frontend/admin-web/src/types/statistics.types.ts b/frontend/admin-web/src/types/statistics.types.ts new file mode 100644 index 00000000..851b477f --- /dev/null +++ b/frontend/admin-web/src/types/statistics.types.ts @@ -0,0 +1,98 @@ +// 统计数据类型定义 + +export interface StatisticsOverview { + totalAdoptions: number; + todayAdoptions: number; + monthlyAdoptions: number; + activeUsers: number; + provincialCompanies: number; + cityCompanies: number; +} + +export interface TrendData { + date: string; + value: number; +} + +export interface MonthlyGrowthData { + month: string; + adoptions: number; + growth: number; +} + +export interface RegionDistributionData { + region: string; + percentage: number; + color: string; +} + +export interface LeaderboardStats { + activeTab: 'daily' | 'weekly' | 'monthly'; + rankings: RankingItem[]; + topProvince: { + name: string; + completionData: number; + }; + topCity: { + name: string; + completionData: number; + }; +} + +export interface RankingItem { + rank: number; + avatar: string; + nickname: string; + isVirtual: boolean; + adoptionCount: number; + teamData: string; + province?: string; + city?: string; +} + +export interface RegionStats { + region: string; + period: string; + adoptionCount: number; + percentage: number; +} + +export interface OperationStats { + summary: { + monthlyHashRate: string; + cumulativeHashRate: string; + monthlyMining: string; + cumulativeMining: string; + monthlyCommission: number; + cumulativeCommission: number; + monthlyAdoptionBonus: number; + }; + details: OperationDetail[]; +} + +export interface OperationDetail { + accountType: 'province' | 'city'; + accountName: string; + statMonth: string; + hashRate: string; + miningAmount: string; + commission: number; +} + +export interface RevenueDetail { + timestamp: string; + account: string; + source: 'mining' | 'adoption_completion' | 'share'; + amount: number; + relatedAddress: string; + transactionId: string; +} + +export interface ActivityItem { + id: string; + type: 'user_register' | 'company_activity' | 'system_update' | 'report_generated'; + icon: string; + title: string; + description: string; + timestamp: string; +} diff --git a/frontend/admin-web/src/types/user.types.ts b/frontend/admin-web/src/types/user.types.ts new file mode 100644 index 00000000..9e605d7e --- /dev/null +++ b/frontend/admin-web/src/types/user.types.ts @@ -0,0 +1,54 @@ +// 用户相关类型定义 + +export type UserRole = 'super_admin' | 'operator' | 'analyst'; + +export interface User { + id: string; + email: string; + username: string; + nickname: string; + avatar: string; + role: UserRole; + status: 'active' | 'inactive'; + createdAt: string; + lastLoginAt: string; +} + +export interface UserListItem { + accountId: string; + avatar: string; + nickname: string; + personalAdoptions: number; + teamAddresses: number; + teamAdoptions: number; + provincialAdoptions: { + count: number; + percentage: number; + }; + cityAdoptions: { + count: number; + percentage: number; + }; + referrerId: string; + ranking: number | null; + status: 'active' | 'inactive'; + isOnline: boolean; +} + +export interface UserFilters { + keyword?: string; + province?: string; + city?: string; + status?: 'active' | 'inactive'; + rankRange?: [number, number]; + adoptionRange?: [number, number]; +} + +export interface UserDetail extends UserListItem { + email: string; + phone: string; + registeredAt: string; + province: string; + city: string; + walletAddress: string; +} diff --git a/frontend/admin-web/src/utils/constants.ts b/frontend/admin-web/src/utils/constants.ts new file mode 100644 index 00000000..586bed0f --- /dev/null +++ b/frontend/admin-web/src/utils/constants.ts @@ -0,0 +1,156 @@ +// 应用常量定义 + +// 分页配置 +export const PAGINATION = { + DEFAULT_PAGE: 1, + DEFAULT_PAGE_SIZE: 10, + PAGE_SIZE_OPTIONS: [10, 20, 50, 100], +} as const; + +// 用户角色 +export const USER_ROLES = { + SUPER_ADMIN: 'super_admin', + OPERATOR: 'operator', + ANALYST: 'analyst', +} as const; + +export const USER_ROLE_LABELS: Record = { + super_admin: '超级管理员', + operator: '运营人员', + analyst: '数据分析员', +}; + +// 用户状态 +export const USER_STATUS = { + ACTIVE: 'active', + INACTIVE: 'inactive', +} as const; + +export const USER_STATUS_LABELS: Record = { + active: '活跃', + inactive: '停用', +}; + +// 授权状态 +export const AUTH_STATUS = { + AUTHORIZED: 'authorized', + PENDING: 'pending', + UNAUTHORIZED: 'unauthorized', +} as const; + +export const AUTH_STATUS_LABELS: Record = { + authorized: '已授权', + pending: '待授权', + unauthorized: '未授权', +}; + +// 考核周期 +export const ASSESSMENT_CYCLES = { + MONTHLY: 'monthly', + QUARTERLY: 'quarterly', +} as const; + +export const ASSESSMENT_CYCLE_LABELS: Record = { + monthly: '月度', + quarterly: '季度', +}; + +// 结算货币 +export const CURRENCIES = ['BNB', 'OG', 'USDT', 'DST'] as const; + +// 龙虎榜类型 +export const LEADERBOARD_TYPES = { + DAILY: 'daily', + WEEKLY: 'weekly', + MONTHLY: 'monthly', +} as const; + +export const LEADERBOARD_TYPE_LABELS: Record = { + daily: '日榜', + weekly: '周榜', + monthly: '月榜', +}; + +// 收益来源 +export const REVENUE_SOURCES = { + MINING: 'mining', + ADOPTION_COMPLETION: 'adoption_completion', + SHARE: 'share', +} as const; + +export const REVENUE_SOURCE_LABELS: Record = { + mining: '挖矿收益', + adoption_completion: '认种完成奖励', + share: '分享奖励', +}; + +// 活动类型 +export const ACTIVITY_TYPES = { + USER_REGISTER: 'user_register', + COMPANY_ACTIVITY: 'company_activity', + SYSTEM_UPDATE: 'system_update', + REPORT_GENERATED: 'report_generated', +} as const; + +export const ACTIVITY_TYPE_LABELS: Record = { + user_register: '用户注册', + company_activity: '公司动态', + system_update: '系统更新', + report_generated: '报表生成', +}; + +// 时间范围选项 +export const TIME_RANGE_OPTIONS = [ + { label: '7天', value: '7d' }, + { label: '30天', value: '30d' }, + { label: '90天', value: '90d' }, + { label: '本月', value: 'month' }, + { label: '本季度', value: 'quarter' }, + { label: '本年度', value: 'year' }, +] as const; + +// 省份列表 +export const PROVINCES = [ + '北京市', + '天津市', + '河北省', + '山西省', + '内蒙古自治区', + '辽宁省', + '吉林省', + '黑龙江省', + '上海市', + '江苏省', + '浙江省', + '安徽省', + '福建省', + '江西省', + '山东省', + '河南省', + '湖北省', + '湖南省', + '广东省', + '广西壮族自治区', + '海南省', + '重庆市', + '四川省', + '贵州省', + '云南省', + '西藏自治区', + '陕西省', + '甘肃省', + '青海省', + '宁夏回族自治区', + '新疆维吾尔自治区', + '台湾省', + '香港特别行政区', + '澳门特别行政区', +] as const; + +// 文档分类 +export const DOCUMENT_CATEGORIES = { + SYSTEM_SETTINGS: '系统设置', + ACCOUNT_SECURITY: '账号安全', + DATA_STATISTICS: '数据统计', + AUTHORIZATION_MANAGEMENT: '授权管理', +} as const; diff --git a/frontend/admin-web/src/utils/formatters.ts b/frontend/admin-web/src/utils/formatters.ts new file mode 100644 index 00000000..ca3cb2ed --- /dev/null +++ b/frontend/admin-web/src/utils/formatters.ts @@ -0,0 +1,104 @@ +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import 'dayjs/locale/zh-cn'; + +// 配置 dayjs +dayjs.extend(relativeTime); +dayjs.locale('zh-cn'); + +/** + * 格式化数字,添加千位分隔符 + */ +export function formatNumber(num: number | string, decimals = 0): string { + const n = typeof num === 'string' ? parseFloat(num) : num; + if (isNaN(n)) return '0'; + return n.toLocaleString('zh-CN', { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }); +} + +/** + * 格式化货币 + */ +export function formatCurrency(amount: number, currency = 'CNY'): string { + const symbols: Record = { + CNY: '¥', + USD: '$', + USDT: 'USDT ', + BTC: 'BTC ', + }; + const symbol = symbols[currency] || ''; + return `${symbol}${formatNumber(amount, 2)}`; +} + +/** + * 格式化百分比 + */ +export function formatPercentage(value: number, decimals = 1): string { + return `${value >= 0 ? '+' : ''}${value.toFixed(decimals)}%`; +} + +/** + * 格式化日期 + */ +export function formatDate(date: string | Date, format = 'YYYY-MM-DD'): string { + return dayjs(date).format(format); +} + +/** + * 格式化日期时间 + */ +export function formatDateTime(date: string | Date, format = 'YYYY-MM-DD HH:mm:ss'): string { + return dayjs(date).format(format); +} + +/** + * 格式化相对时间 (如: 5分钟前, 2小时前) + */ +export function formatRelativeTime(date: string | Date): string { + return dayjs(date).fromNow(); +} + +/** + * 格式化文件大小 + */ +export function formatFileSize(bytes: number): string { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; +} + +/** + * 截断文本 + */ +export function truncateText(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + return `${text.slice(0, maxLength)}...`; +} + +/** + * 格式化手机号 (隐藏中间4位) + */ +export function formatPhone(phone: string): string { + if (!phone || phone.length !== 11) return phone; + return `${phone.slice(0, 3)}****${phone.slice(7)}`; +} + +/** + * 格式化钱包地址 (显示前后几位) + */ +export function formatWalletAddress(address: string, startLen = 6, endLen = 4): string { + if (!address || address.length <= startLen + endLen) return address; + return `${address.slice(0, startLen)}...${address.slice(-endLen)}`; +} + +/** + * 格式化排名显示 + */ +export function formatRanking(rank: number | null): string { + if (rank === null || rank === 0) return '-'; + return `#${rank}`; +} diff --git a/frontend/admin-web/src/utils/helpers.ts b/frontend/admin-web/src/utils/helpers.ts new file mode 100644 index 00000000..fe21af9f --- /dev/null +++ b/frontend/admin-web/src/utils/helpers.ts @@ -0,0 +1,211 @@ +import { clsx, type ClassValue } from 'clsx'; + +/** + * 合并 className + */ +export function cn(...inputs: ClassValue[]): string { + return clsx(inputs); +} + +/** + * 延迟函数 + */ +export function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * 防抖函数 + */ +export function debounce) => ReturnType>( + func: T, + wait: number +): (...args: Parameters) => void { + let timeoutId: ReturnType | null = null; + + return function (this: ThisParameterType, ...args: Parameters) { + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(() => { + func.apply(this, args); + }, wait); + }; +} + +/** + * 节流函数 + */ +export function throttle) => ReturnType>( + func: T, + limit: number +): (...args: Parameters) => void { + let inThrottle: boolean; + + return function (this: ThisParameterType, ...args: Parameters) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => (inThrottle = false), limit); + } + }; +} + +/** + * 深拷贝 + */ +export function deepClone(obj: T): T { + if (obj === null || typeof obj !== 'object') { + return obj; + } + return JSON.parse(JSON.stringify(obj)); +} + +/** + * 生成唯一 ID + */ +export function generateId(): string { + return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; +} + +/** + * 获取对象的嵌套值 + */ +export function getNestedValue(obj: Record, path: string): T | undefined { + const keys = path.split('.'); + let result: unknown = obj; + + for (const key of keys) { + if (result === null || result === undefined) { + return undefined; + } + result = (result as Record)[key]; + } + + return result as T; +} + +/** + * 将对象转为查询字符串 + */ +export function objectToQueryString(obj: Record): string { + const params = new URLSearchParams(); + + Object.entries(obj).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + params.append(key, String(value)); + } + }); + + return params.toString(); +} + +/** + * 从查询字符串解析对象 + */ +export function queryStringToObject(query: string): Record { + const params = new URLSearchParams(query); + const result: Record = {}; + + params.forEach((value, key) => { + result[key] = value; + }); + + return result; +} + +/** + * 数组去重 + */ +export function uniqueArray(arr: T[], key?: keyof T): T[] { + if (key) { + const seen = new Set(); + return arr.filter((item) => { + const k = item[key]; + if (seen.has(k)) { + return false; + } + seen.add(k); + return true; + }); + } + return [...new Set(arr)]; +} + +/** + * 数组分组 + */ +export function groupBy(arr: T[], key: keyof T): Record { + return arr.reduce( + (groups, item) => { + const groupKey = String(item[key]); + if (!groups[groupKey]) { + groups[groupKey] = []; + } + groups[groupKey].push(item); + return groups; + }, + {} as Record + ); +} + +/** + * 复制文本到剪贴板 + */ +export async function copyToClipboard(text: string): Promise { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + // 降级方案 + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + const success = document.execCommand('copy'); + document.body.removeChild(textarea); + return success; + } +} + +/** + * 下载 Blob 文件 + */ +export function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +} + +/** + * 检查是否为移动设备 + */ +export function isMobile(): boolean { + if (typeof window === 'undefined') return false; + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +} + +/** + * 获取颜色列表(用于图表) + */ +export function getChartColors(): string[] { + return [ + '#1565C0', + '#4CAF50', + '#F5A623', + '#E53935', + '#9C27B0', + '#00BCD4', + '#FF9800', + '#795548', + '#607D8B', + '#3F51B5', + ]; +} diff --git a/frontend/admin-web/src/utils/index.ts b/frontend/admin-web/src/utils/index.ts new file mode 100644 index 00000000..073353db --- /dev/null +++ b/frontend/admin-web/src/utils/index.ts @@ -0,0 +1,6 @@ +// 工具函数统一导出 + +export * from './formatters'; +export * from './validators'; +export * from './constants'; +export * from './helpers'; diff --git a/frontend/admin-web/src/utils/validators.ts b/frontend/admin-web/src/utils/validators.ts new file mode 100644 index 00000000..bae4e9be --- /dev/null +++ b/frontend/admin-web/src/utils/validators.ts @@ -0,0 +1,113 @@ +/** + * 验证邮箱格式 + */ +export function isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +/** + * 验证密码强度 + * @returns 返回 0-4 表示强度等级 + */ +export function getPasswordStrength(password: string): number { + let strength = 0; + if (password.length >= 6) strength++; + if (password.length >= 8) strength++; + if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++; + if (/\d/.test(password)) strength++; + if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength++; + return Math.min(strength, 4); +} + +/** + * 验证手机号 + */ +export function isValidPhone(phone: string): boolean { + const phoneRegex = /^1[3-9]\d{9}$/; + return phoneRegex.test(phone); +} + +/** + * 验证密码格式 (6-20位) + */ +export function isValidPassword(password: string): boolean { + return password.length >= 6 && password.length <= 20; +} + +/** + * 验证用户名 (2-20位字母数字下划线) + */ +export function isValidUsername(username: string): boolean { + const usernameRegex = /^[a-zA-Z0-9_]{2,20}$/; + return usernameRegex.test(username); +} + +/** + * 验证数字范围 + */ +export function isInRange(value: number, min: number, max: number): boolean { + return value >= min && value <= max; +} + +/** + * 验证非空 + */ +export function isNotEmpty(value: string | null | undefined): boolean { + return value !== null && value !== undefined && value.trim() !== ''; +} + +/** + * 表单验证规则 + */ +export const validationRules = { + required: (message = '此字段为必填项') => ({ + validate: (value: string) => isNotEmpty(value), + message, + }), + + email: (message = '请输入有效的邮箱地址') => ({ + validate: isValidEmail, + message, + }), + + password: (message = '密码长度应为6-20位') => ({ + validate: isValidPassword, + message, + }), + + phone: (message = '请输入有效的手机号') => ({ + validate: isValidPhone, + message, + }), + + minLength: (min: number, message?: string) => ({ + validate: (value: string) => value.length >= min, + message: message || `最少输入${min}个字符`, + }), + + maxLength: (max: number, message?: string) => ({ + validate: (value: string) => value.length <= max, + message: message || `最多输入${max}个字符`, + }), + + range: (min: number, max: number, message?: string) => ({ + validate: (value: number) => isInRange(value, min, max), + message: message || `请输入${min}-${max}之间的数字`, + }), +}; + +/** + * 验证单个字段 + */ +export function validateField( + value: unknown, + rules: Array<{ validate: (v: unknown) => boolean; message: string }> +): string | null { + for (const rule of rules) { + if (!rule.validate(value)) { + return rule.message; + } + } + return null; +} diff --git a/frontend/admin-web/tsconfig.json b/frontend/admin-web/tsconfig.json new file mode 100644 index 00000000..defe37e2 --- /dev/null +++ b/frontend/admin-web/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./src/*" + ] + }, + "baseUrl": ".", + "target": "ES2017" + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +}