PC Web App draft 0.1 Okay.

This commit is contained in:
hailin 2025-11-26 01:58:34 -08:00
parent 2915c4ccc5
commit f6ede89b18
194 changed files with 20783 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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"
}
}

49
frontend/admin-web/.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,11 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}

View File

@ -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;

6507
frontend/admin-web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.375 3.64062H12.8281V2.54688H11.7344V3.64062H6.26562V2.54688H5.17188V3.64062H4.625C4.02344 3.64062 3.53125 4.13281 3.53125 4.73438V13.4844C3.53125 14.0859 4.02344 14.5781 4.625 14.5781H13.375C13.9766 14.5781 14.4688 14.0859 14.4688 13.4844V4.73438C14.4688 4.13281 13.9766 3.64062 13.375 3.64062ZM13.375 13.4844H4.625V6.375H13.375V13.4844Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@ -0,0 +1,30 @@
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_dd_9_2072)">
<mask id="path-1-inside-1_9_2072" fill="white">
<path d="M5 17C5 8.16344 12.1634 1 21 1C29.8366 1 37 8.16344 37 17C37 25.8366 29.8366 33 21 33C12.1634 33 5 25.8366 5 17Z"/>
</mask>
<path d="M5 17C5 8.16344 12.1634 1 21 1C29.8366 1 37 8.16344 37 17C37 25.8366 29.8366 33 21 33C12.1634 33 5 25.8366 5 17Z" fill="white"/>
<path d="M21 33V32C12.7157 32 6 25.2843 6 17H5H4C4 26.3888 11.6112 34 21 34V33ZM37 17H36C36 25.2843 29.2843 32 21 32V33V34C30.3888 34 38 26.3888 38 17H37ZM21 1V2C29.2843 2 36 8.71573 36 17H37H38C38 7.61116 30.3888 0 21 0V1ZM21 1V0C11.6112 0 4 7.61116 4 17H5H6C6 8.71573 12.7157 2 21 2V1Z" fill="#E5E7EB" mask="url(#path-1-inside-1_9_2072)"/>
<path d="M22.5 21.5L18 17L22.5 12.5L23.55 13.55L20.1 17L23.55 20.45L22.5 21.5Z" fill="#6B7280"/>
</g>
<defs>
<filter id="filter0_dd_9_2072" x="0" y="0" width="42" height="42" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="2" operator="erode" in="SourceAlpha" result="effect1_dropShadow_9_2072"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_9_2072"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="1" operator="erode" in="SourceAlpha" result="effect2_dropShadow_9_2072"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="3"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_9_2072" result="effect2_dropShadow_9_2072"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_9_2072" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,3 @@
<svg width="30" height="40" viewBox="0 0 30 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.5 26.75H11.25V17L16.5 11.75L17.4375 12.6875C17.525 12.775 17.5969 12.8937 17.6531 13.0437C17.7094 13.1937 17.7375 13.3375 17.7375 13.475V13.7375L16.9125 17H21.75C22.15 17 22.5 17.15 22.8 17.45C23.1 17.75 23.25 18.1 23.25 18.5V20C23.25 20.0875 23.2375 20.1812 23.2125 20.2812C23.1875 20.3812 23.1625 20.475 23.1375 20.5625L20.8875 25.85C20.775 26.1 20.5875 26.3125 20.325 26.4875C20.0625 26.6625 19.7875 26.75 19.5 26.75ZM12.75 25.25H19.5L21.75 20V18.5H15L16.0125 14.375L12.75 17.6375V25.25ZM12.75 17.6375V18.5V20V25.25V17.6375ZM11.25 17V18.5H9V25.25H11.25V26.75H7.5V17H11.25Z" fill="#16A34A"/>
</svg>

After

Width:  |  Height:  |  Size: 710 B

View File

@ -0,0 +1,3 @@
<svg width="30" height="40" viewBox="0 0 30 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 13.25H18.75V23L13.5 28.25L12.5625 27.3125C12.475 27.225 12.4031 27.1062 12.3469 26.9562C12.2906 26.8062 12.2625 26.6625 12.2625 26.525V26.2625L13.0875 23H8.25C7.85 23 7.5 22.85 7.2 22.55C6.9 22.25 6.75 21.9 6.75 21.5V20C6.75 19.9125 6.7625 19.8187 6.7875 19.7188C6.8125 19.6187 6.8375 19.525 6.8625 19.4375L9.1125 14.15C9.225 13.9 9.4125 13.6875 9.675 13.5125C9.9375 13.3375 10.2125 13.25 10.5 13.25ZM17.25 14.75H10.5L8.25 20V21.5H15L13.9875 25.625L17.25 22.3625V14.75ZM17.25 22.3625V21.5V20V14.75V22.3625ZM18.75 23V21.5H21V14.75H18.75V13.25H22.5V23H18.75Z" fill="#EF4444"/>
</svg>

After

Width:  |  Height:  |  Size: 693 B

View File

@ -0,0 +1,3 @@
<svg width="36" height="44" viewBox="0 0 36 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.925 37H15.075V31H4.5L10.5 22H7.5L18 7L28.5 22H25.5L31.5 31H20.925V37ZM10.125 28H16.125H13.275H18H22.725H19.875H25.875H10.125ZM10.125 28H25.875L19.875 19H22.725L18 12.25L13.275 19H16.125L10.125 28Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.9822 11.0833V5.25H20.76V11.0833H12.9822ZM3.26 14.9722V5.25H11.0378V14.9722H3.26ZM12.9822 22.75V13.0278H20.76V22.75H12.9822ZM3.26 22.75V16.9167H11.0378V22.75H3.26ZM5.20444 13.0278H9.09333V7.19444H5.20444V13.0278ZM14.9267 20.8056H18.8156V14.9722H14.9267V20.8056ZM14.9267 9.13889H18.8156V7.19444H14.9267V9.13889ZM5.20444 20.8056H9.09333V18.8611H5.20444V20.8056Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 491 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="24" viewBox="0 0 16 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 18H4.66668V9.33333L9.33334 4.66667L10.1667 5.5C10.2445 5.57778 10.3083 5.68333 10.3583 5.81667C10.4083 5.95 10.4333 6.07778 10.4333 6.2V6.43333L9.70001 9.33333H14C14.3556 9.33333 14.6667 9.46667 14.9333 9.73333C15.2 10 15.3333 10.3111 15.3333 10.6667V12C15.3333 12.0778 15.3222 12.1611 15.3 12.25C15.2778 12.3389 15.2556 12.4222 15.2333 12.5L13.2333 17.2C13.1333 17.4222 12.9667 17.6111 12.7333 17.7667C12.5 17.9222 12.2556 18 12 18ZM6.00001 16.6667H12L14 12V10.6667H8.00001L8.90001 7L6.00001 9.9V16.6667ZM6.00001 9.9V10.6667V12V16.6667V9.9ZM4.66668 9.33333V10.6667H2.66668V16.6667H4.66668V18H1.33334V9.33333H4.66668Z" fill="#0F172A"/>
</svg>

After

Width:  |  Height:  |  Size: 752 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="24" viewBox="0 0 16 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.00002 5.99992H11.3334V14.6666L6.66669 19.3333L5.83335 18.4999C5.75558 18.4221 5.69169 18.3166 5.64169 18.1833C5.59169 18.0499 5.56669 17.9221 5.56669 17.7999V17.5666L6.30002 14.6666H2.00002C1.64446 14.6666 1.33335 14.5333 1.06669 14.2666C0.80002 13.9999 0.666687 13.6888 0.666687 13.3333V11.9999C0.666687 11.9221 0.677798 11.8388 0.70002 11.7499C0.722243 11.661 0.744465 11.5777 0.766687 11.4999L2.76669 6.79992C2.86669 6.5777 3.03335 6.38881 3.26669 6.23325C3.50002 6.0777 3.74446 5.99992 4.00002 5.99992ZM10 7.33325H4.00002L2.00002 11.9999V13.3333H8.00002L7.10002 16.9999L10 14.0999V7.33325ZM10 14.0999V13.3333V11.9999V7.33325V14.0999ZM11.3334 14.6666V13.3333H13.3334V7.33325H11.3334V5.99992H14.6667V14.6666H11.3334Z" fill="#0F172A"/>
</svg>

After

Width:  |  Height:  |  Size: 852 B

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.31556 21.7778V19.0556C1.31556 18.5047 1.45734 17.9983 1.7409 17.5365C2.02447 17.0747 2.4012 16.7223 2.87111 16.4792C3.87574 15.9769 4.89657 15.6002 5.93361 15.349C6.97065 15.0979 8.02389 14.9723 9.09333 14.9723C10.1628 14.9723 11.216 15.0979 12.2531 15.349C13.2901 15.6002 14.3109 15.9769 15.3156 16.4792C15.7855 16.7223 16.1622 17.0747 16.4458 17.5365C16.7293 17.9983 16.8711 18.5047 16.8711 19.0556V21.7778H1.31556ZM18.8156 21.7778V18.8612C18.8156 18.1482 18.6171 17.4636 18.2201 16.8073C17.8231 16.1511 17.26 15.588 16.5308 15.1181C17.3572 15.2153 18.135 15.3814 18.8642 15.6164C19.5933 15.8513 20.2739 16.1389 20.9058 16.4792C21.4892 16.8033 21.9348 17.1638 22.2426 17.5608C22.5505 17.9578 22.7044 18.3913 22.7044 18.8612V21.7778H18.8156ZM9.09333 14.0001C8.02389 14.0001 7.10838 13.6193 6.34681 12.8577C5.58523 12.0961 5.20444 11.1806 5.20444 10.1112C5.20444 9.04172 5.58523 8.12621 6.34681 7.36464C7.10838 6.60306 8.02389 6.22228 9.09333 6.22228C10.1628 6.22228 11.0783 6.60306 11.8399 7.36464C12.6014 8.12621 12.9822 9.04172 12.9822 10.1112C12.9822 11.1806 12.6014 12.0961 11.8399 12.8577C11.0783 13.6193 10.1628 14.0001 9.09333 14.0001ZM18.8156 10.1112C18.8156 11.1806 18.4348 12.0961 17.6732 12.8577C16.9116 13.6193 15.9961 14.0001 14.9267 14.0001C14.7484 14.0001 14.5216 13.9798 14.2461 13.9393C13.9706 13.8988 13.7438 13.8542 13.5656 13.8056C14.0031 13.2871 14.3393 12.7119 14.5742 12.0799C14.8092 11.448 14.9267 10.7917 14.9267 10.1112C14.9267 9.43061 14.8092 8.77436 14.5742 8.14242C14.3393 7.51047 14.0031 6.93524 13.5656 6.41672C13.7924 6.3357 14.0193 6.28304 14.2461 6.25873C14.473 6.23443 14.6998 6.22228 14.9267 6.22228C15.9961 6.22228 16.9116 6.60306 17.6732 7.36464C18.4348 8.12621 18.8156 9.04172 18.8156 10.1112ZM3.26 19.8334H14.9267V19.0556C14.9267 18.8774 14.8821 18.7153 14.793 18.5695C14.7039 18.4237 14.5864 18.3102 14.4406 18.2292C13.5656 17.7917 12.6825 17.4636 11.7913 17.2448C10.9 17.0261 10.0007 16.9167 9.09333 16.9167C8.18593 16.9167 7.28662 17.0261 6.39542 17.2448C5.50421 17.4636 4.62111 17.7917 3.74611 18.2292C3.60028 18.3102 3.4828 18.4237 3.39368 18.5695C3.30456 18.7153 3.26 18.8774 3.26 19.0556V19.8334ZM9.09333 12.0556C9.62806 12.0556 10.0858 11.8652 10.4666 11.4844C10.8474 11.1036 11.0378 10.6459 11.0378 10.1112C11.0378 9.57644 10.8474 9.11869 10.4666 8.7379C10.0858 8.35711 9.62806 8.16672 9.09333 8.16672C8.55861 8.16672 8.10086 8.35711 7.72007 8.7379C7.33928 9.11869 7.14889 9.57644 7.14889 10.1112C7.14889 10.6459 7.33928 11.1036 7.72007 11.4844C8.10086 11.8652 8.55861 12.0556 9.09333 12.0556Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.14889 4.27772H16.8711V11.9097C16.8711 12.2824 16.7901 12.6145 16.6281 12.9062C16.466 13.1979 16.2392 13.4328 15.9475 13.6111L12.4961 15.6527L13.1767 17.8888H16.8711L13.8572 20.0277L15.0239 23.7222L12.01 21.4374L8.99611 23.7222L10.1628 20.0277L7.14889 17.8888H10.8433L11.5239 15.6527L8.0725 13.6111C7.78083 13.4328 7.55398 13.1979 7.39194 12.9062C7.22991 12.6145 7.14889 12.2824 7.14889 11.9097V4.27772ZM9.09333 6.22217V11.9097L11.0378 13.0763V6.22217H9.09333ZM14.9267 6.22217H12.9822V13.0763L14.9267 11.9097V6.22217Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 648 B

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.9892 17.4513L16.4822 11.9583L15.0968 10.5729L10.9892 14.6805L8.9475 12.6388L7.56208 14.0243L10.9892 17.4513ZM12.01 23.7222C9.75769 23.155 7.89831 21.8628 6.43188 19.8454C4.96544 17.8281 4.23222 15.5879 4.23222 13.1249V7.19439L12.01 4.27772L19.7878 7.19439V13.1249C19.7878 15.5879 19.0546 17.8281 17.5881 19.8454C16.1217 21.8628 14.2623 23.155 12.01 23.7222ZM12.01 21.6805C13.6952 21.1458 15.0887 20.0763 16.1906 18.4722C17.2924 16.868 17.8433 15.0856 17.8433 13.1249V8.5312L12.01 6.34369L6.17667 8.5312V13.1249C6.17667 15.0856 6.72759 16.868 7.82945 18.4722C8.9313 20.0763 10.3248 21.1458 12.01 21.6805Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 736 B

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.8989 21.7778V14.9723H19.7878V21.7778H15.8989ZM10.0656 21.7778V6.22228H13.9544V21.7778H10.0656ZM4.23222 21.7778V11.0834H8.12111V21.7778H4.23222Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.33639 23.7222L8.9475 20.6111C8.73685 20.53 8.53836 20.4328 8.35201 20.3194C8.16567 20.206 7.98338 20.0844 7.80514 19.9548L4.91278 21.1701L2.23917 16.552L4.74264 14.6562C4.72643 14.5428 4.71833 14.4334 4.71833 14.3281C4.71833 14.2227 4.71833 14.1134 4.71833 13.9999C4.71833 13.8865 4.71833 13.7771 4.71833 13.6718C4.71833 13.5665 4.72643 13.4571 4.74264 13.3437L2.23917 11.4479L4.91278 6.82981L7.80514 8.04508C7.98338 7.91545 8.16972 7.79393 8.36417 7.6805C8.55861 7.56708 8.75306 7.46985 8.9475 7.38883L9.33639 4.27772H14.6836L15.0725 7.38883C15.2831 7.46985 15.4816 7.56708 15.668 7.6805C15.8543 7.79393 16.0366 7.91545 16.2149 8.04508L19.1072 6.82981L21.7808 11.4479L19.2774 13.3437C19.2936 13.4571 19.3017 13.5665 19.3017 13.6718C19.3017 13.7771 19.3017 13.8865 19.3017 13.9999C19.3017 14.1134 19.3017 14.2227 19.3017 14.3281C19.3017 14.4334 19.2855 14.5428 19.2531 14.6562L21.7565 16.552L19.0829 21.1701L16.2149 19.9548C16.0366 20.0844 15.8503 20.206 15.6558 20.3194C15.4614 20.4328 15.2669 20.53 15.0725 20.6111L14.6836 23.7222H9.33639ZM11.0378 21.7777H12.9579L13.2982 19.2013C13.8005 19.0717 14.2664 18.8813 14.6958 18.6302C15.1252 18.379 15.5181 18.0752 15.8746 17.7187L18.2808 18.7152L19.2288 17.0624L17.1385 15.4826C17.2195 15.2557 17.2762 15.0167 17.3086 14.7656C17.341 14.5144 17.3572 14.2592 17.3572 13.9999C17.3572 13.7407 17.341 13.4855 17.3086 13.2343C17.2762 12.9832 17.2195 12.7442 17.1385 12.5173L19.2288 10.9374L18.2808 9.28467L15.8746 10.3055C15.5181 9.93282 15.1252 9.62089 14.6958 9.36974C14.2664 9.11858 13.8005 8.92819 13.2982 8.79856L12.9822 6.22217H11.0621L10.7218 8.79856C10.2195 8.92819 9.75363 9.11858 9.32424 9.36974C8.89484 9.62089 8.5019 9.92471 8.14542 10.2812L5.73917 9.28467L4.79125 10.9374L6.88153 12.493C6.80051 12.7361 6.7438 12.9791 6.71139 13.2222C6.67898 13.4652 6.66278 13.7245 6.66278 13.9999C6.66278 14.2592 6.67898 14.5104 6.71139 14.7534C6.7438 14.9965 6.80051 15.2395 6.88153 15.4826L4.79125 17.0624L5.73917 18.7152L8.14542 17.6944C8.5019 18.0671 8.89484 18.379 9.32424 18.6302C9.75363 18.8813 10.2195 19.0717 10.7218 19.2013L11.0378 21.7777ZM12.0586 17.4027C12.9984 17.4027 13.8005 17.0705 14.4649 16.4062C15.1292 15.7418 15.4614 14.9398 15.4614 13.9999C15.4614 13.0601 15.1292 12.258 14.4649 11.5937C13.8005 10.9293 12.9984 10.5972 12.0586 10.5972C11.1026 10.5972 10.2965 10.9293 9.64021 11.5937C8.98396 12.258 8.65583 13.0601 8.65583 13.9999C8.65583 14.9398 8.98396 15.7418 9.64021 16.4062C10.2965 17.0705 11.1026 17.4027 12.0586 17.4027Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.9614 19.8333C12.3017 19.8333 12.5893 19.7158 12.8242 19.4808C13.0592 19.2459 13.1767 18.9583 13.1767 18.618C13.1767 18.2777 13.0592 17.9901 12.8242 17.7552C12.5893 17.5202 12.3017 17.4027 11.9614 17.4027C11.6211 17.4027 11.3335 17.5202 11.0985 17.7552C10.8636 17.9901 10.7461 18.2777 10.7461 18.618C10.7461 18.9583 10.8636 19.2459 11.0985 19.4808C11.3335 19.7158 11.6211 19.8333 11.9614 19.8333ZM11.0864 16.0902H12.885C12.885 15.5555 12.9458 15.1342 13.0673 14.8263C13.1888 14.5185 13.5331 14.0972 14.1003 13.5624C14.5216 13.1411 14.8537 12.7401 15.0968 12.3593C15.3399 11.9785 15.4614 11.5208 15.4614 10.9861C15.4614 10.0786 15.1292 9.38189 14.4649 8.89578C13.8005 8.40967 13.0146 8.16661 12.1072 8.16661C11.1836 8.16661 10.4342 8.40967 9.85896 8.89578C9.28373 9.38189 8.88268 9.96522 8.65583 10.6458L10.26 11.2777C10.341 10.9861 10.5233 10.6701 10.8069 10.3298C11.0904 9.98953 11.5239 9.81939 12.1072 9.81939C12.6257 9.81939 13.0146 9.96117 13.2739 10.2447C13.5331 10.5283 13.6628 10.8402 13.6628 11.1805C13.6628 11.5046 13.5656 11.8084 13.3711 12.092C13.1767 12.3755 12.9336 12.6388 12.6419 12.8819C11.929 13.5138 11.4915 13.9918 11.3294 14.3159C11.1674 14.64 11.0864 15.2314 11.0864 16.0902ZM12.01 23.7222C10.6651 23.7222 9.4012 23.467 8.21833 22.9565C7.03546 22.4461 6.00653 21.7534 5.13153 20.8784C4.25653 20.0034 3.56382 18.9745 3.0534 17.7916C2.54299 16.6087 2.28778 15.3449 2.28778 13.9999C2.28778 12.655 2.54299 11.3911 3.0534 10.2083C3.56382 9.02541 4.25653 7.99647 5.13153 7.12147C6.00653 6.24647 7.03546 5.55376 8.21833 5.04335C9.4012 4.53293 10.6651 4.27772 12.01 4.27772C13.3549 4.27772 14.6188 4.53293 15.8017 5.04335C16.9845 5.55376 18.0135 6.24647 18.8885 7.12147C19.7635 7.99647 20.4562 9.02541 20.9666 10.2083C21.477 11.3911 21.7322 12.655 21.7322 13.9999C21.7322 15.3449 21.477 16.6087 20.9666 17.7916C20.4562 18.9745 19.7635 20.0034 18.8885 20.8784C18.0135 21.7534 16.9845 22.4461 15.8017 22.9565C14.6188 23.467 13.3549 23.7222 12.01 23.7222ZM12.01 21.7777C14.1813 21.7777 16.0204 21.0243 17.5274 19.5173C19.0343 18.0104 19.7878 16.1712 19.7878 13.9999C19.7878 11.8286 19.0343 9.98953 17.5274 8.48258C16.0204 6.97564 14.1813 6.22217 12.01 6.22217C9.8387 6.22217 7.99958 6.97564 6.49264 8.48258C4.98569 9.98953 4.23222 11.8286 4.23222 13.9999C4.23222 16.1712 4.98569 18.0104 6.49264 19.5173C7.99958 21.0243 9.8387 21.7777 12.01 21.7777Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.20444 22.75C4.66972 22.75 4.21197 22.5596 3.83118 22.1788C3.45039 21.798 3.26 21.3403 3.26 20.8056V7.19444C3.26 6.65972 3.45039 6.20197 3.83118 5.82118C4.21197 5.44039 4.66972 5.25 5.20444 5.25H12.01V7.19444H5.20444V20.8056H12.01V22.75H5.20444ZM15.8989 18.8611L14.5621 17.4514L17.0413 14.9722H9.09333V13.0278H17.0413L14.5621 10.5486L15.8989 9.13889L20.76 14L15.8989 18.8611Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 506 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="28" viewBox="0 0 20 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.3333 21.5L11.0833 16.25C10.6667 16.5833 10.1875 16.8472 9.64583 17.0417C9.10417 17.2361 8.52778 17.3333 7.91667 17.3333C6.40278 17.3333 5.12153 16.809 4.07292 15.7604C3.02431 14.7118 2.5 13.4306 2.5 11.9167C2.5 10.4028 3.02431 9.12153 4.07292 8.07292C5.12153 7.02431 6.40278 6.5 7.91667 6.5C9.43056 6.5 10.7118 7.02431 11.7604 8.07292C12.809 9.12153 13.3333 10.4028 13.3333 11.9167C13.3333 12.5278 13.2361 13.1042 13.0417 13.6458C12.8472 14.1875 12.5833 14.6667 12.25 15.0833L17.5 20.3333L16.3333 21.5ZM7.91667 15.6667C8.95833 15.6667 9.84375 15.3021 10.5729 14.5729C11.3021 13.8438 11.6667 12.9583 11.6667 11.9167C11.6667 10.875 11.3021 9.98958 10.5729 9.26042C9.84375 8.53125 8.95833 8.16667 7.91667 8.16667C6.875 8.16667 5.98958 8.53125 5.26042 9.26042C4.53125 9.98958 4.16667 10.875 4.16667 11.9167C4.16667 12.9583 4.53125 13.8438 5.26042 14.5729C5.98958 15.3021 6.875 15.6667 7.91667 15.6667Z" fill="#6B7280"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

View File

@ -0,0 +1,8 @@
<svg width="44" height="24" viewBox="0 0 44 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="44" height="24" rx="12" fill="#E5E7EB"/>
<mask id="path-2-inside-1_9_1658" fill="white">
<path d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12Z"/>
</mask>
<path d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12Z" fill="white"/>
<path d="M12 22V21C7.02944 21 3 16.9706 3 12H2H1C1 18.0751 5.92487 23 12 23V22ZM22 12H21C21 16.9706 16.9706 21 12 21V22V23C18.0751 23 23 18.0751 23 12H22ZM12 2V3C16.9706 3 21 7.02944 21 12H22H23C23 5.92487 18.0751 1 12 1V2ZM12 2V1C5.92487 1 1 5.92487 1 12H2H3C3 7.02944 7.02944 3 12 3V2Z" fill="#CBD5E1" mask="url(#path-2-inside-1_9_1658)"/>
</svg>

After

Width:  |  Height:  |  Size: 818 B

View File

@ -0,0 +1,8 @@
<svg width="36" height="20" viewBox="0 0 36 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="20" rx="10" fill="#0061A8"/>
<mask id="path-2-inside-1_9_1679" fill="white">
<path d="M18 10C18 5.58172 21.5817 2 26 2C30.4183 2 34 5.58172 34 10C34 14.4183 30.4183 18 26 18C21.5817 18 18 14.4183 18 10Z"/>
</mask>
<path d="M18 10C18 5.58172 21.5817 2 26 2C30.4183 2 34 5.58172 34 10C34 14.4183 30.4183 18 26 18C21.5817 18 18 14.4183 18 10Z" fill="white"/>
<path d="M26 18V17C22.134 17 19 13.866 19 10H18H17C17 14.9706 21.0294 19 26 19V18ZM34 10H33C33 13.866 29.866 17 26 17V18V19C30.9706 19 35 14.9706 35 10H34ZM26 2V3C29.866 3 33 6.13401 33 10H34H35C35 5.02944 30.9706 1 26 1V2ZM26 2V1C21.0294 1 17 5.02944 17 10H18H19C19 6.13401 22.134 3 26 3V2Z" fill="white" mask="url(#path-2-inside-1_9_1679)"/>
</svg>

After

Width:  |  Height:  |  Size: 828 B

View File

@ -0,0 +1,3 @@
<svg width="25" height="30" viewBox="0 0 25 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.7392 24.75C17.7137 24.75 15.7126 24.3084 13.7357 23.4253C11.7589 22.5422 9.96024 21.2905 8.33987 19.6701C6.7195 18.0498 5.46776 16.2512 4.58466 14.2743C3.70156 12.2975 3.26001 10.2963 3.26001 8.27083C3.26001 7.97917 3.35723 7.73611 3.55168 7.54167C3.74612 7.34722 3.98918 7.25 4.28084 7.25H8.21834C8.4452 7.25 8.64774 7.32697 8.82598 7.4809C9.00422 7.63484 9.10955 7.81713 9.14195 8.02778L9.7739 11.4306C9.80631 11.6898 9.7982 11.9086 9.74959 12.0868C9.70098 12.265 9.61186 12.419 9.48223 12.5486L7.12459 14.9306C7.44867 15.5301 7.83351 16.1094 8.27911 16.6684C8.72471 17.2274 9.21487 17.7662 9.74959 18.2847C10.2519 18.787 10.7785 19.2529 11.3295 19.6823C11.8804 20.1117 12.4637 20.5046 13.0795 20.8611L15.3642 18.5764C15.51 18.4306 15.7004 18.3212 15.9354 18.2483C16.1703 18.1753 16.4012 18.1551 16.6281 18.1875L19.9822 18.8681C20.2091 18.9329 20.3954 19.0503 20.5413 19.2205C20.6871 19.3906 20.76 19.581 20.76 19.7917V23.7292C20.76 24.0208 20.6628 24.2639 20.4683 24.4583C20.2739 24.6528 20.0308 24.75 19.7392 24.75ZM6.20098 13.0833L7.80515 11.4792L7.39195 9.19444H5.22876C5.30978 9.8588 5.4232 10.515 5.56904 11.1632C5.71487 11.8113 5.92552 12.4514 6.20098 13.0833ZM14.9024 21.7847C15.5343 22.0602 16.1784 22.2789 16.8347 22.441C17.4909 22.603 18.1512 22.7083 18.8156 22.7569V20.6181L16.5308 20.1562L14.9024 21.7847Z" fill="#0061A8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,3 @@
<svg width="25" height="30" viewBox="0 0 25 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.23223 23.7778C3.6975 23.7778 3.23975 23.5874 2.85896 23.2067C2.47817 22.8259 2.28778 22.3681 2.28778 21.8334V10.1667C2.28778 9.632 2.47817 9.17424 2.85896 8.79346C3.23975 8.41267 3.6975 8.22228 4.23223 8.22228H19.7878C20.3225 8.22228 20.7803 8.41267 21.161 8.79346C21.5418 9.17424 21.7322 9.632 21.7322 10.1667V21.8334C21.7322 22.3681 21.5418 22.8259 21.161 23.2067C20.7803 23.5874 20.3225 23.7778 19.7878 23.7778H4.23223ZM12.01 16.9723L4.23223 12.1112V21.8334H19.7878V12.1112L12.01 16.9723ZM12.01 15.0278L19.7878 10.1667H4.23223L12.01 15.0278Z" fill="#0061A8"/>
</svg>

After

Width:  |  Height:  |  Size: 678 B

View File

@ -0,0 +1,3 @@
<svg width="25" height="30" viewBox="0 0 25 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.0378 24.75V22.8056H18.8156V15.9028C18.8156 14.0069 18.1553 12.3987 16.8347 11.0781C15.5141 9.75752 13.9058 9.09722 12.01 9.09722C10.1142 9.09722 8.50595 9.75752 7.18535 11.0781C5.86475 12.3987 5.20445 14.0069 5.20445 15.9028V21.8333H4.23223C3.6975 21.8333 3.23975 21.6429 2.85896 21.2622C2.47817 20.8814 2.28778 20.4236 2.28778 19.8889V17.9444C2.28778 17.6042 2.37285 17.2841 2.54299 16.9844C2.71313 16.6846 2.95213 16.4456 3.26 16.2674L3.33292 14.9792C3.46255 13.8773 3.78257 12.8565 4.29299 11.9167C4.80341 10.9769 5.44345 10.1586 6.21313 9.46181C6.9828 8.76505 7.86591 8.22222 8.86243 7.83333C9.85896 7.44444 10.9082 7.25 12.01 7.25C13.1119 7.25 14.157 7.44444 15.1454 7.83333C16.1338 8.22222 17.0169 8.76099 17.7947 9.44965C18.5725 10.1383 19.2125 10.9525 19.7149 11.8924C20.2172 12.8322 20.5413 13.853 20.6871 14.9549L20.76 16.2188C21.0679 16.3646 21.3069 16.5833 21.477 16.875C21.6472 17.1667 21.7322 17.4745 21.7322 17.7986V20.0347C21.7322 20.3588 21.6472 20.6667 21.477 20.9583C21.3069 21.25 21.0679 21.4688 20.76 21.6146V22.8056C20.76 23.3403 20.5696 23.798 20.1888 24.1788C19.808 24.5596 19.3503 24.75 18.8156 24.75H11.0378ZM9.09334 17.9444C8.81787 17.9444 8.58697 17.8513 8.40063 17.6649C8.21429 17.4786 8.12111 17.2477 8.12111 16.9722C8.12111 16.6968 8.21429 16.4659 8.40063 16.2795C8.58697 16.0932 8.81787 16 9.09334 16C9.3688 16 9.5997 16.0932 9.78604 16.2795C9.97239 16.4659 10.0656 16.6968 10.0656 16.9722C10.0656 17.2477 9.97239 17.4786 9.78604 17.6649C9.5997 17.8513 9.3688 17.9444 9.09334 17.9444ZM14.9267 17.9444C14.6512 17.9444 14.4203 17.8513 14.234 17.6649C14.0476 17.4786 13.9544 17.2477 13.9544 16.9722C13.9544 16.6968 14.0476 16.4659 14.234 16.2795C14.4203 16.0932 14.6512 16 14.9267 16C15.2021 16 15.433 16.0932 15.6194 16.2795C15.8057 16.4659 15.8989 16.6968 15.8989 16.9722C15.8989 17.2477 15.8057 17.4786 15.6194 17.6649C15.433 17.8513 15.2021 17.9444 14.9267 17.9444ZM6.20098 16.4375C6.08755 14.7199 6.60607 13.2454 7.75653 12.0139C8.90699 10.7824 10.341 10.1667 12.0586 10.1667C13.5007 10.1667 14.7687 10.6244 15.8624 11.5399C16.9562 12.4554 17.6165 13.6262 17.8433 15.0521C16.3688 15.0359 15.0117 14.6389 13.7722 13.8611C12.5326 13.0833 11.5806 12.0301 10.9163 10.7014C10.657 11.9977 10.1101 13.1522 9.27563 14.1649C8.44114 15.1777 7.41625 15.9352 6.20098 16.4375Z" fill="#0061A8"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -0,0 +1,4 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" rx="32" fill="#F3F4F6"/>
<path d="M24.5 45.5V42.5H30.5V37.85C29.275 37.575 28.1813 37.0562 27.2188 36.2937C26.2563 35.5312 25.55 34.575 25.1 33.425C23.225 33.2 21.6563 32.3812 20.3938 30.9687C19.1312 29.5562 18.5 27.9 18.5 26V24.5C18.5 23.675 18.7937 22.9688 19.3813 22.3812C19.9688 21.7937 20.675 21.5 21.5 21.5H24.5V18.5H39.5V21.5H42.5C43.325 21.5 44.0313 21.7937 44.6188 22.3812C45.2063 22.9688 45.5 23.675 45.5 24.5V26C45.5 27.9 44.8688 29.5562 43.6063 30.9687C42.3438 32.3812 40.775 33.2 38.9 33.425C38.45 34.575 37.7438 35.5312 36.7812 36.2937C35.8188 37.0562 34.725 37.575 33.5 37.85V42.5H39.5V45.5H24.5ZM24.5 30.2V24.5H21.5V26C21.5 26.95 21.775 27.8062 22.325 28.5687C22.875 29.3312 23.6 29.875 24.5 30.2ZM32 35C33.25 35 34.3125 34.5625 35.1875 33.6875C36.0625 32.8125 36.5 31.75 36.5 30.5V21.5H27.5V30.5C27.5 31.75 27.9375 32.8125 28.8125 33.6875C29.6875 34.5625 30.75 35 32 35ZM39.5 30.2C40.4 29.875 41.125 29.3312 41.675 28.5687C42.225 27.8062 42.5 26.95 42.5 26V24.5H39.5V30.2Z" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" rx="32" fill="#F3F4F6"/>
<path d="M21.5 47C20.675 47 19.9688 46.7063 19.3813 46.1187C18.7937 45.5312 18.5 44.825 18.5 44V23C18.5 22.175 18.7937 21.4687 19.3813 20.8812C19.9688 20.2937 20.675 20 21.5 20H23V17H26V20H38V17H41V20H42.5C43.325 20 44.0313 20.2937 44.6188 20.8812C45.2063 21.4687 45.5 22.175 45.5 23V44C45.5 44.825 45.2063 45.5312 44.6188 46.1187C44.0313 46.7063 43.325 47 42.5 47H21.5ZM21.5 44H42.5V29H21.5V44ZM21.5 26H42.5V23H21.5V26ZM32 35C31.575 35 31.2188 34.8562 30.9313 34.5687C30.6438 34.2812 30.5 33.925 30.5 33.5C30.5 33.075 30.6438 32.7187 30.9313 32.4312C31.2188 32.1437 31.575 32 32 32C32.425 32 32.7813 32.1437 33.0688 32.4312C33.3563 32.7187 33.5 33.075 33.5 33.5C33.5 33.925 33.3563 34.2812 33.0688 34.5687C32.7813 34.8562 32.425 35 32 35ZM26 35C25.575 35 25.2188 34.8562 24.9313 34.5687C24.6438 34.2812 24.5 33.925 24.5 33.5C24.5 33.075 24.6438 32.7187 24.9313 32.4312C25.2188 32.1437 25.575 32 26 32C26.425 32 26.7812 32.1437 27.0688 32.4312C27.3563 32.7187 27.5 33.075 27.5 33.5C27.5 33.925 27.3563 34.2812 27.0688 34.5687C26.7812 34.8562 26.425 35 26 35ZM38 35C37.575 35 37.2188 34.8562 36.9313 34.5687C36.6438 34.2812 36.5 33.925 36.5 33.5C36.5 33.075 36.6438 32.7187 36.9313 32.4312C37.2188 32.1437 37.575 32 38 32C38.425 32 38.7812 32.1437 39.0688 32.4312C39.3563 32.7187 39.5 33.075 39.5 33.5C39.5 33.925 39.3563 34.2812 39.0688 34.5687C38.7812 34.8562 38.425 35 38 35ZM32 41C31.575 41 31.2188 40.8562 30.9313 40.5687C30.6438 40.2812 30.5 39.925 30.5 39.5C30.5 39.075 30.6438 38.7188 30.9313 38.4312C31.2188 38.1437 31.575 38 32 38C32.425 38 32.7813 38.1437 33.0688 38.4312C33.3563 38.7188 33.5 39.075 33.5 39.5C33.5 39.925 33.3563 40.2812 33.0688 40.5687C32.7813 40.8562 32.425 41 32 41ZM26 41C25.575 41 25.2188 40.8562 24.9313 40.5687C24.6438 40.2812 24.5 39.925 24.5 39.5C24.5 39.075 24.6438 38.7188 24.9313 38.4312C25.2188 38.1437 25.575 38 26 38C26.425 38 26.7812 38.1437 27.0688 38.4312C27.3563 38.7188 27.5 39.075 27.5 39.5C27.5 39.925 27.3563 40.2812 27.0688 40.5687C26.7812 40.8562 26.425 41 26 41ZM38 41C37.575 41 37.2188 40.8562 36.9313 40.5687C36.6438 40.2812 36.5 39.925 36.5 39.5C36.5 39.075 36.6438 38.7188 36.9313 38.4312C37.2188 38.1437 37.575 38 38 38C38.425 38 38.7812 38.1437 39.0688 38.4312C39.3563 38.7188 39.5 39.075 39.5 39.5C39.5 39.925 39.3563 40.2812 39.0688 40.5687C38.7812 40.8562 38.425 41 38 41Z" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,26 @@
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_dd_9_1124)">
<rect x="5" y="1" width="24" height="24" rx="12" fill="white"/>
<path d="M18.3334 17L14.3334 13L18.3334 9L19.2667 9.93333L16.2 13L19.2667 16.0667L18.3334 17Z" fill="#4B5563"/>
</g>
<defs>
<filter id="filter0_dd_9_1124" x="0" y="0" width="34" height="34" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="2" operator="erode" in="SourceAlpha" result="effect1_dropShadow_9_1124"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_9_1124"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="1" operator="erode" in="SourceAlpha" result="effect2_dropShadow_9_1124"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="3"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_9_1124" result="effect2_dropShadow_9_1124"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_9_1124" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

View File

@ -0,0 +1,3 @@
<svg width="36" height="44" viewBox="0 0 36 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.925 37H15.075V31H4.5L10.5 22H7.5L18 7L28.5 22H25.5L31.5 31H20.925V37ZM10.125 28H16.125H13.275H18H22.725H19.875H25.875H10.125ZM10.125 28H25.875L19.875 19H22.725L18 12.25L13.275 19H16.125L10.125 28Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.26001 13.0278V5.25H11.0378V13.0278H3.26001ZM3.26001 22.75V14.9722H11.0378V22.75H3.26001ZM12.9822 13.0278V5.25H20.76V13.0278H12.9822ZM12.9822 22.75V14.9722H20.76V22.75H12.9822ZM5.20445 11.0833H9.09334V7.19444H5.20445V11.0833ZM14.9267 11.0833H18.8156V7.19444H14.9267V11.0833ZM14.9267 20.8056H18.8156V16.9167H14.9267V20.8056ZM5.20445 20.8056H9.09334V16.9167H5.20445V20.8056Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 503 B

View File

@ -0,0 +1,3 @@
<svg width="37" height="37" viewBox="0 0 37 37" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31.3989 27.25L25.2739 21.125C24.7878 21.5139 24.2288 21.8218 23.5968 22.0486C22.9649 22.2755 22.2924 22.3889 21.5795 22.3889C19.8133 22.3889 18.3185 21.7772 17.0951 20.5538C15.8717 19.3304 15.26 17.8356 15.26 16.0694C15.26 14.3032 15.8717 12.8084 17.0951 11.5851C18.3185 10.3617 19.8133 9.75 21.5795 9.75C23.3457 9.75 24.8404 10.3617 26.0638 11.5851C27.2872 12.8084 27.8989 14.3032 27.8989 16.0694C27.8989 16.7824 27.7855 17.4549 27.5586 18.0868C27.3318 18.7188 27.0239 19.2778 26.635 19.7639L32.76 25.8889L31.3989 27.25ZM21.5795 20.4444C22.7947 20.4444 23.8277 20.0191 24.6784 19.1684C25.5291 18.3177 25.9545 17.2847 25.9545 16.0694C25.9545 14.8542 25.5291 13.8212 24.6784 12.9705C23.8277 12.1198 22.7947 11.6944 21.5795 11.6944C20.3642 11.6944 19.3312 12.1198 18.4805 12.9705C17.6298 13.8212 17.2045 14.8542 17.2045 16.0694C17.2045 17.2847 17.6298 18.3177 18.4805 19.1684C19.3312 20.0191 20.3642 20.4444 21.5795 20.4444Z" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,3 @@
<svg width="41" height="48" viewBox="0 0 41 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M35.3989 32.75L29.2739 26.625C28.7878 27.0139 28.2288 27.3218 27.5968 27.5486C26.9649 27.7755 26.2924 27.8889 25.5795 27.8889C23.8133 27.8889 22.3185 27.2772 21.0951 26.0538C19.8717 24.8304 19.26 23.3356 19.26 21.5694C19.26 19.8032 19.8717 18.3084 21.0951 17.0851C22.3185 15.8617 23.8133 15.25 25.5795 15.25C27.3457 15.25 28.8404 15.8617 30.0638 17.0851C31.2872 18.3084 31.8989 19.8032 31.8989 21.5694C31.8989 22.2824 31.7855 22.9549 31.5586 23.5868C31.3318 24.2188 31.0239 24.7778 30.635 25.2639L36.76 31.3889L35.3989 32.75ZM25.5795 25.9444C26.7947 25.9444 27.8277 25.5191 28.6784 24.6684C29.5291 23.8177 29.9545 22.7847 29.9545 21.5694C29.9545 20.3542 29.5291 19.3212 28.6784 18.4705C27.8277 17.6198 26.7947 17.1944 25.5795 17.1944C24.3642 17.1944 23.3312 17.6198 22.4805 18.4705C21.6298 19.3212 21.2045 20.3542 21.2045 21.5694C21.2045 22.7847 21.6298 23.8177 22.4805 24.6684C23.3312 25.5191 24.3642 25.9444 25.5795 25.9444Z" fill="#6B7280"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0101 17.2812L6.17676 11.4479L7.53787 10.0868L12.0101 14.559L16.4823 10.0868L17.8434 11.4479L12.0101 17.2812Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 243 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="24" viewBox="0 0 16 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.66667 15.3333H6V10.6667H4.66667V15.3333ZM7.33333 15.3333H8.66667V8.66667H7.33333V15.3333ZM10 15.3333H11.3333V12.6667H10V15.3333ZM3.33333 18C2.96667 18 2.65278 17.8694 2.39167 17.6083C2.13056 17.3472 2 17.0333 2 16.6667V7.33333C2 6.96667 2.13056 6.65278 2.39167 6.39167C2.65278 6.13056 2.96667 6 3.33333 6H12.6667C13.0333 6 13.3472 6.13056 13.6083 6.39167C13.8694 6.65278 14 6.96667 14 7.33333V16.6667C14 17.0333 13.8694 17.3472 13.6083 17.6083C13.3472 17.8694 13.0333 18 12.6667 18H3.33333ZM3.33333 16.6667H12.6667V7.33333H3.33333V16.6667Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 671 B

View File

@ -0,0 +1,10 @@
<svg width="248" height="257" viewBox="0 0 248 257" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.09241 214.084C19.896 214.084 19.896 123.972 38.6995 123.972C57.5031 123.972 57.5031 197.7 76.3068 197.7C95.11 197.7 95.11 136.26 113.914 136.26C132.717 136.26 132.717 148.548 151.521 148.548C170.324 148.548 170.324 226.372 189.128 226.372C207.932 226.372 207.932 1.09235 226.735 1.09235C245.539 1.09235 245.539 132.164 245.539 132.164V255.044H1.09241V214.084Z" fill="url(#paint0_linear_9_2741)"/>
<path d="M1.09241 214.084C19.896 214.084 19.896 123.972 38.6995 123.972C57.5031 123.972 57.5031 197.7 76.3068 197.7C95.11 197.7 95.11 136.26 113.914 136.26C132.717 136.26 132.717 148.548 151.521 148.548C170.324 148.548 170.324 226.372 189.128 226.372C207.932 226.372 207.932 1.09235 226.735 1.09235C245.539 1.09235 245.539 132.164 245.539 132.164" stroke="#005A9C" stroke-width="2.1847" stroke-linecap="round"/>
<defs>
<linearGradient id="paint0_linear_9_2741" x1="123.316" y1="1.09235" x2="123.316" y2="255.044" gradientUnits="userSpaceOnUse">
<stop stop-color="#005A9C" stop-opacity="0.3"/>
<stop offset="1" stop-color="#005A9C" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.31555 21.7778V19.0556C1.31555 18.5047 1.45733 17.9983 1.7409 17.5365C2.02446 17.0747 2.4012 16.7223 2.87111 16.4792C3.87574 15.9769 4.89657 15.6002 5.93361 15.349C6.97064 15.0979 8.02389 14.9723 9.09333 14.9723C10.1628 14.9723 11.216 15.0979 12.2531 15.349C13.2901 15.6002 14.3109 15.9769 15.3156 16.4792C15.7855 16.7223 16.1622 17.0747 16.4458 17.5365C16.7293 17.9983 16.8711 18.5047 16.8711 19.0556V21.7778H1.31555ZM18.8156 21.7778V18.8612C18.8156 18.1482 18.6171 17.4636 18.2201 16.8073C17.8231 16.1511 17.26 15.588 16.5308 15.1181C17.3572 15.2153 18.135 15.3814 18.8642 15.6164C19.5933 15.8513 20.2739 16.1389 20.9058 16.4792C21.4892 16.8033 21.9348 17.1638 22.2426 17.5608C22.5505 17.9578 22.7044 18.3913 22.7044 18.8612V21.7778H18.8156ZM9.09333 14.0001C8.02389 14.0001 7.10838 13.6193 6.3468 12.8577C5.58523 12.0961 5.20444 11.1806 5.20444 10.1112C5.20444 9.04172 5.58523 8.12621 6.3468 7.36464C7.10838 6.60306 8.02389 6.22228 9.09333 6.22228C10.1628 6.22228 11.0783 6.60306 11.8399 7.36464C12.6014 8.12621 12.9822 9.04172 12.9822 10.1112C12.9822 11.1806 12.6014 12.0961 11.8399 12.8577C11.0783 13.6193 10.1628 14.0001 9.09333 14.0001ZM18.8156 10.1112C18.8156 11.1806 18.4348 12.0961 17.6732 12.8577C16.9116 13.6193 15.9961 14.0001 14.9267 14.0001C14.7484 14.0001 14.5216 13.9798 14.2461 13.9393C13.9706 13.8988 13.7438 13.8542 13.5656 13.8056C14.0031 13.2871 14.3393 12.7119 14.5742 12.0799C14.8092 11.448 14.9267 10.7917 14.9267 10.1112C14.9267 9.43061 14.8092 8.77436 14.5742 8.14242C14.3393 7.51047 14.0031 6.93524 13.5656 6.41672C13.7924 6.3357 14.0193 6.28304 14.2461 6.25873C14.473 6.23443 14.6998 6.22228 14.9267 6.22228C15.9961 6.22228 16.9116 6.60306 17.6732 7.36464C18.4348 8.12621 18.8156 9.04172 18.8156 10.1112ZM3.26 19.8334H14.9267V19.0556C14.9267 18.8774 14.8821 18.7153 14.793 18.5695C14.7039 18.4237 14.5864 18.3102 14.4406 18.2292C13.5656 17.7917 12.6825 17.4636 11.7912 17.2448C10.9 17.0261 10.0007 16.9167 9.09333 16.9167C8.18592 16.9167 7.28662 17.0261 6.39541 17.2448C5.50421 17.4636 4.62111 17.7917 3.74611 18.2292C3.60027 18.3102 3.4828 18.4237 3.39368 18.5695C3.30456 18.7153 3.26 18.8774 3.26 19.0556V19.8334ZM9.09333 12.0556C9.62805 12.0556 10.0858 11.8652 10.4666 11.4844C10.8474 11.1036 11.0378 10.6459 11.0378 10.1112C11.0378 9.57644 10.8474 9.11869 10.4666 8.7379C10.0858 8.35711 9.62805 8.16672 9.09333 8.16672C8.55861 8.16672 8.10085 8.35711 7.72007 8.7379C7.33928 9.11869 7.14889 9.57644 7.14889 10.1112C7.14889 10.6459 7.33928 11.1036 7.72007 11.4844C8.10085 11.8652 8.55861 12.0556 9.09333 12.0556Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.14893 4.27772H16.8711V11.9097C16.8711 12.2824 16.7901 12.6145 16.6281 12.9062C16.4661 13.1979 16.2392 13.4328 15.9475 13.6111L12.4961 15.6527L13.1767 17.8888H16.8711L13.8573 20.0277L15.0239 23.7222L12.01 21.4374L8.99615 23.7222L10.1628 20.0277L7.14893 17.8888H10.8434L11.5239 15.6527L8.07254 13.6111C7.78087 13.4328 7.55402 13.1979 7.39198 12.9062C7.22994 12.6145 7.14893 12.2824 7.14893 11.9097V4.27772ZM9.09337 6.22217V11.9097L11.0378 13.0763V6.22217H9.09337ZM14.9267 6.22217H12.9823V13.0763L14.9267 11.9097V6.22217Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 650 B

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.9891 17.4513L16.4822 11.9583L15.0968 10.5729L10.9891 14.6805L8.94746 12.6388L7.56204 14.0243L10.9891 17.4513ZM12.01 23.7222C9.75764 23.155 7.89827 21.8628 6.43183 19.8454C4.9654 17.8281 4.23218 15.5879 4.23218 13.1249V7.19439L12.01 4.27772L19.7877 7.19439V13.1249C19.7877 15.5879 19.0545 17.8281 17.5881 19.8454C16.1216 21.8628 14.2623 23.155 12.01 23.7222ZM12.01 21.6805C13.6951 21.1458 15.0887 20.0763 16.1905 18.4722C17.2924 16.868 17.8433 15.0856 17.8433 13.1249V8.5312L12.01 6.34369L6.17662 8.5312V13.1249C6.17662 15.0856 6.72755 16.868 7.8294 18.4722C8.93125 20.0763 10.3248 21.1458 12.01 21.6805Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 736 B

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.8988 21.7778V14.9723H19.7877V21.7778H15.8988ZM10.0655 21.7778V6.22228H13.9544V21.7778H10.0655ZM4.23218 21.7778V11.0834H8.12107V21.7778H4.23218Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.33636 23.7222L8.94747 20.6111C8.73682 20.53 8.53833 20.4328 8.35198 20.3194C8.16564 20.206 7.98335 20.0844 7.80511 19.9548L4.91275 21.1701L2.23914 16.552L4.74261 14.6562C4.7264 14.5428 4.7183 14.4334 4.7183 14.3281C4.7183 14.2227 4.7183 14.1134 4.7183 13.9999C4.7183 13.8865 4.7183 13.7771 4.7183 13.6718C4.7183 13.5665 4.7264 13.4571 4.74261 13.3437L2.23914 11.4479L4.91275 6.82981L7.80511 8.04508C7.98335 7.91545 8.16969 7.79393 8.36414 7.6805C8.55858 7.56708 8.75303 7.46985 8.94747 7.38883L9.33636 4.27772H14.6836L15.0725 7.38883C15.2831 7.46985 15.4816 7.56708 15.668 7.6805C15.8543 7.79393 16.0366 7.91545 16.2148 8.04508L19.1072 6.82981L21.7808 11.4479L19.2773 13.3437C19.2935 13.4571 19.3016 13.5665 19.3016 13.6718C19.3016 13.7771 19.3016 13.8865 19.3016 13.9999C19.3016 14.1134 19.3016 14.2227 19.3016 14.3281C19.3016 14.4334 19.2854 14.5428 19.253 14.6562L21.7565 16.552L19.0829 21.1701L16.2148 19.9548C16.0366 20.0844 15.8502 20.206 15.6558 20.3194C15.4614 20.4328 15.2669 20.53 15.0725 20.6111L14.6836 23.7222H9.33636ZM11.0377 21.7777H12.9579L13.2982 19.2013C13.8005 19.0717 14.2663 18.8813 14.6957 18.6302C15.1251 18.379 15.5181 18.0752 15.8746 17.7187L18.2808 18.7152L19.2287 17.0624L17.1384 15.4826C17.2195 15.2557 17.2762 15.0167 17.3086 14.7656C17.341 14.5144 17.3572 14.2592 17.3572 13.9999C17.3572 13.7407 17.341 13.4855 17.3086 13.2343C17.2762 12.9832 17.2195 12.7442 17.1384 12.5173L19.2287 10.9374L18.2808 9.28467L15.8746 10.3055C15.5181 9.93282 15.1251 9.62089 14.6957 9.36974C14.2663 9.11858 13.8005 8.92819 13.2982 8.79856L12.9822 6.22217H11.0621L10.7218 8.79856C10.2195 8.92819 9.7536 9.11858 9.32421 9.36974C8.89481 9.62089 8.50187 9.92471 8.14539 10.2812L5.73914 9.28467L4.79122 10.9374L6.8815 12.493C6.80048 12.7361 6.74377 12.9791 6.71136 13.2222C6.67895 13.4652 6.66275 13.7245 6.66275 13.9999C6.66275 14.2592 6.67895 14.5104 6.71136 14.7534C6.74377 14.9965 6.80048 15.2395 6.8815 15.4826L4.79122 17.0624L5.73914 18.7152L8.14539 17.6944C8.50187 18.0671 8.89481 18.379 9.32421 18.6302C9.7536 18.8813 10.2195 19.0717 10.7218 19.2013L11.0377 21.7777ZM12.0586 17.4027C12.9984 17.4027 13.8005 17.0705 14.4648 16.4062C15.1292 15.7418 15.4614 14.9398 15.4614 13.9999C15.4614 13.0601 15.1292 12.258 14.4648 11.5937C13.8005 10.9293 12.9984 10.5972 12.0586 10.5972C11.1026 10.5972 10.2964 10.9293 9.64018 11.5937C8.98393 12.258 8.6558 13.0601 8.6558 13.9999C8.6558 14.9398 8.98393 15.7418 9.64018 16.4062C10.2964 17.0705 11.1026 17.4027 12.0586 17.4027Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.9613 19.8333C12.3016 19.8333 12.5892 19.7158 12.8242 19.4808C13.0591 19.2459 13.1766 18.9583 13.1766 18.618C13.1766 18.2777 13.0591 17.9901 12.8242 17.7552C12.5892 17.5202 12.3016 17.4027 11.9613 17.4027C11.6211 17.4027 11.3334 17.5202 11.0985 17.7552C10.8635 17.9901 10.7461 18.2777 10.7461 18.618C10.7461 18.9583 10.8635 19.2459 11.0985 19.4808C11.3334 19.7158 11.6211 19.8333 11.9613 19.8333ZM11.0863 16.0902H12.8849C12.8849 15.5555 12.9457 15.1342 13.0672 14.8263C13.1888 14.5185 13.5331 14.0972 14.1002 13.5624C14.5215 13.1411 14.8537 12.7401 15.0967 12.3593C15.3398 11.9785 15.4613 11.5208 15.4613 10.9861C15.4613 10.0786 15.1292 9.38189 14.4648 8.89578C13.8005 8.40967 13.0146 8.16661 12.1072 8.16661C11.1836 8.16661 10.4341 8.40967 9.8589 8.89578C9.28367 9.38189 8.88263 9.96522 8.65578 10.6458L10.2599 11.2777C10.341 10.9861 10.5233 10.6701 10.8068 10.3298C11.0904 9.98953 11.5238 9.81939 12.1072 9.81939C12.6257 9.81939 13.0146 9.96117 13.2738 10.2447C13.5331 10.5283 13.6627 10.8402 13.6627 11.1805C13.6627 11.5046 13.5655 11.8084 13.3711 12.092C13.1766 12.3755 12.9336 12.6388 12.6419 12.8819C11.9289 13.5138 11.4914 13.9918 11.3294 14.3159C11.1673 14.64 11.0863 15.2314 11.0863 16.0902ZM12.0099 23.7222C10.665 23.7222 9.40115 23.467 8.21828 22.9565C7.03541 22.4461 6.00647 21.7534 5.13147 20.8784C4.25647 20.0034 3.56376 18.9745 3.05334 17.7916C2.54293 16.6087 2.28772 15.3449 2.28772 13.9999C2.28772 12.655 2.54293 11.3911 3.05334 10.2083C3.56376 9.02541 4.25647 7.99647 5.13147 7.12147C6.00647 6.24647 7.03541 5.55376 8.21828 5.04335C9.40115 4.53293 10.665 4.27772 12.0099 4.27772C13.3548 4.27772 14.6187 4.53293 15.8016 5.04335C16.9845 5.55376 18.0134 6.24647 18.8884 7.12147C19.7634 7.99647 20.4561 9.02541 20.9665 10.2083C21.477 11.3911 21.7322 12.655 21.7322 13.9999C21.7322 15.3449 21.477 16.6087 20.9665 17.7916C20.4561 18.9745 19.7634 20.0034 18.8884 20.8784C18.0134 21.7534 16.9845 22.4461 15.8016 22.9565C14.6187 23.467 13.3548 23.7222 12.0099 23.7222ZM12.0099 21.7777C14.1812 21.7777 16.0204 21.0243 17.5273 19.5173C19.0342 18.0104 19.7877 16.1712 19.7877 13.9999C19.7877 11.8286 19.0342 9.98953 17.5273 8.48258C16.0204 6.97564 14.1812 6.22217 12.0099 6.22217C9.83865 6.22217 7.99953 6.97564 6.49258 8.48258C4.98564 9.98953 4.23216 11.8286 4.23216 13.9999C4.23216 16.1712 4.98564 18.0104 6.49258 19.5173C7.99953 21.0243 9.83865 21.7777 12.0099 21.7777Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,3 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.20445 22.75C4.66973 22.75 4.21198 22.5596 3.83119 22.1788C3.4504 21.798 3.26001 21.3403 3.26001 20.8056V7.19444C3.26001 6.65972 3.4504 6.20197 3.83119 5.82118C4.21198 5.44039 4.66973 5.25 5.20445 5.25H12.01V7.19444H5.20445V20.8056H12.01V22.75H5.20445ZM15.8989 18.8611L14.5621 17.4514L17.0413 14.9722H9.09334V13.0278H17.0413L14.5621 10.5486L15.8989 9.13889L20.76 14L15.8989 18.8611Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 513 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="24" viewBox="0 0 16 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00008 14.6666L4.66675 11.3333L5.60008 10.3666L7.33342 12.0999V6.66658H8.66675V12.0999L10.4001 10.3666L11.3334 11.3333L8.00008 14.6666ZM4.00008 17.3333C3.63341 17.3333 3.31953 17.2027 3.05841 16.9416C2.7973 16.6805 2.66675 16.3666 2.66675 15.9999V13.9999H4.00008V15.9999H12.0001V13.9999H13.3334V15.9999C13.3334 16.3666 13.2029 16.6805 12.9417 16.9416C12.6806 17.2027 12.3667 17.3333 12.0001 17.3333H4.00008Z" fill="#475569"/>
</svg>

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,8 @@
<svg width="44" height="24" viewBox="0 0 44 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="44" height="24" rx="12" fill="#005A9C"/>
<mask id="path-2-inside-1_9_783" fill="white">
<path d="M22 12C22 6.47715 26.4772 2 32 2C37.5228 2 42 6.47715 42 12C42 17.5228 37.5228 22 32 22C26.4772 22 22 17.5228 22 12Z"/>
</mask>
<path d="M22 12C22 6.47715 26.4772 2 32 2C37.5228 2 42 6.47715 42 12C42 17.5228 37.5228 22 32 22C26.4772 22 22 17.5228 22 12Z" fill="white"/>
<path d="M32 22V21C27.0294 21 23 16.9706 23 12H22H21C21 18.0751 25.9249 23 32 23V22ZM42 12H41C41 16.9706 36.9706 21 32 21V22V23C38.0751 23 43 18.0751 43 12H42ZM32 2V3C36.9706 3 41 7.02944 41 12H42H43C43 5.92487 38.0751 1 32 1V2ZM32 2V1C25.9249 1 21 5.92487 21 12H22H23C23 7.02944 27.0294 3 32 3V2Z" fill="white" mask="url(#path-2-inside-1_9_783)"/>
</svg>

After

Width:  |  Height:  |  Size: 832 B

View File

@ -0,0 +1,4 @@
<svg width="44" height="24" viewBox="0 0 44 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="44" height="24" rx="12" fill="#E5E7EB"/>
<rect y="4" width="16" height="16" rx="8" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 214 B

View File

@ -0,0 +1,3 @@
<svg width="41" height="32" viewBox="0 0 41 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.26001 15.0278V7.25H11.0378V15.0278H3.26001ZM3.26001 24.75V16.9722H11.0378V24.75H3.26001ZM12.9822 15.0278V7.25H20.76V15.0278H12.9822ZM12.9822 24.75V16.9722H20.76V24.75H12.9822ZM5.20445 13.0833H9.09334V9.19444H5.20445V13.0833ZM14.9267 13.0833H18.8156V9.19444H14.9267V13.0833ZM14.9267 22.8056H18.8156V18.9167H14.9267V22.8056ZM5.20445 22.8056H9.09334V18.9167H5.20445V22.8056Z" fill="white" fill-opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 522 B

View File

@ -0,0 +1,3 @@
<svg width="41" height="32" viewBox="0 0 41 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.343384 21.8334V20.3021C0.343384 19.6054 0.699865 19.0382 1.41283 18.6007C2.12579 18.1632 3.06561 17.9445 4.23227 17.9445C4.44292 17.9445 4.64547 17.9485 4.83991 17.9566C5.03436 17.9647 5.2207 17.985 5.39894 18.0174C5.17209 18.3577 5.00195 18.7142 4.88852 19.0868C4.7751 19.4595 4.71838 19.8484 4.71838 20.2535V21.8334H0.343384ZM6.17672 21.8334V20.2535C6.17672 19.735 6.3185 19.261 6.60206 18.8316C6.88563 18.4022 7.28667 18.0255 7.80519 17.7014C8.32371 17.3774 8.9435 17.1343 9.66456 16.9723C10.3856 16.8102 11.1675 16.7292 12.0101 16.7292C12.8688 16.7292 13.6588 16.8102 14.3798 16.9723C15.1009 17.1343 15.7207 17.3774 16.2392 17.7014C16.7577 18.0255 17.1547 18.4022 17.4302 18.8316C17.7057 19.261 17.8434 19.735 17.8434 20.2535V21.8334H6.17672ZM19.3017 21.8334V20.2535C19.3017 19.8322 19.2491 19.4352 19.1437 19.0625C19.0384 18.6899 18.8804 18.3415 18.6698 18.0174C18.848 17.985 19.0303 17.9647 19.2166 17.9566C19.403 17.9485 19.5934 17.9445 19.7878 17.9445C20.9545 17.9445 21.8943 18.1592 22.6073 18.5886C23.3202 19.018 23.6767 19.5892 23.6767 20.3021V21.8334H19.3017ZM8.24269 19.8889H15.8017C15.6397 19.5649 15.19 19.2813 14.4528 19.0382C13.7155 18.7952 12.9013 18.6737 12.0101 18.6737C11.1188 18.6737 10.3046 18.7952 9.56734 19.0382C8.83007 19.2813 8.38852 19.5649 8.24269 19.8889ZM4.23227 16.9723C3.69755 16.9723 3.2398 16.7819 2.85901 16.4011C2.47822 16.0203 2.28783 15.5625 2.28783 15.0278C2.28783 14.4769 2.47822 14.0151 2.85901 13.6424C3.2398 13.2697 3.69755 13.0834 4.23227 13.0834C4.7832 13.0834 5.245 13.2697 5.61769 13.6424C5.99037 14.0151 6.17672 14.4769 6.17672 15.0278C6.17672 15.5625 5.99037 16.0203 5.61769 16.4011C5.245 16.7819 4.7832 16.9723 4.23227 16.9723ZM19.7878 16.9723C19.2531 16.9723 18.7954 16.7819 18.4146 16.4011C18.0338 16.0203 17.8434 15.5625 17.8434 15.0278C17.8434 14.4769 18.0338 14.0151 18.4146 13.6424C18.7954 13.2697 19.2531 13.0834 19.7878 13.0834C20.3388 13.0834 20.8006 13.2697 21.1732 13.6424C21.5459 14.0151 21.7323 14.4769 21.7323 15.0278C21.7323 15.5625 21.5459 16.0203 21.1732 16.4011C20.8006 16.7819 20.3388 16.9723 19.7878 16.9723ZM12.0101 16C11.1999 16 10.5112 15.7165 9.94408 15.1493C9.37695 14.5822 9.09338 13.8936 9.09338 13.0834C9.09338 12.257 9.37695 11.5643 9.94408 11.0052C10.5112 10.4462 11.1999 10.1667 12.0101 10.1667C12.8364 10.1667 13.5291 10.4462 14.0882 11.0052C14.6472 11.5643 14.9267 12.257 14.9267 13.0834C14.9267 13.8936 14.6472 14.5822 14.0882 15.1493C13.5291 15.7165 12.8364 16 12.0101 16ZM12.0101 14.0556C12.2855 14.0556 12.5164 13.9624 12.7028 13.7761C12.8891 13.5897 12.9823 13.3588 12.9823 13.0834C12.9823 12.8079 12.8891 12.577 12.7028 12.3907C12.5164 12.2043 12.2855 12.1112 12.0101 12.1112C11.7346 12.1112 11.5037 12.2043 11.3173 12.3907C11.131 12.577 11.0378 12.8079 11.0378 13.0834C11.0378 13.3588 11.131 13.5897 11.3173 13.7761C11.5037 13.9624 11.7346 14.0556 12.0101 14.0556Z" fill="white" fill-opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,3 @@
<svg width="41" height="32" viewBox="0 0 41 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.1489 24.75V22.8056H11.0378V19.7917C10.2438 19.6134 9.53489 19.2772 8.91105 18.783C8.28721 18.2888 7.82945 17.669 7.53779 16.9236C6.32251 16.7778 5.30573 16.2471 4.48744 15.3316C3.66915 14.4161 3.26001 13.3426 3.26001 12.1111V11.1389C3.26001 10.6042 3.4504 10.1464 3.83119 9.76562C4.21198 9.38484 4.66973 9.19444 5.20445 9.19444H7.1489V7.25H16.8711V9.19444H18.8156C19.3503 9.19444 19.808 9.38484 20.1888 9.76562C20.5696 10.1464 20.76 10.6042 20.76 11.1389V12.1111C20.76 13.3426 20.3509 14.4161 19.5326 15.3316C18.7143 16.2471 17.6975 16.7778 16.4822 16.9236C16.1906 17.669 15.7328 18.2888 15.109 18.783C14.4851 19.2772 13.7762 19.6134 12.9822 19.7917V22.8056H16.8711V24.75H7.1489ZM7.1489 14.8333V11.1389H5.20445V12.1111C5.20445 12.7269 5.38269 13.2818 5.73918 13.776C6.09566 14.2703 6.56557 14.6227 7.1489 14.8333ZM12.01 17.9444C12.8202 17.9444 13.5089 17.6609 14.076 17.0938C14.6431 16.5266 14.9267 15.838 14.9267 15.0278V9.19444H9.09334V15.0278C9.09334 15.838 9.37691 16.5266 9.94404 17.0938C10.5112 17.6609 11.1998 17.9444 12.01 17.9444ZM16.8711 14.8333C17.4545 14.6227 17.9244 14.2703 18.2808 13.776C18.6373 13.2818 18.8156 12.7269 18.8156 12.1111V11.1389H16.8711V14.8333Z" fill="white" fill-opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="41" height="32" viewBox="0 0 41 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.01 25.7222C9.75764 25.155 7.89827 23.8628 6.43183 21.8454C4.9654 19.8281 4.23218 17.5879 4.23218 15.1249V9.19439L12.01 6.27772L19.7877 9.19439V15.1249C19.7877 17.5879 19.0545 19.8281 17.5881 21.8454C16.1216 23.8628 14.2623 25.155 12.01 25.7222ZM12.01 23.6805C13.6951 23.1458 15.0887 22.0763 16.1905 20.4722C17.2924 18.868 17.8433 17.0856 17.8433 15.1249V10.5312L12.01 8.34369L6.17662 10.5312V15.1249C6.17662 17.0856 6.72755 18.868 7.8294 20.4722C8.93125 22.0763 10.3248 23.1458 12.01 23.6805Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 625 B

View File

@ -0,0 +1,3 @@
<svg width="41" height="32" viewBox="0 0 41 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.8988 23.7778V16.9723H19.7877V23.7778H15.8988ZM10.0655 23.7778V8.22228H13.9544V23.7778H10.0655ZM4.23218 23.7778V13.0834H8.12107V23.7778H4.23218Z" fill="white" fill-opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 295 B

View File

@ -0,0 +1,3 @@
<svg width="41" height="32" viewBox="0 0 41 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.33636 25.7222L8.94747 22.6111C8.73682 22.53 8.53833 22.4328 8.35198 22.3194C8.16564 22.206 7.98335 22.0844 7.80511 21.9548L4.91275 23.1701L2.23914 18.552L4.74261 16.6562C4.7264 16.5428 4.7183 16.4334 4.7183 16.3281C4.7183 16.2227 4.7183 16.1134 4.7183 15.9999C4.7183 15.8865 4.7183 15.7771 4.7183 15.6718C4.7183 15.5665 4.7264 15.4571 4.74261 15.3437L2.23914 13.4479L4.91275 8.82981L7.80511 10.0451C7.98335 9.91545 8.16969 9.79393 8.36414 9.6805C8.55858 9.56708 8.75303 9.46985 8.94747 9.38883L9.33636 6.27772H14.6836L15.0725 9.38883C15.2831 9.46985 15.4816 9.56708 15.668 9.6805C15.8543 9.79393 16.0366 9.91545 16.2148 10.0451L19.1072 8.82981L21.7808 13.4479L19.2773 15.3437C19.2935 15.4571 19.3016 15.5665 19.3016 15.6718C19.3016 15.7771 19.3016 15.8865 19.3016 15.9999C19.3016 16.1134 19.3016 16.2227 19.3016 16.3281C19.3016 16.4334 19.2854 16.5428 19.253 16.6562L21.7565 18.552L19.0829 23.1701L16.2148 21.9548C16.0366 22.0844 15.8502 22.206 15.6558 22.3194C15.4614 22.4328 15.2669 22.53 15.0725 22.6111L14.6836 25.7222H9.33636ZM11.0377 23.7777H12.9579L13.2982 21.2013C13.8005 21.0717 14.2663 20.8813 14.6957 20.6302C15.1251 20.379 15.5181 20.0752 15.8746 19.7187L18.2808 20.7152L19.2287 19.0624L17.1384 17.4826C17.2195 17.2557 17.2762 17.0167 17.3086 16.7656C17.341 16.5144 17.3572 16.2592 17.3572 15.9999C17.3572 15.7407 17.341 15.4855 17.3086 15.2343C17.2762 14.9832 17.2195 14.7442 17.1384 14.5173L19.2287 12.9374L18.2808 11.2847L15.8746 12.3055C15.5181 11.9328 15.1251 11.6209 14.6957 11.3697C14.2663 11.1186 13.8005 10.9282 13.2982 10.7986L12.9822 8.22217H11.0621L10.7218 10.7986C10.2195 10.9282 9.7536 11.1186 9.32421 11.3697C8.89481 11.6209 8.50187 11.9247 8.14539 12.2812L5.73914 11.2847L4.79122 12.9374L6.8815 14.493C6.80048 14.7361 6.74377 14.9791 6.71136 15.2222C6.67895 15.4652 6.66275 15.7245 6.66275 15.9999C6.66275 16.2592 6.67895 16.5104 6.71136 16.7534C6.74377 16.9965 6.80048 17.2395 6.8815 17.4826L4.79122 19.0624L5.73914 20.7152L8.14539 19.6944C8.50187 20.0671 8.89481 20.379 9.32421 20.6302C9.7536 20.8813 10.2195 21.0717 10.7218 21.2013L11.0377 23.7777ZM12.0586 19.4027C12.9984 19.4027 13.8005 19.0705 14.4648 18.4062C15.1292 17.7418 15.4614 16.9398 15.4614 15.9999C15.4614 15.0601 15.1292 14.258 14.4648 13.5937C13.8005 12.9293 12.9984 12.5972 12.0586 12.5972C11.1026 12.5972 10.2964 12.9293 9.64018 13.5937C8.98393 14.258 8.6558 15.0601 8.6558 15.9999C8.6558 16.9398 8.98393 17.7418 9.64018 18.4062C10.2964 19.0705 11.1026 19.4027 12.0586 19.4027Z" fill="white" fill-opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,3 @@
<svg width="41" height="32" viewBox="0 0 41 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.9613 21.8333C12.3016 21.8333 12.5892 21.7158 12.8242 21.4808C13.0591 21.2459 13.1766 20.9583 13.1766 20.618C13.1766 20.2777 13.0591 19.9901 12.8242 19.7552C12.5892 19.5202 12.3016 19.4027 11.9613 19.4027C11.6211 19.4027 11.3334 19.5202 11.0985 19.7552C10.8635 19.9901 10.7461 20.2777 10.7461 20.618C10.7461 20.9583 10.8635 21.2459 11.0985 21.4808C11.3334 21.7158 11.6211 21.8333 11.9613 21.8333ZM11.0863 18.0902H12.8849C12.8849 17.5555 12.9457 17.1342 13.0672 16.8263C13.1888 16.5185 13.5331 16.0972 14.1002 15.5624C14.5215 15.1411 14.8537 14.7401 15.0967 14.3593C15.3398 13.9785 15.4613 13.5208 15.4613 12.9861C15.4613 12.0786 15.1292 11.3819 14.4648 10.8958C13.8005 10.4097 13.0146 10.1666 12.1072 10.1666C11.1836 10.1666 10.4341 10.4097 9.8589 10.8958C9.28367 11.3819 8.88263 11.9652 8.65578 12.6458L10.2599 13.2777C10.341 12.9861 10.5233 12.6701 10.8068 12.3298C11.0904 11.9895 11.5238 11.8194 12.1072 11.8194C12.6257 11.8194 13.0146 11.9612 13.2738 12.2447C13.5331 12.5283 13.6627 12.8402 13.6627 13.1805C13.6627 13.5046 13.5655 13.8084 13.3711 14.092C13.1766 14.3755 12.9336 14.6388 12.6419 14.8819C11.9289 15.5138 11.4914 15.9918 11.3294 16.3159C11.1673 16.64 11.0863 17.2314 11.0863 18.0902ZM12.0099 25.7222C10.665 25.7222 9.40115 25.467 8.21828 24.9565C7.03541 24.4461 6.00647 23.7534 5.13147 22.8784C4.25647 22.0034 3.56376 20.9745 3.05334 19.7916C2.54293 18.6087 2.28772 17.3449 2.28772 15.9999C2.28772 14.655 2.54293 13.3911 3.05334 12.2083C3.56376 11.0254 4.25647 9.99647 5.13147 9.12147C6.00647 8.24647 7.03541 7.55376 8.21828 7.04335C9.40115 6.53293 10.665 6.27772 12.0099 6.27772C13.3548 6.27772 14.6187 6.53293 15.8016 7.04335C16.9845 7.55376 18.0134 8.24647 18.8884 9.12147C19.7634 9.99647 20.4561 11.0254 20.9665 12.2083C21.477 13.3911 21.7322 14.655 21.7322 15.9999C21.7322 17.3449 21.477 18.6087 20.9665 19.7916C20.4561 20.9745 19.7634 22.0034 18.8884 22.8784C18.0134 23.7534 16.9845 24.4461 15.8016 24.9565C14.6187 25.467 13.3548 25.7222 12.0099 25.7222ZM12.0099 23.7777C14.1812 23.7777 16.0204 23.0243 17.5273 21.5173C19.0342 20.0104 19.7877 18.1712 19.7877 15.9999C19.7877 13.8286 19.0342 11.9895 17.5273 10.4826C16.0204 8.97564 14.1812 8.22217 12.0099 8.22217C9.83865 8.22217 7.99953 8.97564 6.49258 10.4826C4.98564 11.9895 4.23216 13.8286 4.23216 15.9999C4.23216 18.1712 4.98564 20.0104 6.49258 21.5173C7.99953 23.0243 9.83865 23.7777 12.0099 23.7777Z" fill="white" fill-opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,3 @@
<svg width="41" height="32" viewBox="0 0 41 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.20445 24.75C4.66973 24.75 4.21198 24.5596 3.83119 24.1788C3.4504 23.798 3.26001 23.3403 3.26001 22.8056V9.19444C3.26001 8.65972 3.4504 8.20197 3.83119 7.82118C4.21198 7.44039 4.66973 7.25 5.20445 7.25H12.01V9.19444H5.20445V22.8056H12.01V24.75H5.20445ZM15.8989 20.8611L14.5621 19.4514L17.0413 16.9722H9.09334V15.0278H17.0413L14.5621 12.5486L15.8989 11.1389L20.76 16L15.8989 20.8611Z" fill="white" fill-opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 532 B

View File

@ -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;
}
}
}

View File

@ -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 (
<div className={styles.forgotPassword}>
<div className={styles.forgotPassword__card}>
<div className={styles.forgotPassword__header}>
<div className={styles.forgotPassword__icon}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
<polyline points="22,6 12,13 2,6" />
</svg>
</div>
<h1 className={styles.forgotPassword__title}></h1>
<p className={styles.forgotPassword__subtitle}>
{email}
</p>
</div>
<div className={styles.forgotPassword__actions}>
<Link href="/login">
<Button variant="primary" fullWidth>
</Button>
</Link>
<Button variant="ghost" fullWidth onClick={() => setSent(false)}>
</Button>
</div>
</div>
</div>
);
}
return (
<div className={styles.forgotPassword}>
<div className={styles.forgotPassword__card}>
<div className={styles.forgotPassword__header}>
<div className={styles.forgotPassword__logo}>🔐</div>
<h1 className={styles.forgotPassword__title}></h1>
<p className={styles.forgotPassword__subtitle}>
</p>
</div>
<form className={styles.forgotPassword__form} onSubmit={handleSubmit}>
<Input
type="email"
placeholder="请输入邮箱"
value={email}
onChange={(e) => {
setEmail(e.target.value);
setError('');
}}
error={error}
prefix={
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
<polyline points="22,6 12,13 2,6" />
</svg>
}
/>
<Button type="submit" variant="primary" fullWidth loading={loading}>
</Button>
</form>
<div className={styles.forgotPassword__footer}>
<Link href="/login" className={styles.forgotPassword__link}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="19" y1="12" x2="5" y2="12" />
<polyline points="12 19 5 12 12 5" />
</svg>
</Link>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,17 @@
import { ReactNode } from 'react';
export default function AuthLayout({ children }: { children: ReactNode }) {
return (
<div
style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F5F5F5',
}}
>
{children}
</div>
);
}

View File

@ -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);
}
}

View File

@ -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<HTMLInputElement>
) => {
setFormData((prev) => ({ ...prev, [field]: e.target.value }));
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: '' }));
}
};
return (
<div className={styles.login}>
{/* 品牌 Logo 区域 */}
<div className={styles.login__header}>
<div className={styles.login__logo}>
<Image
src="/images/Container.svg"
width={36}
height={44}
alt="RWADurian Logo"
priority
/>
</div>
<h3 className={styles.login__brandName}></h3>
</div>
{/* 登录表单卡片 */}
<section className={styles.login__card}>
<div className={styles.login__titleWrapper}>
<h1 className={styles.login__title}></h1>
</div>
<form className={styles.login__form} onSubmit={handleSubmit}>
{/* 输入框组 */}
<div className={styles.login__inputGroup}>
<div
className={`${styles.login__inputWrapper} ${
errors.email ? styles['login__inputWrapper--error'] : ''
}`}
>
<input
className={styles.login__input}
placeholder="电子邮件"
type="email"
value={formData.email}
onChange={handleInputChange('email')}
aria-label="电子邮件"
autoComplete="email"
/>
</div>
{errors.email && (
<span className={styles.login__error}>{errors.email}</span>
)}
<div
className={`${styles.login__inputWrapper} ${
errors.password ? styles['login__inputWrapper--error'] : ''
}`}
>
<input
className={styles.login__input}
placeholder="密码"
type="password"
value={formData.password}
onChange={handleInputChange('password')}
aria-label="密码"
autoComplete="current-password"
/>
</div>
{errors.password && (
<span className={styles.login__error}>{errors.password}</span>
)}
</div>
{/* 忘记密码链接 */}
<div className={styles.login__options}>
<Link href="/forgot-password" className={styles.login__link}>
</Link>
</div>
{/* 登录按钮 */}
<button
className={styles.login__button}
type="submit"
disabled={loading}
aria-label="登录"
>
{loading && <span className={styles.login__spinner} />}
<b className={styles.login__buttonText}>
{loading ? '登录中...' : '登录'}
</b>
</button>
</form>
{/* 注册链接 */}
<div className={styles.login__footer}>
<span className={styles.login__footerText}></span>
<Link href="/register" className={styles.login__registerLink}>
</Link>
</div>
</section>
</div>
);
}

View File

@ -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;
}
}

View File

@ -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) => (
<div
className={cn(styles.authorization__toggle, checked ? styles['authorization__toggle--on'] : styles['authorization__toggle--off'])}
onClick={() => onChange(!checked)}
role="switch"
aria-checked={checked}
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && onChange(!checked)}
>
<div className={cn(styles.authorization__toggleHandle, checked ? styles['authorization__toggleHandle--on'] : styles['authorization__toggleHandle--off'])} />
</div>
);
// 渲染省公司表格
const renderProvinceTable = () => (
<div className={styles.authorization__table}>
<div className={styles.authorization__tableHeader}>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--avatar'])}></div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--nickname'])}></div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--accountId'])}></div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--province'])}></div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--teamAdoptions'])}></div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--status'])}></div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--actions'])}></div>
</div>
{mockProvinceCompanies.map((item) => (
<div key={item.id} className={styles.authorization__tableRow}>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--avatar'])}>
<div
className={styles.authorization__avatar}
style={item.avatar ? { backgroundImage: `url(${item.avatar})` } : undefined}
/>
</div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--nickname'])}>{item.nickname}</div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--accountId'])}>{item.accountId}</div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--province'])}>{item.province}</div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--teamAdoptions'])}>{item.teamAdoptions.toLocaleString()}</div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--status'])}>
<span className={cn(styles.authorization__badge, item.authStatus === 'authorized' ? styles['authorization__badge--authorized'] : styles['authorization__badge--pending'])}>
{item.authStatus === 'authorized' ? '已授权' : '待授权'}
</span>
</div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--actions'])}>
<button className={cn(styles.authorization__actionBtn, styles['authorization__actionBtn--authorize'])}></button>
<button className={cn(styles.authorization__actionBtn, styles['authorization__actionBtn--revoke'])}></button>
</div>
</div>
))}
</div>
);
// 渲染市公司表格
const renderCityTable = () => (
<div className={styles.authorization__table}>
<div className={styles.authorization__tableHeader}>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--avatar'])}></div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--nickname'])}></div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--accountId'])}></div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--city'])}>/</div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--teamAdoptions'])}></div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--status'])}></div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--header'], styles['authorization__tableCell--actions'])}></div>
</div>
{mockCityCompanies.map((item) => (
<div key={item.id} className={styles.authorization__tableRow}>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--avatar'])}>
<div
className={styles.authorization__avatar}
style={item.avatar ? { backgroundImage: `url(${item.avatar})` } : undefined}
/>
</div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--nickname'])}>{item.nickname}</div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--accountId'])}>{item.accountId}</div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--city'])}>{item.province} / {item.city}</div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--teamAdoptions'])}>{item.teamAdoptions}</div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--status'])}>
<span className={cn(styles.authorization__badge, item.authStatus === 'authorized' ? styles['authorization__badge--authorized'] : styles['authorization__badge--pending'])}>
{item.authStatus === 'authorized' ? '已授权' : '待授权'}
</span>
</div>
<div className={cn(styles.authorization__tableCell, styles['authorization__tableCell--actions'])}>
<button className={cn(styles.authorization__actionBtn, styles['authorization__actionBtn--authorize'])}></button>
<button className={cn(styles.authorization__actionBtn, styles['authorization__actionBtn--revoke'])}></button>
</div>
</div>
))}
</div>
);
return (
<PageContainer title="授权管理">
<div className={styles.authorization}>
{/* 授权省公司管理 */}
<section className={styles.authorization__card}>
<h3 className={styles.authorization__cardTitle}></h3>
<div className={styles.authorization__filters}>
<select className={styles.authorization__select} aria-label="选择省份">
<option value=""></option>
<option value="guangdong">广</option>
<option value="zhejiang"></option>
<option value="jiangsu"></option>
</select>
<select className={styles.authorization__select} aria-label="授权状态">
<option value=""></option>
<option value="authorized"></option>
<option value="pending"></option>
</select>
<input className={styles.authorization__input} placeholder="关键词搜索" type="text" aria-label="关键词搜索" />
<button className={styles.authorization__searchBtn}></button>
</div>
{renderProvinceTable()}
<p className={styles.authorization__help}></p>
</section>
{/* 授权省公司团队权益考核规则 */}
<section className={styles.authorization__card}>
<h3 className={styles.authorization__cardTitle}></h3>
<div className={styles.authorization__form}>
<div className={styles.authorization__formRow}>
<div className={styles.authorization__formGroup}>
<label className={styles.authorization__formLabel}></label>
<input
className={styles.authorization__formInput}
type="text"
value={provinceThreshold}
onChange={(e) => setProvinceThreshold(e.target.value)}
placeholder="500"
/>
</div>
<div className={styles.authorization__formGroup}>
<label className={styles.authorization__formLabel}>USDT</label>
<input
className={styles.authorization__formInput}
type="text"
value={provinceBenefit}
onChange={(e) => setProvinceBenefit(e.target.value)}
placeholder="20"
/>
<span className={styles.authorization__formHint}> {provinceThreshold} 1 </span>
</div>
</div>
<div className={styles.authorization__formRow}>
<div className={styles.authorization__formGroup}>
<label className={styles.authorization__formLabel}> 1 USDT</label>
<input
className={styles.authorization__formInput}
type="text"
value={provinceAfterBenefit}
onChange={(e) => setProvinceAfterBenefit(e.target.value)}
placeholder="20"
/>
</div>
<div className={styles.authorization__formGroup}>
<label className={styles.authorization__formLabel}></label>
<select className={styles.authorization__select} style={{ width: '100%' }} aria-label="考核周期">
<option value="monthly"></option>
<option value="quarterly"></option>
</select>
</div>
</div>
</div>
<div className={styles.authorization__toggleRow}>
<span className={styles.authorization__toggleLabel}> 0 {provinceThreshold} </span>
{renderToggle(provinceResetEnabled, setProvinceResetEnabled)}
</div>
<div className={styles.authorization__saveWrapper}>
<button className={styles.authorization__saveBtn}></button>
</div>
</section>
{/* 授权市公司管理 */}
<section className={styles.authorization__card}>
<h3 className={styles.authorization__cardTitle}></h3>
<div className={styles.authorization__filters}>
<select className={styles.authorization__select} aria-label="选择省份">
<option value=""></option>
<option value="guangdong">广</option>
<option value="zhejiang"></option>
</select>
<select className={styles.authorization__select} aria-label="选择城市">
<option value=""></option>
<option value="shenzhen"></option>
<option value="guangzhou">广</option>
</select>
<select className={styles.authorization__select} aria-label="授权状态">
<option value=""></option>
<option value="authorized"></option>
<option value="pending"></option>
</select>
<input className={styles.authorization__input} placeholder="关键词搜索" type="text" aria-label="关键词搜索" />
<button className={styles.authorization__searchBtn}></button>
</div>
{renderCityTable()}
<p className={styles.authorization__help}></p>
</section>
{/* 授权市公司团队权益考核规则 */}
<section className={styles.authorization__card}>
<h3 className={styles.authorization__cardTitle}></h3>
<div className={styles.authorization__form}>
<div className={styles.authorization__formRow}>
<div className={styles.authorization__formGroup}>
<label className={styles.authorization__formLabel}></label>
<input
className={styles.authorization__formInput}
type="text"
value={cityThreshold}
onChange={(e) => setCityThreshold(e.target.value)}
placeholder="100"
/>
</div>
<div className={styles.authorization__formGroup}>
<label className={styles.authorization__formLabel}>USDT</label>
<input
className={styles.authorization__formInput}
type="text"
value={cityBenefit}
onChange={(e) => setCityBenefit(e.target.value)}
placeholder="40"
/>
<span className={styles.authorization__formHint}> {cityThreshold} 1 </span>
</div>
</div>
<div className={styles.authorization__formRow}>
<div className={styles.authorization__formGroup}>
<label className={styles.authorization__formLabel}> 1 USDT</label>
<input
className={styles.authorization__formInput}
type="text"
value={cityAfterBenefit}
onChange={(e) => setCityAfterBenefit(e.target.value)}
placeholder="40"
/>
</div>
<div className={styles.authorization__formGroup}>
<label className={styles.authorization__formLabel}></label>
<select className={styles.authorization__select} style={{ width: '100%' }} aria-label="考核周期">
<option value="monthly"></option>
<option value="quarterly"></option>
</select>
</div>
</div>
</div>
<div className={styles.authorization__toggleRow}>
<span className={styles.authorization__toggleLabel}> 0 {cityThreshold} </span>
{renderToggle(cityResetEnabled, setCityResetEnabled)}
</div>
<div className={styles.authorization__saveWrapper}>
<button className={styles.authorization__saveBtn}></button>
</div>
</section>
{/* 正式省公司/市公司授权管理 */}
<div className={styles.authorization__officialSection}>
<section className={styles.authorization__officialCard}>
<h3 className={styles.authorization__officialTitle}></h3>
<p className={styles.authorization__officialDesc}></p>
<div className={styles.authorization__officialTable}>
<div className={styles.authorization__officialRow}>
<div className={styles.authorization__officialCell}></div>
<div className={styles.authorization__officialCell}></div>
<div className={styles.authorization__officialCell}></div>
<div className={styles.authorization__officialCell}></div>
<div className={styles.authorization__officialCell}></div>
<div className={styles.authorization__officialCell}></div>
</div>
</div>
</section>
<section className={styles.authorization__officialCard}>
<h3 className={styles.authorization__officialTitle}></h3>
<p className={styles.authorization__officialDesc}></p>
<div className={styles.authorization__officialTable}>
<div className={styles.authorization__officialRow}>
<div className={styles.authorization__officialCell}></div>
<div className={styles.authorization__officialCell}></div>
<div className={styles.authorization__officialCell}></div>
<div className={styles.authorization__officialCell}>/</div>
<div className={styles.authorization__officialCell}></div>
<div className={styles.authorization__officialCell}></div>
</div>
</div>
</section>
</div>
{/* 省公司 / 市公司 授权限制规则 */}
<section className={styles.authorization__card}>
<h3 className={styles.authorization__cardTitle}> / </h3>
<div className={styles.authorization__rules}>
<div className={styles.authorization__ruleItem}> 1 </div>
<div className={styles.authorization__ruleItem}>
<div className={styles.authorization__ruleInline}>
<span> 1 </span>
<span className={styles.authorization__ruleNote}>(广)</span>
</div>
</div>
<div className={styles.authorization__ruleItem}>
<div className={styles.authorization__ruleInline}>
<span></span>
<input
className={styles.authorization__ruleInput}
type="text"
value={topRankLimit}
onChange={(e) => setTopRankLimit(e.target.value)}
aria-label="排名限制"
/>
<span></span>
</div>
</div>
<div className={styles.authorization__specialRules}>
<div className={styles.authorization__specialTitle}> ()</div>
<div className={styles.authorization__specialTable}>
{specialRules.map((rule, index) => (
<div key={rule.city} className={styles.authorization__specialRow}>
<div className={styles.authorization__specialCity}>{rule.city}</div>
<div className={styles.authorization__specialToggle}>
{renderToggle(rule.enabled, (enabled) => {
const newRules = [...specialRules];
newRules[index] = { ...rule, enabled };
setSpecialRules(newRules);
})}
<span className={styles.authorization__specialLabel}></span>
</div>
</div>
))}
</div>
</div>
</div>
</section>
{/* 省公司 / 市公司 阶梯性考核目标 */}
<section className={styles.authorization__card}>
<div className={styles.authorization__targetHeader}>
<h3 className={styles.authorization__targetTitle}> / </h3>
<button className={styles.authorization__editBtn}></button>
</div>
<div className={styles.authorization__targetTable}>
<div className={styles.authorization__targetHeader2}>
<div className={cn(styles.authorization__targetCell, styles['authorization__targetCell--header'], styles['authorization__targetCell--month'])}></div>
<div className={cn(styles.authorization__targetCell, styles['authorization__targetCell--header'], styles['authorization__targetCell--provinceMonthly'])}></div>
<div className={cn(styles.authorization__targetCell, styles['authorization__targetCell--header'], styles['authorization__targetCell--provinceTotal'])}></div>
<div className={cn(styles.authorization__targetCell, styles['authorization__targetCell--header'], styles['authorization__targetCell--cityMonthly'])}></div>
<div className={cn(styles.authorization__targetCell, styles['authorization__targetCell--header'], styles['authorization__targetCell--cityTotal'])}></div>
</div>
<div className={styles.authorization__targetBody}>
{targetData.map((row, index) => (
<div key={row.month} className={cn(styles.authorization__targetRow, index < targetData.length - 1 && styles['authorization__targetRow--bordered'])}>
<div className={cn(styles.authorization__targetCell, styles['authorization__targetCell--month'])}>{row.month}</div>
<div className={cn(styles.authorization__targetCell, styles['authorization__targetCell--provinceMonthly'])}>{row.month === '...' ? '...' : row.provinceMonthly}</div>
<div className={cn(styles.authorization__targetCell, styles['authorization__targetCell--provinceTotal'])}>{row.month === '...' ? '...' : row.provinceTotal}</div>
<div className={cn(styles.authorization__targetCell, styles['authorization__targetCell--cityMonthly'])}>{row.month === '...' ? '...' : row.cityMonthly}</div>
<div className={cn(styles.authorization__targetCell, styles['authorization__targetCell--cityTotal'])}>{row.month === '...' ? '...' : row.cityTotal}</div>
</div>
))}
</div>
</div>
<p className={styles.authorization__help}> 9 /</p>
</section>
</div>
</PageContainer>
);
}

View File

@ -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;
}
}

View File

@ -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 = (
<>
<Button variant="outline" size="sm">
</Button>
<Button variant="outline" size="sm">
</Button>
<Button variant="primary" size="sm">
</Button>
</>
);
return (
<PageContainer
title="仪表板"
breadcrumb={[{ label: '首页', path: '/' }, { label: '仪表板' }]}
actions={headerActions}
>
<div className={styles.dashboard}>
{/* 统计卡片区 */}
<div className={styles.dashboard__stats}>
{statsData.map((stat, index) => (
<StatCard key={index} {...stat} />
))}
</div>
{/* 图表区 */}
<div className={styles.dashboard__charts}>
<div className={styles.dashboard__mainChart}>
<TrendChart title="认种趋势" data={trendData} />
</div>
<div className={styles.dashboard__sidePanel}>
<RecentActivity activities={activityData} />
</div>
</div>
{/* 底部区域 */}
<div className={styles.dashboard__bottom}>
<div className={styles.dashboard__regionChart}>
<RegionDistribution data={regionData} />
</div>
</div>
</div>
</PageContainer>
);
}

View File

@ -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;
}

View File

@ -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 = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8" />
<path d="M21 21l-4.35-4.35" />
</svg>
);
// 点赞图标
const ThumbUpIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" />
</svg>
);
// 点踩图标
const ThumbDownIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" />
</svg>
);
// 电话图标
const PhoneIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
</svg>
);
// 邮件图标
const MailIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
<polyline points="22,6 12,13 2,6" />
</svg>
);
// 聊天图标
const ChatIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
);
export default function HelpPage() {
const [searchKeyword, setSearchKeyword] = useState('');
return (
<PageContainer title="帮助中心">
<div className={styles.help}>
{/* 页面头部 */}
<header className={styles.help__header}>
<h1 className={styles.help__title}></h1>
<p className={styles.help__desc}></p>
<div className={styles.help__searchBox}>
<span className={styles.help__searchIcon}>
<SearchIcon />
</span>
<input
type="text"
className={styles.help__searchInput}
placeholder="搜索文档或问题..."
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
</div>
</header>
{/* 推荐文档 */}
<section className={styles.help__section}>
<h2 className={styles.help__sectionTitle}></h2>
<div className={styles.help__docCards}>
{recommendedDocs.map((doc) => (
<div key={doc.id} className={styles.help__docCard}>
<h3 className={styles.help__docCardTitle}>{doc.title}</h3>
<p className={styles.help__docCardDesc}>{doc.description}</p>
<div className={styles.help__docCardFooter}>
<span className={styles.help__docCardQuestion}></span>
<div className={styles.help__docCardBtns}>
<button className={styles.help__docCardBtn}>
<ThumbUpIcon />
<span></span>
</button>
<button className={styles.help__docCardBtn}>
<ThumbDownIcon />
<span></span>
</button>
</div>
</div>
</div>
))}
</div>
</section>
{/* 文档列表 */}
<section className={styles.help__section}>
<h2 className={styles.help__sectionTitle}></h2>
<div className={styles.help__tableWrapper}>
<div className={styles.help__table}>
<div className={styles.help__tableHead}>
<div className={cn(styles.help__tableHeadCell, styles['help__tableHeadCell--title'])}></div>
<div className={cn(styles.help__tableHeadCell, styles['help__tableHeadCell--category'])}></div>
<div className={cn(styles.help__tableHeadCell, styles['help__tableHeadCell--date'])}></div>
<div className={cn(styles.help__tableHeadCell, styles['help__tableHeadCell--rating'])}></div>
</div>
<div className={styles.help__tableBody}>
{docList.map((doc) => (
<div key={doc.id} className={styles.help__tableRow}>
<div className={cn(styles.help__tableCell, styles['help__tableCell--title'])}>{doc.title}</div>
<div className={cn(styles.help__tableCell, styles['help__tableCell--category'])}>{doc.category}</div>
<div className={cn(styles.help__tableCell, styles['help__tableCell--date'])}>{doc.lastUpdate}</div>
<div className={cn(styles.help__tableCell, styles['help__tableCell--rating'])}>
<button className={styles.help__ratingBtn}>
<ThumbUpIcon />
</button>
<button className={styles.help__ratingBtn}>
<ThumbDownIcon />
</button>
</div>
</div>
))}
</div>
</div>
</div>
</section>
{/* FAQ 和联系支持 */}
<div className={styles.help__bottomSection}>
{/* 常见问题 */}
<div className={styles.help__faqCard}>
<h2 className={styles.help__faqTitle}> (FAQ)</h2>
<div className={styles.help__faqList}>
{faqData.map((faq, index) => (
<div key={index} className={styles.help__faqItem}>
<p className={styles.help__faqQuestion}>{faq.question}</p>
<p className={styles.help__faqAnswer}>{faq.answer}</p>
</div>
))}
</div>
</div>
{/* 联系支持 */}
<div className={styles.help__contactCard}>
<h2 className={styles.help__contactTitle}></h2>
<div className={styles.help__contactList}>
<div className={styles.help__contactItem}>
<span className={styles.help__contactIcon}>
<PhoneIcon />
</span>
<div className={styles.help__contactInfo}>
<p className={styles.help__contactLabel}>线</p>
<p className={styles.help__contactValue}>400-123-4567 ( 9:00 - 18:00)</p>
</div>
</div>
<div className={styles.help__contactItem}>
<span className={styles.help__contactIcon}>
<MailIcon />
</span>
<div className={styles.help__contactInfo}>
<p className={styles.help__contactLabel}></p>
<p className={styles.help__contactValue}>support@durian-adopt.com</p>
</div>
</div>
<div className={styles.help__contactItem}>
<span className={styles.help__contactIcon}>
<ChatIcon />
</span>
<div className={styles.help__contactInfo}>
<p className={styles.help__contactLabel}>线</p>
<p className={styles.help__contactValue}>线</p>
</div>
</div>
</div>
</div>
</div>
</div>
</PageContainer>
);
}

View File

@ -0,0 +1,13 @@
'use client';
import { ReactNode } from 'react';
import { ToastContainer } from '@/components/common';
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<>
{children}
<ToastContainer />
</>
);
}

View File

@ -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;
}
}

View File

@ -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) => (
<div
className={cn(styles.leaderboard__toggle, checked ? styles['leaderboard__toggle--on'] : styles['leaderboard__toggle--off'])}
onClick={() => onChange(!checked)}
>
<div
className={cn(
styles.leaderboard__toggleHandle,
checked ? styles['leaderboard__toggleHandle--on'] : styles['leaderboard__toggleHandle--off']
)}
/>
</div>
);
// 渲染排名表格行
const renderRankingRow = (item: RankingItem) => {
const rankStyle = getRankStyle(item.rank);
return (
<div
key={item.rank}
className={cn(
styles.leaderboard__tableRow,
rankStyle.row && styles[`leaderboard__tableRow--${rankStyle.row}`]
)}
>
<div className={cn(styles.leaderboard__tableCell, styles['leaderboard__tableCell--rank'])}>
<b className={cn(styles.leaderboard__rankMedal, styles[`leaderboard__rankMedal--${rankStyle.color}`])}>
{rankStyle.medal ? `${rankStyle.medal} ${item.rank}` : item.rank}
</b>
</div>
<div className={cn(styles.leaderboard__tableCell, styles['leaderboard__tableCell--avatar'])}>
<Image
className={styles.leaderboard__avatar}
src={item.avatar}
width={60}
height={60}
alt={item.nickname}
/>
</div>
<div className={cn(styles.leaderboard__tableCell, styles['leaderboard__tableCell--nickname'])}>
<span>{item.nickname}</span>
{item.isVirtual && <span className={styles.leaderboard__virtualTag}></span>}
</div>
<div className={cn(styles.leaderboard__tableCell, styles['leaderboard__tableCell--count'])}>
{item.adoptionCount}
</div>
<div className={cn(styles.leaderboard__tableCell, styles['leaderboard__tableCell--team'])}>
{item.teamData}
</div>
</div>
);
};
return (
<PageContainer title="龙虎榜管理">
<div className={styles.leaderboard}>
{/* 页面头部 */}
<div className={styles.leaderboard__header}>
<h3 className={styles.leaderboard__title}></h3>
<button className={styles.leaderboard__exportBtn} onClick={handleExport}>
<Image src="/images/Container9.svg" width={18} height={18} alt="导出" />
<span className={styles.leaderboard__exportText}></span>
</button>
</div>
{/* 主内容区域 */}
<div className={styles.leaderboard__content}>
{/* 榜单列表 */}
<div className={styles.leaderboard__boards}>
{/* 日榜 */}
<div className={styles.leaderboard__board}>
<div className={styles.leaderboard__boardHeader}>
<div className={styles.leaderboard__boardInfo}>
<h3 className={styles.leaderboard__boardTitle}></h3>
<span className={styles.leaderboard__boardDesc}></span>
</div>
<div className={styles.leaderboard__boardToggle}>
<span className={styles.leaderboard__toggleLabel}></span>
{renderToggle(boardEnabled.daily, (checked) =>
setBoardEnabled({ ...boardEnabled, daily: checked })
)}
<span className={styles.leaderboard__toggleLabel}></span>
</div>
</div>
{boardEnabled.daily ? (
<div className={styles.leaderboard__table}>
<div className={styles.leaderboard__tableHeader}>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--rank'])}>
</div>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--avatar'])}>
</div>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--nickname'])}>
</div>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--count'])}>
</div>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--team'])}>
</div>
</div>
<div className={styles.leaderboard__tableBody}>
{dailyRankings.map(renderRankingRow)}
</div>
</div>
) : (
<div className={styles.leaderboard__empty}>
<div className={styles.leaderboard__emptyIcon}>
<Image src="/images/Background.svg" width={64} height={64} alt="空状态" />
</div>
<h4 className={styles.leaderboard__emptyTitle}></h4>
<span className={styles.leaderboard__emptyStatus}></span>
</div>
)}
</div>
{/* 周榜 */}
<div className={styles.leaderboard__board}>
<div className={styles.leaderboard__boardHeader}>
<div className={styles.leaderboard__boardInfo}>
<h3 className={styles.leaderboard__boardTitle}></h3>
<span className={styles.leaderboard__boardDesc}></span>
</div>
<div className={styles.leaderboard__boardToggle}>
<span className={styles.leaderboard__toggleLabel}></span>
{renderToggle(boardEnabled.weekly, (checked) =>
setBoardEnabled({ ...boardEnabled, weekly: checked })
)}
<span className={styles.leaderboard__toggleLabel}></span>
</div>
</div>
<div className={styles.leaderboard__empty}>
<div className={styles.leaderboard__emptyIcon}>
<Image src="/images/Background.svg" width={64} height={64} alt="空状态" />
</div>
<h4 className={styles.leaderboard__emptyTitle}></h4>
<span className={styles.leaderboard__emptyStatus}></span>
</div>
</div>
{/* 月榜 */}
<div className={styles.leaderboard__board}>
<div className={styles.leaderboard__boardHeader}>
<div className={styles.leaderboard__boardInfo}>
<h3 className={styles.leaderboard__boardTitle}></h3>
<span className={styles.leaderboard__boardDesc}></span>
</div>
<div className={styles.leaderboard__boardToggle}>
<span className={styles.leaderboard__toggleLabel}></span>
{renderToggle(boardEnabled.monthly, (checked) =>
setBoardEnabled({ ...boardEnabled, monthly: checked })
)}
<span className={styles.leaderboard__toggleLabel}></span>
</div>
</div>
<div className={styles.leaderboard__empty}>
<div className={styles.leaderboard__emptyIcon}>
<Image src="/images/Background1.svg" width={64} height={64} alt="空状态" />
</div>
<h4 className={styles.leaderboard__emptyTitle}></h4>
<span className={styles.leaderboard__emptyStatus}></span>
</div>
</div>
</div>
{/* 设置面板 */}
<div className={styles.leaderboard__settings}>
<h3 className={styles.leaderboard__settingsTitle}></h3>
<div className={styles.leaderboard__settingsContent}>
{/* 虚拟排名设置 */}
<div className={styles.leaderboard__settingsSection}>
<h4 className={styles.leaderboard__sectionTitle}></h4>
<div className={styles.leaderboard__settingsItem}>
<span className={styles.leaderboard__settingsLabel}></span>
{renderToggle(virtualSettings.enabled, (checked) =>
updateVirtualSettings({ enabled: checked })
)}
</div>
<div className={styles.leaderboard__settingsItem}>
<span className={styles.leaderboard__settingsLabel}></span>
<div className={styles.leaderboard__sliderGroup}>
<input
type="text"
className={styles.leaderboard__numberInput}
value={virtualSettings.virtualAccountCount}
onChange={(e) =>
updateVirtualSettings({ virtualAccountCount: parseInt(e.target.value) || 0 })
}
/>
<input
type="range"
className={styles.leaderboard__slider}
min={0}
max={10}
value={virtualSettings.virtualAccountCount}
onChange={(e) =>
updateVirtualSettings({ virtualAccountCount: parseInt(e.target.value) })
}
/>
</div>
</div>
<div
className={cn(
styles.leaderboard__rulesToggle,
showRules && styles['leaderboard__rulesToggle--expanded']
)}
onClick={() => setShowRules(!showRules)}
>
<span></span>
<Image src="/images/Container10.svg" width={24} height={24} alt="展开" />
</div>
{/* 实时预览 */}
<div className={styles.leaderboard__preview}>
<span className={styles.leaderboard__previewTitle}></span>
<div className={styles.leaderboard__previewList}>
{virtualUsers.map((user, index) => (
<div key={index} className={styles.leaderboard__previewItem}>
<Image
className={styles.leaderboard__previewAvatar}
src={user.avatar}
width={32}
height={32}
alt={user.nickname}
/>
<span className={styles.leaderboard__previewName}>{user.nickname}</span>
<span className={styles.leaderboard__previewTag}></span>
</div>
))}
</div>
</div>
</div>
{/* 显示设置 */}
<div className={styles.leaderboard__settingsSection}>
<h4 className={styles.leaderboard__sectionTitle}></h4>
<div className={styles.leaderboard__settingsItem}>
<span className={styles.leaderboard__settingsLabel}></span>
</div>
<select
className={styles.leaderboard__select}
value={displaySettings.frontendDisplayCount}
onChange={(e) =>
updateDisplaySettings({ frontendDisplayCount: parseInt(e.target.value) as 10 | 20 | 50 })
}
>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</select>
</div>
</div>
{/* 保存按钮 */}
<div className={styles.leaderboard__settingsFooter}>
<button className={styles.leaderboard__saveBtn} onClick={handleSave}>
</button>
</div>
</div>
</div>
</div>
</PageContainer>
);
}

View File

@ -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<string[]>(['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 (
<div
className={cn(styles.settings__toggleSmall, checked ? styles['settings__toggleSmall--on'] : styles['settings__toggleSmall--off'])}
onClick={() => onChange(!checked)}
>
<div className={cn(styles.settings__toggleSmallHandle, checked ? styles['settings__toggleSmallHandle--on'] : styles['settings__toggleSmallHandle--off'])} />
</div>
);
}
return (
<div
className={cn(styles.settings__toggle, checked ? styles['settings__toggle--on'] : styles['settings__toggle--off'])}
onClick={() => onChange(!checked)}
>
<div className={cn(styles.settings__toggleHandle, checked ? styles['settings__toggleHandle--on'] : styles['settings__toggleHandle--off'])} />
</div>
);
};
return (
<PageContainer title="系统设置">
<div className={styles.settings}>
{/* 页面头部 */}
<header className={styles.settings__header}>
<h1 className={styles.settings__title}></h1>
<p className={styles.settings__desc}></p>
<div className={styles.settings__notice}>
"保存设置"
</div>
</header>
{/* 结算参数设置 */}
<section className={styles.settings__section}>
<div className={styles.settings__sectionHeader}>
<h2 className={styles.settings__sectionTitle}></h2>
<button className={styles.settings__resetBtn}></button>
</div>
<div className={styles.settings__content}>
<div className={styles.settings__fieldGroup}>
<label className={styles.settings__label}></label>
<div className={styles.settings__checkboxGroup}>
{['BNB', 'OG', 'USDT', 'DST'].map(currency => (
<label key={currency} className={styles.settings__checkboxItem}>
<input
type="checkbox"
className={styles.settings__checkbox}
checked={settlementCurrencies.includes(currency)}
onChange={() => toggleCurrency(currency)}
/>
<span className={styles.settings__checkboxLabel}>{currency}</span>
</label>
))}
</div>
</div>
<div className={styles.settings__selectWrapper}>
<label className={styles.settings__label}></label>
<select
className={styles.settings__select}
value={defaultCurrency}
onChange={(e) => setDefaultCurrency(e.target.value)}
>
<option value=""></option>
{settlementCurrencies.map(currency => (
<option key={currency} value={currency}>{currency}</option>
))}
</select>
<span className={styles.settings__hint}>"一键结算"</span>
</div>
</div>
</section>
{/* 龙虎榜设置 */}
<section className={styles.settings__section}>
<div className={styles.settings__sectionHeader}>
<h2 className={styles.settings__sectionTitle}></h2>
<button className={styles.settings__resetBtn}></button>
</div>
<div className={styles.settings__content}>
<div className={styles.settings__toggleRow}>
<span className={styles.settings__toggleLabel}></span>
<Toggle checked={enableVirtualAccount} onChange={setEnableVirtualAccount} />
</div>
<div className={styles.settings__inputRow}>
<span className={styles.settings__toggleLabel}></span>
<input
type="text"
className={cn(styles.settings__input, styles['settings__input--medium'])}
placeholder="请输入数量"
value={virtualAccountCount}
onChange={(e) => setVirtualAccountCount(e.target.value)}
/>
<span className={styles.settings__inputUnit}></span>
</div>
<div className={styles.settings__fieldGroup}>
<label className={styles.settings__label}></label>
<div className={styles.settings__switchGroup}>
<div className={styles.settings__switchItem}>
<span className={styles.settings__switchLabel}></span>
<Toggle checked={dailyRankEnabled} onChange={setDailyRankEnabled} size="small" />
</div>
<div className={styles.settings__switchItem}>
<span className={styles.settings__switchLabel}></span>
<Toggle checked={weeklyRankEnabled} onChange={setWeeklyRankEnabled} size="small" />
</div>
<div className={styles.settings__switchItem}>
<span className={styles.settings__switchLabel}></span>
<Toggle checked={monthlyRankEnabled} onChange={setMonthlyRankEnabled} size="small" />
</div>
</div>
</div>
<div className={styles.settings__fieldGroup}>
<label className={styles.settings__label}></label>
<input
type="text"
className={cn(styles.settings__input, styles['settings__input--medium'])}
placeholder="例如10、20、31"
value={displayCount}
onChange={(e) => setDisplayCount(e.target.value)}
/>
<span className={styles.settings__hint}> N </span>
</div>
<div className={styles.settings__hint}>
</div>
</div>
</section>
{/* 认种限额设置 */}
<section className={styles.settings__section}>
<div className={styles.settings__sectionHeader}>
<h2 className={styles.settings__sectionTitle}></h2>
<button className={styles.settings__resetBtn}></button>
</div>
<div className={styles.settings__content}>
{/* 单账户认种限额 */}
<div className={styles.settings__quotaCard}>
<div className={styles.settings__quotaHeader}>
<span className={styles.settings__quotaTitle}></span>
<Toggle checked={singleAccountLimitEnabled} onChange={setSingleAccountLimitEnabled} />
</div>
<div className={styles.settings__quotaInputRow}>
<span className={styles.settings__quotaText}></span>
<input
type="text"
className={cn(styles.settings__input, styles['settings__input--small'])}
placeholder="天数"
value={singleAccountDays}
onChange={(e) => setSingleAccountDays(e.target.value)}
/>
<span className={styles.settings__quotaText}></span>
<input
type="text"
className={cn(styles.settings__input, styles['settings__input--small'])}
placeholder="棵数"
value={singleAccountTrees}
onChange={(e) => setSingleAccountTrees(e.target.value)}
/>
<span className={styles.settings__quotaText}></span>
</div>
<span className={styles.settings__hint}>5 1 </span>
</div>
{/* 全网总量限额 */}
<div className={styles.settings__quotaCard}>
<div className={styles.settings__quotaHeader}>
<span className={styles.settings__quotaTitle}></span>
<Toggle checked={networkLimitEnabled} onChange={setNetworkLimitEnabled} />
</div>
<div className={styles.settings__quotaInputRow}>
<span className={styles.settings__quotaText}></span>
<input
type="text"
className={cn(styles.settings__input, styles['settings__input--small'])}
placeholder="天数"
value={networkDays}
onChange={(e) => setNetworkDays(e.target.value)}
/>
<span className={styles.settings__quotaText}></span>
<input
type="text"
className={cn(styles.settings__input, styles['settings__input--small'])}
placeholder="棵数"
value={networkTrees}
onChange={(e) => setNetworkTrees(e.target.value)}
/>
<span className={styles.settings__quotaText}></span>
</div>
<span className={styles.settings__hint}>2 100 </span>
</div>
</div>
</section>
{/* 考核规则设置 */}
<section className={styles.settings__section}>
<div className={styles.settings__sectionHeader}>
<h2 className={styles.settings__sectionTitle}></h2>
<button className={styles.settings__resetBtn}></button>
</div>
<div className={styles.settings__content}>
{/* 本地用户占比阈值 */}
<div className={styles.settings__assessmentRow}>
<label className={styles.settings__assessmentLabel}>/</label>
<div className={styles.settings__assessmentInputRow}>
<input
type="text"
className={cn(styles.settings__input, styles['settings__input--small'])}
value={localUserThreshold}
onChange={(e) => setLocalUserThreshold(e.target.value)}
/>
<span className={styles.settings__assessmentUnit}>%</span>
</div>
<span className={styles.settings__hint}>/</span>
</div>
{/* 豁免设置 */}
<div className={styles.settings__exemptionRow}>
<div className={styles.settings__exemptionInfo}>
<span className={styles.settings__exemptionLabel}>"不考核自有团队本省/市占比"</span>
<span className={styles.settings__exemptionHint}></span>
</div>
<Toggle checked={exemptionEnabled} onChange={setExemptionEnabled} />
</div>
{/* 阶梯性考核目标表 */}
<div className={styles.settings__tableSection}>
<div className={styles.settings__tableHeader}>
<h3 className={styles.settings__tableTitle}> / </h3>
<div className={styles.settings__tableBtns}>
<button className={styles.settings__tableBtn}></button>
<button className={styles.settings__tableBtn}></button>
</div>
</div>
<div className={styles.settings__table}>
<div className={styles.settings__tableHead}>
<div className={cn(styles.settings__tableHeadCell, styles['settings__tableHeadCell--month'])}></div>
<div className={cn(styles.settings__tableHeadCell, styles['settings__tableHeadCell--target'])}></div>
<div className={cn(styles.settings__tableHeadCell, styles['settings__tableHeadCell--target'])}></div>
<div className={cn(styles.settings__tableHeadCell, styles['settings__tableHeadCell--target'])}></div>
<div className={cn(styles.settings__tableHeadCell, styles['settings__tableHeadCell--target'])}></div>
</div>
<div className={styles.settings__tableBody}>
{mockTargets.map((target) => (
<div key={target.month} className={styles.settings__tableRow}>
<div className={cn(styles.settings__tableCell, styles['settings__tableCell--month'])}>{target.month}</div>
<div className={cn(styles.settings__tableCell, styles['settings__tableCell--target'])}>{target.provinceMonthly}</div>
<div className={cn(styles.settings__tableCell, styles['settings__tableCell--target'])}>{target.provinceCumulative}</div>
<div className={cn(styles.settings__tableCell, styles['settings__tableCell--target'])}>{target.cityMonthly}</div>
<div className={cn(styles.settings__tableCell, styles['settings__tableCell--target'])}>{target.cityCumulative}</div>
</div>
))}
</div>
</div>
<span className={styles.settings__hint}></span>
</div>
</div>
</section>
{/* 前端展示设置 */}
<section className={styles.settings__section}>
<div className={styles.settings__sectionHeader}>
<h2 className={styles.settings__sectionTitle}></h2>
<button className={styles.settings__resetBtn}></button>
</div>
<div className={styles.settings__content}>
<div className={styles.settings__toggleRow}>
<span className={styles.settings__toggleLabel}></span>
<Toggle checked={allowNonAdopterView} onChange={setAllowNonAdopterView} />
</div>
<div className={styles.settings__fieldGroup}>
<label className={styles.settings__label}></label>
<div className={styles.settings__radioGroup}>
<label className={styles.settings__radioItem}>
<input
type="radio"
className={styles.settings__radio}
name="heatDisplayMode"
checked={heatDisplayMode === 'count'}
onChange={() => setHeatDisplayMode('count')}
/>
<span className={styles.settings__radioLabel}></span>
</label>
<label className={styles.settings__radioItem}>
<input
type="radio"
className={styles.settings__radio}
name="heatDisplayMode"
checked={heatDisplayMode === 'level'}
onChange={() => setHeatDisplayMode('level')}
/>
<span className={styles.settings__radioLabel}> / / </span>
</label>
</div>
</div>
<span className={styles.settings__hint}></span>
</div>
</section>
{/* 后台账号与安全 */}
<section className={styles.settings__section}>
<div className={styles.settings__sectionHeader}>
<h2 className={styles.settings__sectionTitle}></h2>
<button className={styles.settings__resetBtn}></button>
</div>
<div className={styles.settings__securityContent}>
{/* 左侧:账号管理 */}
<div className={styles.settings__securityLeft}>
<div className={styles.settings__accountHeader}>
<h3 className={styles.settings__accountTitle}></h3>
<button className={styles.settings__addBtn}></button>
</div>
<div className={styles.settings__accountTable}>
<div className={styles.settings__accountTableInner}>
<div className={styles.settings__accountHead}>
<div className={cn(styles.settings__accountHeadCell, styles['settings__accountHeadCell--name'])}></div>
<div className={cn(styles.settings__accountHeadCell, styles['settings__accountHeadCell--role'])}></div>
<div className={cn(styles.settings__accountHeadCell, styles['settings__accountHeadCell--status'])}></div>
<div className={cn(styles.settings__accountHeadCell, styles['settings__accountHeadCell--login'])}></div>
<div className={cn(styles.settings__accountHeadCell, styles['settings__accountHeadCell--actions'])}></div>
</div>
<div className={styles.settings__accountBody}>
{mockAccounts.map((account) => (
<div key={account.id} className={styles.settings__accountRow}>
<div className={cn(styles.settings__accountCell, styles['settings__accountCell--name'])}>{account.name}</div>
<div className={cn(styles.settings__accountCell, styles['settings__accountCell--role'])}>{account.role}</div>
<div className={cn(styles.settings__accountCell, styles['settings__accountCell--status'])}>
<span className={cn(styles.settings__statusBadge, account.status === 'disabled' && styles['settings__statusBadge--disabled'])}>
{account.status === 'active' ? '正常' : '禁用'}
</span>
</div>
<div className={cn(styles.settings__accountCell, styles['settings__accountCell--login'])}>{account.lastLogin}</div>
<div className={cn(styles.settings__accountCell, styles['settings__accountCell--actions'])}>
<button className={styles.settings__actionLink}></button>
<button className={cn(styles.settings__actionLink, styles['settings__actionLink--danger'])}></button>
</div>
</div>
))}
</div>
</div>
</div>
<span className={styles.settings__hint}></span>
</div>
{/* 右侧:敏感操作和日志 */}
<div className={styles.settings__securityRight}>
{/* 敏感操作审批 */}
<div className={styles.settings__sensitiveCard}>
<div className={styles.settings__sensitiveRow}>
<span className={styles.settings__sensitiveLabel}></span>
<input
type="text"
className={cn(styles.settings__input, styles['settings__input--small'])}
value={approvalCount}
onChange={(e) => setApprovalCount(e.target.value)}
/>
<span className={styles.settings__inputUnit}></span>
</div>
<div className={styles.settings__sensitiveFieldGroup}>
<label className={styles.settings__label}></label>
<input
type="text"
className={styles.settings__tagInput}
placeholder="选择或输入操作名称"
/>
<div className={styles.settings__tagList}>
{sensitiveOperations.map((op, index) => (
<span key={index} className={styles.settings__tag}>
{op}
<button
className={styles.settings__tagRemove}
onClick={() => setSensitiveOperations(sensitiveOperations.filter((_, i) => i !== index))}
>
×
</button>
</span>
))}
</div>
</div>
</div>
{/* 操作日志 */}
<div className={styles.settings__logSection}>
<h3 className={styles.settings__logTitle}></h3>
<div className={styles.settings__logFilters}>
<input
type="date"
className={styles.settings__dateInput}
value={logDate}
onChange={(e) => setLogDate(e.target.value)}
/>
<input
type="text"
className={styles.settings__searchInput}
placeholder="操作账号/关键字搜索"
value={logSearch}
onChange={(e) => setLogSearch(e.target.value)}
/>
<button className={styles.settings__exportBtn}></button>
</div>
<div className={styles.settings__logTable}>
<div className={styles.settings__logTableInner}>
<div className={styles.settings__logHead}>
<div className={cn(styles.settings__logHeadCell, styles['settings__logHeadCell--time'])}></div>
<div className={cn(styles.settings__logHeadCell, styles['settings__logHeadCell--account'])}></div>
<div className={cn(styles.settings__logHeadCell, styles['settings__logHeadCell--type'])}></div>
<div className={cn(styles.settings__logHeadCell, styles['settings__logHeadCell--desc'])}></div>
<div className={cn(styles.settings__logHeadCell, styles['settings__logHeadCell--result'])}></div>
</div>
<div className={styles.settings__logBody}>
{mockLogs.map((log) => (
<div key={log.id} className={styles.settings__logRow}>
<div className={cn(styles.settings__logCell, styles['settings__logCell--time'])}>{log.time}</div>
<div className={cn(styles.settings__logCell, styles['settings__logCell--account'])}>{log.account}</div>
<div className={cn(styles.settings__logCell, styles['settings__logCell--type'])}>{log.type}</div>
<div className={cn(styles.settings__logCell, styles['settings__logCell--desc'])}>{log.description}</div>
<div className={cn(styles.settings__logCell, styles['settings__logCell--result'])}>
<span className={cn(
styles.settings__logResult,
log.result === 'pass' && styles['settings__logResult--pass'],
log.result === 'reject' && styles['settings__logResult--reject'],
log.result === 'pending' && styles['settings__logResult--pending']
)}>
{log.result === 'pass' ? '通过' : log.result === 'reject' ? '拒绝' : '待审'}
</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* 页面底部操作按钮 */}
<footer className={styles.settings__footer}>
<button className={styles.settings__cancelBtn}></button>
<button className={styles.settings__saveBtn}></button>
</footer>
</div>
</PageContainer>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -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 (
<PageContainer title="数据统计">
<div className={styles.statistics}>
{/* 页面标题 */}
<h2 className={styles.statistics__title}></h2>
{/* 统计概览卡片 */}
<section className={styles.statistics__overview}>
<div className={styles.statistics__overviewCard}>
<div className={styles.statistics__overviewLabel}></div>
<h1 className={styles.statistics__overviewValue}>12,345</h1>
</div>
<div className={styles.statistics__overviewCard}>
<div className={styles.statistics__overviewLabel}></div>
<h1 className={styles.statistics__overviewValue}>123</h1>
</div>
<div className={styles.statistics__overviewCard}>
<div className={styles.statistics__overviewLabel}></div>
<h1 className={styles.statistics__overviewValue}>1,456</h1>
</div>
</section>
{/* 榴莲树认种数量趋势 */}
<section className={styles.statistics__card}>
<div className={styles.statistics__cardHeader}>
<b className={styles.statistics__cardTitle}></b>
<div className={styles.statistics__periodTabs}>
{(['day', 'week', 'month', 'quarter', 'year'] as const).map((period) => (
<button
key={period}
className={cn(styles.statistics__periodTab, trendPeriod === period && styles['statistics__periodTab--active'])}
onClick={() => setTrendPeriod(period)}
>
{period === 'day' ? '日' : period === 'week' ? '周' : period === 'month' ? '月' : period === 'quarter' ? '季度' : '年度'}
</button>
))}
</div>
</div>
<div className={styles.statistics__chartArea}>Chart Placeholder</div>
</section>
{/* 龙虎榜与排名统计 */}
<section>
<h3 className={styles.statistics__sectionTitle}></h3>
<div className={styles.statistics__tabs}>
<button
className={cn(styles.statistics__tab, rankingTab === 'daily' && styles['statistics__tab--active'])}
onClick={() => setRankingTab('daily')}
>
</button>
<button
className={cn(styles.statistics__tab, rankingTab === 'weekly' && styles['statistics__tab--active'])}
onClick={() => setRankingTab('weekly')}
>
</button>
<button
className={cn(styles.statistics__tab, rankingTab === 'monthly' && styles['statistics__tab--active'])}
onClick={() => setRankingTab('monthly')}
>
</button>
</div>
<div className={styles.statistics__rankingSection}>
{/* 排名表格 */}
<div className={styles.statistics__rankingTable}>
<div className={styles.statistics__table}>
<div className={styles.statistics__tableHeader}>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__tableCell--rank'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__tableCell--account'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__tableCell--count'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__tableCell--province'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__tableCell--city'])}></div>
</div>
{rankingData.map((item) => (
<div key={item.rank} className={styles.statistics__tableRow}>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--rank'])}>{item.rank}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--account'])}>{item.account}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--count'])}>{item.count}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--province'])}>{item.province}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--city'])}>{item.city}</div>
</div>
))}
</div>
</div>
{/* 授权公司第一名 */}
<div className={styles.statistics__highlightCards}>
<div className={styles.statistics__highlightCard}>
<b className={styles.statistics__highlightTitle}></b>
<h3 className={styles.statistics__highlightName}>广</h3>
<div className={styles.statistics__highlightData}>完成数据: 2,345</div>
</div>
<div className={styles.statistics__highlightCard}>
<b className={styles.statistics__highlightTitle}></b>
<h3 className={styles.statistics__highlightName}></h3>
<div className={styles.statistics__highlightData}>完成数据: 1,120</div>
</div>
</div>
</div>
</section>
{/* 区域认种数据统计 */}
<section className={styles.statistics__regionCard}>
<div className={styles.statistics__regionHeader}>
<h3 className={styles.statistics__regionTitle}></h3>
<div className={styles.statistics__regionTabs}>
<button
className={cn(styles.statistics__regionTab, regionType === 'province' && styles['statistics__regionTab--active'])}
onClick={() => setRegionType('province')}
>
</button>
<button
className={cn(styles.statistics__regionTab, regionType === 'city' && styles['statistics__regionTab--active'])}
onClick={() => setRegionType('city')}
>
</button>
</div>
</div>
<div className={styles.statistics__regionContent}>
<div className={styles.statistics__regionChart}>
<b className={styles.statistics__regionChartTitle}></b>
<div className={styles.statistics__regionChartArea}>Bar Chart Placeholder</div>
</div>
<div className={styles.statistics__regionTableWrapper}>
<div className={styles.statistics__table} style={{ width: '100%' }}>
<div className={styles.statistics__tableHeader} style={{ minWidth: '341px' }}>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__regionCell--region'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__regionCell--period'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__regionCell--count'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__regionCell--ratio'])}></div>
</div>
{regionData.map((item, index) => (
<div key={item.region} className={cn(styles.statistics__tableRow, index === regionData.length - 1 && styles['statistics__tableRow--bordered'])} style={{ minWidth: '341px' }}>
<div className={cn(styles.statistics__tableCell, styles['statistics__regionCell--region'])}>{item.region}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__regionCell--period'])}>{item.period}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__regionCell--count'])}>{item.count.toLocaleString()}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__regionCell--ratio'])}>{item.ratio}</div>
</div>
))}
</div>
</div>
</div>
<div className={styles.statistics__viewDetail}>
<button className={styles.statistics__viewDetailLink}></button>
</div>
</section>
{/* 省/市公司运营统计 */}
<section className={styles.statistics__operationCard}>
<div className={styles.statistics__operationHeader}>
<h3 className={styles.statistics__operationTitle}> / </h3>
<button className={styles.statistics__exportBtn}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
<span></span>
</button>
</div>
<div className={styles.statistics__metricsGrid}>
{metricsData.map((metric) => (
<div key={metric.label} className={styles.statistics__metricCard}>
<div className={styles.statistics__metricLabel} style={{ whiteSpace: 'pre-wrap' }}>{metric.label}</div>
<b className={styles.statistics__metricValue} style={{ whiteSpace: 'pre-wrap' }}>{metric.value}</b>
</div>
))}
</div>
<div className={styles.statistics__table} style={{ minWidth: '736px' }}>
<div className={styles.statistics__tableHeader} style={{ minWidth: '736px' }}>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__operationCell--type'])}> / </div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__operationCell--name'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__operationCell--month'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__operationCell--hashrate'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__operationCell--mining'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__operationCell--commission'])}></div>
</div>
{operationData.map((item, index) => (
<div key={`${item.name}-${index}`} className={styles.statistics__tableRow} style={{ minWidth: '736px' }}>
<div className={cn(styles.statistics__tableCell, styles['statistics__operationCell--type'])}>{item.type}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__operationCell--name'])}>{item.name}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__operationCell--month'])}>{item.month}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__operationCell--hashrate'])}>{item.hashrate}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__operationCell--mining'])}>{item.mining}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__operationCell--commission'])}>{item.commission}</div>
</div>
))}
</div>
</section>
{/* 收益明细与来源 */}
<section className={styles.statistics__revenueCard}>
<h3 className={styles.statistics__revenueTitle}></h3>
<div className={styles.statistics__filters}>
<div className={styles.statistics__filterGroup}>
<label className={styles.statistics__filterLabel}></label>
<select className={styles.statistics__filterSelect} aria-label="收益来源">
<option value=""></option>
<option value="mining"></option>
<option value="adoption"></option>
<option value="profit"></option>
</select>
</div>
<div className={styles.statistics__filterGroup}>
<label className={styles.statistics__filterLabel}> / </label>
<div className={styles.statistics__filterInputWrapper}>
<div className={styles.statistics__filterIcon}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
</div>
<input
className={styles.statistics__filterInput}
type="text"
placeholder="按地址 / 交易流水号筛选"
aria-label="按地址 / 交易流水号筛选"
/>
</div>
</div>
</div>
<div className={styles.statistics__table} style={{ minWidth: '762px' }}>
<div className={styles.statistics__tableHeader} style={{ minWidth: '762px' }}>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__revenueCell--time'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__revenueCell--account'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__revenueCell--source'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__revenueCell--amount'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__revenueCell--address'])}></div>
<div className={cn(styles.statistics__tableCell, styles['statistics__tableCell--header'], styles['statistics__revenueCell--txId'])}></div>
</div>
{revenueData.map((item, index) => (
<div key={item.txId} className={cn(styles.statistics__tableRow, index === revenueData.length - 1 && styles['statistics__tableRow--bordered'])} style={{ minWidth: '762px' }}>
<div className={cn(styles.statistics__tableCell, styles['statistics__revenueCell--time'])}>{item.time}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__revenueCell--account'])}>{item.account}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__revenueCell--source'])}>{item.source}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__revenueCell--amount'])}>{item.amount}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__revenueCell--address'])}>{item.address}</div>
<div className={cn(styles.statistics__tableCell, styles['statistics__revenueCell--txId'])}>{item.txId}</div>
</div>
))}
</div>
<div className={styles.statistics__timelineLink}>
<button className={styles.statistics__viewDetailLink}></button>
</div>
</section>
</div>
</PageContainer>
);
}

View File

@ -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;
}

View File

@ -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<string[]>([]);
const [detailModal, setDetailModal] = useState<UserItem | null>(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(
<button
key="first"
className={cn(styles.users__pageBtn, pagination.current === 1 && styles['users__pageBtn--disabled'])}
onClick={() => setPagination((prev) => ({ ...prev, current: 1 }))}
disabled={pagination.current === 1}
>
</button>
);
// 上一页按钮
buttons.push(
<button
key="prev"
className={cn(styles.users__pageBtn, pagination.current === 1 && styles['users__pageBtn--disabled'])}
onClick={() => setPagination((prev) => ({ ...prev, current: Math.max(1, prev.current - 1) }))}
disabled={pagination.current === 1}
>
</button>
);
// 页码按钮
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(
<button
key={1}
className={styles.users__pageBtn}
onClick={() => setPagination((prev) => ({ ...prev, current: 1 }))}
>
1
</button>
);
if (startPage > 2) {
buttons.push(
<span key="ellipsis1" className={cn(styles.users__pageBtn, styles['users__pageBtn--ellipsis'])}>
...
</span>
);
}
}
for (let i = startPage; i <= endPage; i++) {
buttons.push(
<button
key={i}
className={cn(
styles.users__pageBtn,
pagination.current === i && styles['users__pageBtn--active']
)}
onClick={() => setPagination((prev) => ({ ...prev, current: i }))}
>
{i}
</button>
);
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
buttons.push(
<span key="ellipsis2" className={cn(styles.users__pageBtn, styles['users__pageBtn--ellipsis'])}>
...
</span>
);
}
buttons.push(
<button
key={totalPages}
className={styles.users__pageBtn}
onClick={() => setPagination((prev) => ({ ...prev, current: totalPages }))}
>
{totalPages}
</button>
);
}
// 下一页按钮
buttons.push(
<button
key="next"
className={cn(
styles.users__pageBtn,
pagination.current === totalPages && styles['users__pageBtn--disabled']
)}
onClick={() => setPagination((prev) => ({ ...prev, current: Math.min(totalPages, prev.current + 1) }))}
disabled={pagination.current === totalPages}
>
</button>
);
// 末页按钮
buttons.push(
<button
key="last"
className={cn(
styles.users__pageBtn,
pagination.current === totalPages && styles['users__pageBtn--disabled']
)}
onClick={() => setPagination((prev) => ({ ...prev, current: totalPages }))}
disabled={pagination.current === totalPages}
>
</button>
);
return buttons;
};
return (
<PageContainer title="用户管理">
<div className={styles.users}>
{/* 页面标题和操作按钮 */}
<div className={styles.users__header}>
<h1 className={styles.users__title}></h1>
<div className={styles.users__actions}>
<button className={styles.users__actionBtn} onClick={handleExport}>
<Image src="/images/Container9.svg" width={16} height={16} alt="导出" />
<span className={styles.users__actionText}>Excel</span>
</button>
<button className={styles.users__actionBtn} onClick={handleBatchEdit}>
<Image src="/images/Container10.svg" width={16} height={16} alt="编辑" />
<span className={styles.users__actionText}></span>
</button>
</div>
</div>
{/* 主内容卡片 */}
<div className={styles.users__card}>
{/* 搜索和筛选区域 */}
<div className={styles.users__filters}>
{/* 搜索框 */}
<div className={styles.users__searchBar}>
<div className={styles.users__searchIcon}>
<Image src="/images/Container11.svg" width={20} height={20} alt="搜索" />
</div>
<input
className={styles.users__searchInput}
placeholder="搜索账户ID、昵称"
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
</div>
{/* 高级筛选面板 */}
<div className={styles.users__filterPanel}>
<div
className={styles.users__filterHeader}
onClick={() => setShowFilters(!showFilters)}
>
<span className={styles.users__filterTitle}></span>
<div
className={cn(
styles.users__filterArrow,
showFilters && styles['users__filterArrow--expanded']
)}
>
<Image src="/images/Container12.svg" width={16} height={16} alt="展开" />
</div>
</div>
{showFilters && (
<div className={styles.users__filterContent}>
<select className={styles.users__paginationSelect}>
<option value=""></option>
<option value="guangdong">广</option>
<option value="zhejiang"></option>
</select>
<select className={styles.users__paginationSelect}>
<option value=""></option>
<option value="shenzhen"></option>
<option value="guangzhou">广</option>
</select>
<select className={styles.users__paginationSelect}>
<option value=""></option>
<option value="online">线</option>
<option value="offline">线</option>
</select>
</div>
)}
</div>
</div>
{/* 表格区域 */}
<div className={styles.users__table}>
{/* 表格头部 */}
<div className={styles.users__tableHeader}>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--checkbox'])}>
<input
type="checkbox"
className={styles.users__checkbox}
checked={selectedRows.length === paginatedData.length && paginatedData.length > 0}
onChange={(e) => handleSelectAll(e.target.checked)}
/>
</div>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--id'])}>
<b></b>
</div>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--avatar'])}>
<b></b>
</div>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--nickname'])}>
<b></b>
</div>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--adoptions'])}>
<b></b>
</div>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--teamAddress'])}>
<b></b>
</div>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--teamTotal'])}>
<b></b>
</div>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--province'])}>
<b></b>
</div>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--city'])}>
<b></b>
</div>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--referrer'])}>
<b></b>
</div>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--ranking'])}>
<b></b>
</div>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--actions'])}>
<b></b>
</div>
</div>
{/* 表格内容 */}
<div className={styles.users__tableBody}>
{paginatedData.map((user) => (
<div
key={user.accountId}
className={cn(
styles.users__tableRow,
selectedRows.includes(user.accountId) && styles['users__tableRow--selected']
)}
>
{/* 复选框 */}
<div className={cn(styles.users__tableCell, styles['users__tableCell--checkbox'])}>
<input
type="checkbox"
className={styles.users__checkbox}
checked={selectedRows.includes(user.accountId)}
onChange={(e) => handleSelectRow(user.accountId, e.target.checked)}
/>
</div>
{/* 账户序号 */}
<div className={cn(styles.users__tableCell, styles['users__tableCell--id'])}>
{user.accountId}
</div>
{/* 头像 */}
<div className={cn(styles.users__tableCell, styles['users__tableCell--avatar'])}>
<div
className={styles.users__avatar}
style={{ backgroundImage: `url(${user.avatar})` }}
>
<div
className={cn(
styles.users__avatarStatus,
styles[`users__avatarStatus--${user.status}`]
)}
/>
</div>
</div>
{/* 昵称 */}
<div className={cn(styles.users__tableCell, styles['users__tableCell--nickname'])}>
{user.nickname}
</div>
{/* 账户认种量 */}
<div className={cn(styles.users__tableCell, styles['users__tableCell--adoptions'])}>
{formatNumber(user.personalAdoptions)}
</div>
{/* 团队总注册地址量 */}
<div className={cn(styles.users__tableCell, styles['users__tableCell--teamAddress'])}>
{formatNumber(user.teamAddresses)}
</div>
{/* 团队总认种量 */}
<div className={cn(styles.users__tableCell, styles['users__tableCell--teamTotal'])}>
{formatNumber(user.teamAdoptions)}
</div>
{/* 团队本省认种量及占比 */}
<div className={cn(styles.users__tableCell, styles['users__tableCell--province'])}>
<span>{formatNumber(user.provincialAdoptions.count)}</span>
<span className={styles.users__percentage}>
({user.provincialAdoptions.percentage}%)
</span>
</div>
{/* 团队本市认种量及占比 */}
<div className={cn(styles.users__tableCell, styles['users__tableCell--city'])}>
<span>{formatNumber(user.cityAdoptions.count)}</span>
<span className={styles.users__percentage}>
({user.cityAdoptions.percentage}%)
</span>
</div>
{/* 推荐人序列号 */}
<div className={cn(styles.users__tableCell, styles['users__tableCell--referrer'])}>
{user.referrerId}
</div>
{/* 龙虎榜排名 */}
<div
className={cn(
styles.users__tableCell,
styles['users__tableCell--ranking'],
user.ranking && user.ranking <= 10
? styles['users__tableCell--gold']
: styles['users__tableCell--normal']
)}
>
{user.ranking ? formatRanking(user.ranking) : '-'}
</div>
{/* 操作 */}
<div className={cn(styles.users__tableCell, styles['users__tableCell--actions'])}>
<button
className={styles.users__rowAction}
onClick={() => setDetailModal(user)}
>
</button>
<button className={styles.users__rowAction}>
</button>
</div>
</div>
))}
</div>
</div>
{/* 分页区域 */}
<div className={styles.users__pagination}>
<div className={styles.users__paginationInfo}>
<span className={styles.users__paginationLabel}>:</span>
<select
className={styles.users__paginationSelect}
value={pagination.pageSize}
onChange={(e) =>
setPagination({ current: 1, pageSize: Number(e.target.value) })
}
>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</select>
<span className={styles.users__paginationTotal}>
<span>{filteredData.length}</span>
</span>
</div>
<div className={styles.users__paginationList}>{renderPaginationButtons()}</div>
</div>
</div>
{/* 用户详情弹窗 */}
<Modal
visible={!!detailModal}
title="用户详情"
onClose={() => setDetailModal(null)}
footer={null}
width={600}
>
{detailModal && (
<div className={styles.userDetail}>
<div className={styles.userDetail__header}>
<div
className={styles.users__avatar}
style={{
backgroundImage: `url(${detailModal.avatar})`,
width: 64,
height: 64,
}}
/>
<div className={styles.userDetail__info}>
<h3>{detailModal.nickname}</h3>
<p>ID: {detailModal.accountId}</p>
</div>
</div>
<div className={styles.userDetail__stats}>
<div className={styles.userDetail__statItem}>
<span className={styles.userDetail__statLabel}></span>
<span className={styles.userDetail__statValue}>
{formatNumber(detailModal.personalAdoptions)}
</span>
</div>
<div className={styles.userDetail__statItem}>
<span className={styles.userDetail__statLabel}></span>
<span className={styles.userDetail__statValue}>
{formatNumber(detailModal.teamAdoptions)}
</span>
</div>
<div className={styles.userDetail__statItem}>
<span className={styles.userDetail__statLabel}></span>
<span className={styles.userDetail__statValue}>
{formatRanking(detailModal.ranking)}
</span>
</div>
</div>
</div>
)}
</Modal>
</div>
</PageContainer>
);
}

View File

@ -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;
}
}

Some files were not shown because too many files have changed in this diff Show More