first commit

This commit is contained in:
2025-06-05 23:05:33 +08:00
parent 6c50403cf6
commit 2da15fa933
67 changed files with 19905 additions and 1935 deletions

61
.cursor/rules/nextjs.mdc Normal file
View File

@@ -0,0 +1,61 @@
---
description:
globs:
alwaysApply: true
---
Always respond in 中文
### 角色设定
你是一位精通以下技术的专家:
- TypeScript
- Node.js 22
- Next.js 15 Page Router
- React 19
- Ant Design 5.x (antd)
- Tailwind CSS
- MongoDB (通过 Mongoose 8.x 进行管理)
- pnpm 包管理器
我们正在使用以上技术栈进行开发
### 开发规范
- 必须包含三级注释体系:
▸ 文件头注释(作者:阿瑞/功能/版本)
▸ 模块级注释(逻辑分段说明)
▸ 关键代码行注释(复杂逻辑解释)
### TypeScript 规范
- 所有代码必须使用 TypeScript
- 优先使用 `interface` 而非 `type`
- 避免使用 `enum`,改用映射对象(map)
- 函数式组件需配合 TypeScript 接口使用
### 代码风格与结构
- 编写简洁专业的技术型 TypeScript 代码
- 采用函数式与声明式编程模式,避免类(class)的使用已启用TypeScript严格模式不要出现类型错误和未使用的变量
- 优先使用迭代和模块化,避免代码重复
- 变量命名需具描述性,使用辅助动词(如 `isLoading`, `hasError`
- 文件结构顺序:
1. 导出的主组件
2. 子组件
3. 工具函数
4. 静态内容
5. 类型定义
### UI 与样式
- **UI框架使用Ant Design (antd) 组件库**
- 设计到UI/UX设计请查看README.md文件中的样式规范
- UI使用Ant Design 5.X 注意兼容性问题
- 使用现代简约、扁平化毛玻璃的设计来制作设计UI/UX
### 数据获取与路由Page Router规范
- API路由使用 `pages/api/` 目录
- 页面跳转优先使用 `next/link` 组件
- 使用自带的fetch禁止使用axios
## 补充
- 适当使用useMemo、React.memo来优化性能和防止不必要的重新渲染但是要注意防止React.memo过度使用、useCallback依赖项设置错误的问题要合理使用。
- 当执行终端命令时先解释这个命令的作用如果要使用pnpm安装包时先解释为什么需要这个包
- 当前在Windows 11环境下使用PowerShell终端进行开发禁用类Unix命令使用`Get-ChildItem` 替代ls
- 已使用pnpm run dev启动了项目禁止重复运行启动命令
- 当修改超过300行的文件时确保每次修改代码行数 ≤ 100行分多次修改。

View File

@@ -1,40 +1,85 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app). # 私域管理系统 - 现代毛玻璃UI效果
## 项目简介
## Getting Started 奥云私域管理系统是一个基于现代前端技术栈构建的管理系统,旨在为私域流量的管理和运营提供高效便捷的解决方案。
这是一个基于Next.js和React开发的现代化SaaS管理平台使用最新的毛玻璃UI设计风格提供明亮通透的用户界面体验。
First, run the development server: ## 项目特点
```bash - **现代毛玻璃UI效果**采用最新流行的毛玻璃Glassmorphism设计风格提供通透、现代的用户界面
npm run dev - **主题切换功能**:支持明亮/深色两种主题模式,满足不同用户偏好和使用场景
# or - **主题持久化存储**:使用 zustand 管理状态localStorage 保存主题偏好,刷新页面后仍保持设置
yarn dev - **响应式设计**:完全适配移动端和桌面端的各种屏幕尺寸
# or - **动态视觉元素**:使用多种动画效果增强用户体验,包括背景气泡、渐变动画等
pnpm dev - **模块化组件**:基于组件化思想构建,便于维护和扩展
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. ## 技术栈
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. - **前端框架**Next.js 15.x + React 19
- **状态管理**Zustand
- **样式解决方案**Tailwind CSS 4.x
- **字体**Geist字体家族提供现代简约风格
- **动画**CSS原生动画
- **开发语言**TypeScript
[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. ## 界面预览
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages. 项目提供了多种精美的UI组件和效果
This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - **毛玻璃导航栏**:半透明模糊效果,随着主题变化而调整
- **功能展示卡片**:带有微妙悬浮效果的信息卡片
- **数据统计面板**:展示关键数据指标的可视化组件
- **主题切换开关**:允许用户在明亮/深色主题间切换,并记住用户选择
## Learn More ## 状态管理
To learn more about Next.js, take a look at the following resources: 系统使用 Zustand 进行状态管理,具有以下特点:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - **轻量级**Zustand 更加简洁易用
- [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial. - **持久化存储**:通过 localStorage 保存用户设置
- **类型安全**:完全支持 TypeScript 类型
- **主题切换**:提供主题切换功能,并支持刷新后保持用户偏好
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! ## 设计说明
## Deploy on Vercel ### 色彩系统
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 项目使用了一套鲜明而和谐的色彩系统:
Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details. - **主色调**
- 蓝色 (#2d7ff9)
- 紫色 (#8e6bff)
- 青色 (#06d7b2)
- 粉色 (#ff66c2)
- 橙色 (#ff9640)
- **明亮主题背景**:明亮的淡蓝色,配合多彩渐变气泡
- **暗色主题背景**:深蓝色调,带有鲜艳的强调色点缀
### 毛玻璃效果参数
精心调整的毛玻璃效果参数,确保最佳视觉体验:
- **背景模糊**`backdrop-blur-xl`确保适当的模糊程度
- **透明度**卡片背景透明度在0.25-0.6之间
- **边框**:微妙的半透明边框提升层次感
## 许可证
MIT
## 项目创建命令:
PS C:\Users\29897\Desktop\TEST> npx create-next-app@latest saas2 --typescript
Need to install the following packages:
create-next-app@15.3.3
Ok to proceed? (y) y
√ Would you like to use ESLint? ... No
√ Would you like to use Tailwind CSS? ... Yes
√ Would you like your code inside a `src/` directory? ... Yes
√ Would you like to use App Router? (recommended) ... No
√ Would you like to use Turbopack for `next dev`? ... No
√ Would you like to customize the import alias (`@/*` by default)? ... Yes
√ What import alias would you like configured? ... @/*
Creating a new Next.js app in C:\Users\29897\Desktop\TEST\saas2.

1753
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,16 +9,35 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@ant-design/cssinjs": "^1.23.0",
"@ant-design/icons": "^6.0.0",
"@ant-design/pro-components": "^2.8.7",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@iconify/react": "^4.1.1",
"antd": "^5.25.4",
"bcryptjs": "^3.0.2",
"geist": "^1.4.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.15.1",
"next": "15.3.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"next": "15.3.3" "react-error-boundary": "^6.0.0",
"react-icons": "^5.5.0",
"styled-components": "^6.0.9",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-react": "^5.22.0",
"zustand": "^5.0.5"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "@tailwindcss/postcss": "^4",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@tailwindcss/postcss": "^4", "@types/swagger-jsdoc": "^6.0.4",
"tailwindcss": "^4" "@types/swagger-ui-react": "^5.18.0",
"tailwindcss": "^4",
"typescript": "^5"
} }
} }

4626
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/aoun.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

474
public/welcome.svg Normal file
View File

@@ -0,0 +1,474 @@
<svg width="1052" height="652" viewBox="0 0 1052 652" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M909.026 641.482H254.027H209.272V338.044C209.272 316.552 226.717 299.128 248.235 299.128C269.754 299.128 287.198 281.705 287.198 260.212V227.344C287.198 187.699 319.376 155.56 359.069 155.56H683.672C717.986 155.56 745.803 183.343 745.803 217.615C745.803 251.887 773.62 279.67 807.933 279.67H827.414C872.487 279.67 909.026 316.165 909.026 361.183V641.482Z"
fill="#F3F6FF"/>
<path
d="M809.512 139.783C812.42 139.783 814.778 137.429 814.778 134.524C814.778 131.62 812.42 129.265 809.512 129.265C806.604 129.265 804.247 131.62 804.247 134.524C804.247 137.429 806.604 139.783 809.512 139.783Z"
fill="#54B7FF"/>
<path
d="M841.63 165.552C848.609 165.552 854.267 159.901 854.267 152.93C854.267 145.96 848.609 140.309 841.63 140.309C834.651 140.309 828.994 145.96 828.994 152.93C828.994 159.901 834.651 165.552 841.63 165.552Z"
stroke="#54B7FF" stroke-width="1.053"/>
<path
d="M319.062 107.54C328.382 109.316 337.377 103.208 339.151 93.8978C340.925 84.5874 334.807 75.5999 325.486 73.8237C316.166 72.0475 307.171 78.1553 305.397 87.4657C303.623 96.7761 309.741 105.764 319.062 107.54Z"
fill="#54B7FF"/>
<path
d="M294.367 140.737C310.392 143.791 325.857 133.289 328.907 117.281C331.958 101.274 321.439 85.821 305.413 82.7671C289.387 79.7132 273.923 90.2145 270.872 106.222C267.822 122.23 278.341 137.683 294.367 140.737Z"
stroke="#7268F5" stroke-width="1.053"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M316.007 651.438C313.858 651.438 312.182 649.581 312.405 647.447L338.901 393.717L349.371 394.84L319.603 648.243C319.389 650.065 317.843 651.438 316.007 651.438Z"
fill="#627AC1"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M433.809 651.438C435.957 651.438 437.634 649.581 437.411 647.447L410.915 393.717L400.444 394.84L430.212 648.243C430.427 650.065 431.973 651.438 433.809 651.438Z"
fill="#627AC1"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M602.437 651.438C600.289 651.438 598.612 649.581 598.835 647.447L625.331 393.717L635.802 394.84L606.034 648.243C605.819 650.065 604.273 651.438 602.437 651.438Z"
fill="#627AC1"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M720.24 651.438C722.388 651.438 724.064 649.581 723.841 647.447L697.345 393.717L686.875 394.84L716.642 648.243C716.857 650.065 718.403 651.438 720.24 651.438Z"
fill="#627AC1"/>
<path d="M419.399 382.744H323.572V398.521H419.399V382.744Z" fill="#A3BAFF"/>
<path d="M716.36 382.744H419.399V398.521H716.36V382.744Z" fill="#CCD9FF"/>
<path
d="M487.278 2H373.548C369.477 2 366.177 5.30011 366.177 9.371V82.9785C366.177 87.0494 369.477 90.3495 373.548 90.3495H487.278C491.349 90.3495 494.649 87.0494 494.649 82.9785V9.371C494.649 5.30011 491.349 2 487.278 2Z"
fill="white" stroke="#E5E5E5" stroke-width="2.106"/>
<path
d="M387.238 32.5017C390.728 32.5017 393.556 29.6763 393.556 26.1911C393.556 22.7058 390.728 19.8804 387.238 19.8804C383.749 19.8804 380.92 22.7058 380.92 26.1911C380.92 29.6763 383.749 32.5017 387.238 32.5017Z"
fill="#A3B7FF"/>
<path d="M393.556 61.9514H380.92V79.8317H393.556V61.9514Z" fill="#5FFFD8"/>
<path d="M393.556 49.3301H380.92V67.2103H393.556V49.3301Z" fill="#1BE3B3"/>
<path d="M415.67 64.0549H403.034V80.8834H415.67V64.0549Z" fill="#5FFFD8"/>
<path d="M415.67 56.6926H403.034V71.4175H415.67V56.6926Z" fill="#1BE3B3"/>
<path d="M437.785 53.5371H425.148V80.8834H437.785V53.5371Z" fill="#5FFFD8"/>
<path d="M437.785 44.0713H425.148V61.9515H437.785V44.0713Z" fill="#1BE3B3"/>
<path d="M459.899 53.5371H447.262V80.8834H459.899V53.5371Z" fill="#5FFFD8"/>
<path d="M459.899 29.3462H447.262V56.6925H459.899V29.3462Z" fill="#1BE3B3"/>
<path d="M482.013 53.5371H469.376V80.8834H482.013V53.5371Z" fill="#5FFFD8"/>
<path d="M482.013 37.7605H469.376V65.1068H482.013V37.7605Z" fill="#1BE3B3"/>
<path
d="M453.408 200.823C453.408 196.155 457.196 192.372 461.868 192.372H656.464C661.136 192.372 664.924 196.155 664.924 200.823V336.029C664.924 340.696 661.136 344.479 656.464 344.479H461.868C457.196 344.479 453.408 340.696 453.408 336.029V200.823Z"
fill="#706AC7"/>
<path
d="M460.176 205.893C460.176 201.226 463.964 197.442 468.637 197.442H649.695C654.368 197.442 658.156 201.226 658.156 205.893V330.959C658.156 335.627 654.368 339.41 649.695 339.41H468.637C463.964 339.41 460.176 335.627 460.176 330.959V205.893Z"
fill="#282F48"/>
<path d="M580.318 344.479H538.015V374.057H580.318V344.479Z" fill="#2C2770"/>
<path
d="M488.942 378.282C488.942 375.949 490.836 374.057 493.172 374.057H626.005C628.341 374.057 630.235 375.949 630.235 378.282C630.235 380.616 628.341 382.507 626.005 382.507H493.172C490.836 382.507 488.942 380.616 488.942 378.282Z"
fill="#706AC7"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M461.281 203.262C460.574 204.676 460.175 206.272 460.175 207.96V328.892C460.175 334.7 464.89 339.41 470.706 339.41H647.624C653.44 339.41 658.155 334.7 658.155 328.892V313.635C656.039 304.339 638.271 277.722 601.469 294.622C564.665 311.523 516.862 288.707 506.286 248.145C498.271 217.406 473.681 205.726 461.281 203.262Z"
fill="#2F3B67"/>
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="591" y="249" width="34"
height="34">
<path
d="M608.094 282.802C617.395 282.802 624.936 275.27 624.936 265.979C624.936 256.688 617.395 249.156 608.094 249.156C598.791 249.156 591.25 256.688 591.25 265.979C591.25 275.27 598.791 282.802 608.094 282.802ZM608.094 277.034C614.207 277.034 619.162 272.085 619.162 265.979C619.162 259.874 614.207 254.924 608.094 254.924C601.981 254.924 597.025 259.874 597.025 265.979C597.025 272.085 601.981 277.034 608.094 277.034Z"
fill="white"/>
</mask>
<g mask="url(#mask0)">
<path
d="M608.094 282.802C617.396 282.802 624.937 275.27 624.937 265.979C624.937 256.688 617.396 249.156 608.094 249.156C598.792 249.156 591.251 256.688 591.251 265.979C591.251 275.27 598.792 282.802 608.094 282.802Z"
fill="#324178"/>
</g>
<mask id="mask1" mask-type="alpha" maskUnits="userSpaceOnUse" x="591" y="249" width="34"
height="34">
<path
d="M608.094 282.802C617.395 282.802 624.936 275.27 624.936 265.979C624.936 256.688 617.395 249.156 608.094 249.156C598.791 249.156 591.25 256.688 591.25 265.979C591.25 275.27 598.791 282.802 608.094 282.802ZM608.094 277.034C614.207 277.034 619.162 272.085 619.162 265.979C619.162 259.874 614.207 254.924 608.094 254.924C601.981 254.924 597.025 259.874 597.025 265.979C597.025 272.085 601.981 277.034 608.094 277.034Z"
fill="white"/>
</mask>
<g mask="url(#mask1)">
<path
d="M624.936 265.979C624.936 262.652 623.949 259.399 622.098 256.633C620.247 253.866 617.617 251.71 614.54 250.437C611.462 249.164 608.075 248.831 604.808 249.48C601.541 250.129 598.54 251.731 596.184 254.084L608.094 265.979H624.936Z"
fill="white"/>
</g>
<mask id="mask2" mask-type="alpha" maskUnits="userSpaceOnUse" x="543" y="249" width="35"
height="34">
<path
d="M560.225 282.802C569.527 282.802 577.068 275.27 577.068 265.979C577.068 256.688 569.527 249.156 560.225 249.156C550.923 249.156 543.382 256.688 543.382 265.979C543.382 275.27 550.923 282.802 560.225 282.802ZM560.225 277.034C566.338 277.034 571.294 272.085 571.294 265.979C571.294 259.874 566.338 254.924 560.225 254.924C554.112 254.924 549.157 259.874 549.157 265.979C549.157 272.085 554.112 277.034 560.225 277.034Z"
fill="white"/>
</mask>
<g mask="url(#mask2)">
<path
d="M560.225 282.802C569.527 282.802 577.068 275.27 577.068 265.979C577.068 256.688 569.527 249.156 560.225 249.156C550.923 249.156 543.382 256.688 543.382 265.979C543.382 275.27 550.923 282.802 560.225 282.802Z"
fill="#324178"/>
</g>
<mask id="mask3" mask-type="alpha" maskUnits="userSpaceOnUse" x="543" y="249" width="35"
height="34">
<path
d="M560.225 282.802C569.527 282.802 577.068 275.27 577.068 265.979C577.068 256.688 569.527 249.156 560.225 249.156C550.923 249.156 543.382 256.688 543.382 265.979C543.382 275.27 550.923 282.802 560.225 282.802ZM560.225 277.034C566.338 277.034 571.294 272.085 571.294 265.979C571.294 259.874 566.338 254.924 560.225 254.924C554.112 254.924 549.157 259.874 549.157 265.979C549.157 272.085 554.112 277.034 560.225 277.034Z"
fill="white"/>
</mask>
<g mask="url(#mask3)">
<path
d="M577.068 265.979C577.068 263.213 576.385 260.489 575.079 258.049C573.774 255.609 571.886 253.529 569.582 251.992C567.279 250.455 564.632 249.509 561.876 249.237C559.119 248.966 556.338 249.378 553.779 250.437C551.22 251.496 548.962 253.168 547.205 255.307C545.448 257.445 544.246 259.984 543.706 262.697C543.165 265.411 543.303 268.215 544.107 270.863C544.911 273.51 546.356 275.918 548.315 277.875L560.225 265.979H577.068Z"
fill="white"/>
</g>
<mask id="mask4" mask-type="alpha" maskUnits="userSpaceOnUse" x="493" y="249" width="35"
height="34">
<path
d="M510.582 282.802C519.885 282.802 527.425 275.27 527.425 265.979C527.425 256.688 519.885 249.156 510.582 249.156C501.28 249.156 493.739 256.688 493.739 265.979C493.739 275.27 501.28 282.802 510.582 282.802ZM510.582 277.034C516.695 277.034 521.651 272.085 521.651 265.979C521.651 259.874 516.695 254.924 510.582 254.924C504.47 254.924 499.514 259.874 499.514 265.979C499.514 272.085 504.47 277.034 510.582 277.034Z"
fill="white"/>
</mask>
<g mask="url(#mask4)">
<path
d="M510.582 282.802C519.884 282.802 527.425 275.27 527.425 265.979C527.425 256.688 519.884 249.156 510.582 249.156C501.28 249.156 493.739 256.688 493.739 265.979C493.739 275.27 501.28 282.802 510.582 282.802Z"
fill="#324178"/>
</g>
<mask id="mask5" mask-type="alpha" maskUnits="userSpaceOnUse" x="493" y="249" width="35"
height="34">
<path
d="M510.582 282.802C519.885 282.802 527.425 275.27 527.425 265.979C527.425 256.688 519.885 249.156 510.582 249.156C501.28 249.156 493.739 256.688 493.739 265.979C493.739 275.27 501.28 282.802 510.582 282.802ZM510.582 277.034C516.695 277.034 521.651 272.085 521.651 265.979C521.651 259.874 516.695 254.924 510.582 254.924C504.47 254.924 499.514 259.874 499.514 265.979C499.514 272.085 504.47 277.034 510.582 277.034Z"
fill="white"/>
</mask>
<g mask="url(#mask5)">
<path
d="M527.425 265.979C527.425 262.652 526.438 259.399 524.587 256.633C522.736 253.866 520.106 251.71 517.028 250.437C513.95 249.164 510.564 248.831 507.296 249.48C504.029 250.129 501.028 251.731 498.672 254.084C496.317 256.436 494.713 259.434 494.063 262.697C493.413 265.961 493.747 269.343 495.021 272.417C496.296 275.491 498.455 278.118 501.225 279.967C503.995 281.815 507.251 282.802 510.582 282.802V265.979H527.425Z"
fill="white"/>
</g>
<path
d="M490.193 311.467C490.193 310.306 491.135 309.364 492.298 309.364H532.307C533.47 309.364 534.413 310.306 534.413 311.467C534.413 312.628 533.47 313.571 532.307 313.571H492.298C491.135 313.571 490.193 312.628 490.193 311.467Z"
fill="white"/>
<path
d="M490.193 322.706C490.193 321.544 491.135 320.602 492.298 320.602H521.778C522.941 320.602 523.884 321.544 523.884 322.706C523.884 323.867 522.941 324.809 521.778 324.809H492.298C491.135 324.809 490.193 323.867 490.193 322.706Z"
fill="white"/>
<path
d="M605.434 230.01C605.434 228.849 606.376 227.907 607.54 227.907H622.837C623.999 227.907 624.942 228.849 624.942 230.01C624.942 231.172 623.999 232.113 622.837 232.113H607.54C606.376 232.113 605.434 231.172 605.434 230.01Z"
fill="white"/>
<path
d="M594.798 321.76C597.736 321.76 600.117 319.381 600.117 316.447C600.117 313.513 597.736 311.135 594.798 311.135C591.86 311.135 589.479 313.513 589.479 316.447C589.479 319.381 591.86 321.76 594.798 321.76Z"
fill="white"/>
<path
d="M614.299 321.76C617.237 321.76 619.618 319.381 619.618 316.447C619.618 313.513 617.237 311.135 614.299 311.135C611.362 311.135 608.981 313.513 608.981 316.447C608.981 319.381 611.362 321.76 614.299 321.76Z"
fill="white"/>
<path
d="M522.998 222.594H498.166C495.721 222.594 493.739 224.576 493.739 227.021C493.739 229.466 495.721 231.448 498.166 231.448H522.998C525.443 231.448 527.425 229.466 527.425 227.021C527.425 224.576 525.443 222.594 522.998 222.594Z"
fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M619.618 209.313H499.057C489.755 209.313 482.214 216.845 482.214 226.136V281.031C482.214 290.322 489.755 297.854 499.057 297.854H619.618C628.92 297.854 636.461 290.322 636.461 281.031V226.136C636.461 216.845 628.92 209.313 619.618 209.313ZM499.057 208.428C489.266 208.428 481.328 216.356 481.328 226.136V281.031C481.328 290.811 489.266 298.74 499.057 298.74H619.618C629.41 298.74 637.348 290.811 637.348 281.031V226.136C637.348 216.356 629.41 208.428 619.618 208.428H499.057Z"
fill="white"/>
<path d="M481.608 329.104H403.034L413.968 382.744H492.543L481.608 329.104Z" fill="#BE7430"/>
<path d="M403.404 329.104H480.96L470.06 382.744H392.503L403.404 329.104Z" fill="#FF9330"/>
<path
d="M407.977 337.512C408.237 336.289 409.317 335.414 410.567 335.414H469.886C471.571 335.414 472.827 336.966 472.476 338.614L464.432 376.439C464.172 377.663 463.093 378.537 461.843 378.537H402.523C400.839 378.537 399.583 376.985 399.934 375.337L407.977 337.512Z"
fill="#FFB36C"/>
<path
d="M470.429 351.191L464.68 377.776C464.584 378.22 464.186 378.537 463.724 378.537H392.775C392.68 378.537 392.589 378.524 392.503 378.499V378.462C402.695 378.718 425.032 376.271 431.283 368.661C435.428 363.615 440.391 364.24 445.219 364.37C449.495 364.485 453.358 364.589 455.606 361.069C457.694 357.8 460.339 357.548 462.878 357.305C465.94 357.012 468.847 356.735 470.429 351.191Z"
fill="#F2A864"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M407.799 326.419C408.124 327.047 408.299 327.802 408.299 328.578H407.096C407.096 327.474 406.457 326.579 405.669 326.579C404.88 326.579 404.241 327.474 404.241 328.578C404.241 329.681 404.88 330.576 405.669 330.576C405.816 330.576 405.958 330.545 406.091 330.487L406.448 332.093C405.919 332.323 405.351 332.314 404.825 332.066C404.3 331.818 403.844 331.345 403.522 330.713C403.2 330.082 403.029 329.324 403.034 328.549C403.038 327.773 403.218 327.02 403.546 326.395C403.875 325.771 404.337 325.307 404.865 325.071C405.393 324.835 405.961 324.838 406.488 325.08C407.015 325.322 407.473 325.791 407.799 326.419Z"
fill="#C4C4C4"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M468.875 326.419C469.201 327.047 469.376 327.802 469.376 328.578H468.173C468.173 327.474 467.534 326.579 466.745 326.579C465.957 326.579 465.318 327.474 465.318 328.578C465.318 329.681 465.957 330.576 466.745 330.576C466.892 330.576 467.035 330.545 467.168 330.487L467.525 332.093C466.996 332.323 466.428 332.314 465.902 332.066C465.377 331.818 464.921 331.345 464.599 330.713C464.278 330.082 464.107 329.324 464.111 328.549C464.116 327.773 464.295 327.02 464.624 326.395C464.952 325.771 465.414 325.307 465.942 325.071C466.47 324.835 467.038 324.838 467.565 325.08C468.092 325.322 468.55 325.791 468.875 326.419Z"
fill="#C4C4C4"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M448.868 326.419C449.193 327.047 449.368 327.802 449.368 328.578H448.164C448.164 327.474 447.526 326.579 446.738 326.579C445.949 326.579 445.311 327.474 445.311 328.578C445.311 329.681 445.949 330.576 446.738 330.576C446.885 330.576 447.026 330.545 447.16 330.487L447.517 332.093C446.988 332.323 446.42 332.314 445.895 332.066C445.369 331.818 444.913 331.345 444.591 330.713C444.269 330.082 444.099 329.324 444.103 328.549C444.107 327.773 444.287 327.02 444.615 326.395C444.944 325.771 445.406 325.307 445.934 325.071C446.462 324.835 447.03 324.838 447.557 325.08C448.084 325.322 448.542 325.791 448.868 326.419Z"
fill="#C4C4C4"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M428.86 326.419C429.185 327.047 429.36 327.802 429.36 328.578H428.157C428.157 327.474 427.518 326.579 426.73 326.579C425.941 326.579 425.303 327.474 425.303 328.578C425.303 329.681 425.941 330.576 426.73 330.576C426.877 330.576 427.019 330.545 427.152 330.487L427.509 332.093C426.98 332.323 426.412 332.314 425.886 332.066C425.361 331.818 424.905 331.345 424.583 330.713C424.261 330.082 424.091 329.324 424.095 328.549C424.099 327.773 424.279 327.02 424.607 326.395C424.936 325.771 425.398 325.307 425.926 325.071C426.454 324.835 427.022 324.838 427.549 325.08C428.076 325.322 428.534 325.791 428.86 326.419Z"
fill="#C4C4C4"/>
<path
d="M412.929 343.52C413.143 342.475 414.06 341.725 415.123 341.725H418.696C420.113 341.725 421.174 343.03 420.889 344.423L420.518 346.24C420.304 347.285 419.387 348.036 418.324 348.036H414.751C413.334 348.036 412.273 346.731 412.558 345.338L412.929 343.52Z"
fill="#FFDAB8"/>
<path
d="M428.725 343.52C428.939 342.475 429.855 341.725 430.919 341.725H434.492C435.909 341.725 436.97 343.03 436.685 344.423L436.313 346.24C436.1 347.285 435.183 348.036 434.12 348.036H430.547C429.13 348.036 428.069 346.731 428.354 345.338L428.725 343.52Z"
fill="#FFDAB8"/>
<path
d="M443.468 343.52C443.682 342.475 444.598 341.725 445.661 341.725H449.235C450.652 341.725 451.713 343.03 451.428 344.423L451.056 346.24C450.843 347.285 449.926 348.036 448.863 348.036H445.29C443.872 348.036 442.811 346.731 443.096 345.338L443.468 343.52Z"
fill="#FFDAB8"/>
<path
d="M459.264 343.52C459.477 342.475 460.394 341.725 461.457 341.725H465.03C466.448 341.725 467.508 343.03 467.223 344.423L466.852 346.24C466.638 347.285 465.722 348.036 464.659 348.036H461.085C459.668 348.036 458.607 346.731 458.892 345.338L459.264 343.52Z"
fill="#FFDAB8"/>
<path
d="M409.77 355.09C409.984 354.045 410.9 353.294 411.964 353.294H415.537C416.954 353.294 418.015 354.599 417.73 355.992L417.359 357.81C417.145 358.855 416.228 359.605 415.165 359.605H411.592C410.175 359.605 409.114 358.3 409.399 356.907L409.77 355.09Z"
fill="#FFDAB8"/>
<path
d="M425.566 355.09C425.78 354.045 426.696 353.294 427.759 353.294H431.333C432.75 353.294 433.811 354.599 433.526 355.992L433.154 357.81C432.941 358.855 432.024 359.605 430.961 359.605H427.388C425.97 359.605 424.91 358.3 425.194 356.907L425.566 355.09Z"
fill="#FFDAB8"/>
<path
d="M440.309 355.09C440.522 354.045 441.439 353.294 442.502 353.294H446.075C447.493 353.294 448.554 354.599 448.269 355.992L447.897 357.81C447.683 358.855 446.767 359.605 445.703 359.605H442.13C440.713 359.605 439.652 358.3 439.937 356.907L440.309 355.09Z"
fill="#FFDAB8"/>
<path
d="M456.105 355.09C456.318 354.045 457.235 353.294 458.298 353.294H461.871C463.289 353.294 464.349 354.599 464.064 355.992L463.693 357.81C463.479 358.855 462.563 359.605 461.5 359.605H457.926C456.509 359.605 455.448 358.3 455.733 356.907L456.105 355.09Z"
fill="#FFDAB8"/>
<path
d="M406.611 366.659C406.825 365.614 407.741 364.864 408.804 364.864H412.378C413.795 364.864 414.856 366.169 414.571 367.562L414.199 369.38C413.986 370.425 413.069 371.175 412.006 371.175H408.433C407.015 371.175 405.955 369.87 406.239 368.477L406.611 366.659Z"
fill="#FFDAB8"/>
<path
d="M422.407 366.659C422.62 365.614 423.537 364.864 424.6 364.864H428.173C429.591 364.864 430.652 366.169 430.367 367.562L429.995 369.38C429.781 370.425 428.865 371.175 427.802 371.175H424.229C422.811 371.175 421.75 369.87 422.035 368.477L422.407 366.659Z"
fill="#FFDAB8"/>
<path
d="M437.15 366.659C437.363 365.614 438.28 364.864 439.343 364.864H442.916C444.334 364.864 445.394 366.169 445.11 367.562L444.738 369.38C444.524 370.425 443.608 371.175 442.545 371.175H438.971C437.554 371.175 436.493 369.87 436.778 368.477L437.15 366.659Z"
fill="#FFDAB8"/>
<path
d="M452.945 366.659C453.159 365.614 454.076 364.864 455.139 364.864H458.712C460.13 364.864 461.19 366.169 460.905 367.562L460.534 369.38C460.32 370.425 459.404 371.175 458.34 371.175H454.767C453.35 371.175 452.289 369.87 452.574 368.477L452.945 366.659Z"
fill="#FFDAB8"/>
<path
d="M322.344 639.673L309.917 629.034C303.305 623.375 304.57 606.09 304.57 606.09L286.798 605.129V624.32L283.915 634.396L320.903 641.592L322.344 639.673Z"
fill="#FFAE64"/>
<path
d="M316.14 651.188H328.015C329.56 651.188 330.988 650.368 331.767 649.036C332.943 647.023 332.322 644.436 330.381 643.145C328.032 641.584 324.606 639.264 320.423 636.315C312.256 630.557 308.894 626.719 308.894 626.719C308.894 626.719 304.09 629.598 296.885 629.598C289.68 629.598 285.837 624.32 285.837 624.32C282.745 632.04 282.634 640.632 285.526 648.429L285.837 649.268L299.767 650.229L302.169 648.309L308.522 650.122C311 650.829 313.563 651.188 316.14 651.188Z"
fill="#2C2770"/>
<path
d="M193.609 639.673L206.036 629.034C212.647 623.375 211.382 606.09 211.382 606.09L229.155 605.129V624.32L232.037 634.396L195.05 641.592L193.609 639.673Z"
fill="#FFAE64"/>
<path
d="M199.813 651.188H187.938C186.393 651.188 184.964 650.368 184.186 649.036C183.01 647.023 183.63 644.436 185.573 643.145C187.92 641.584 191.347 639.264 195.53 636.315C203.696 630.557 207.059 626.719 207.059 626.719C207.059 626.719 211.862 629.598 219.068 629.598C226.273 629.598 230.116 624.32 230.116 624.32C233.208 632.04 233.319 640.632 230.427 648.429L230.116 649.268L216.186 650.229L213.784 648.309L207.431 650.122C204.953 650.829 202.389 651.188 199.813 651.188Z"
fill="#2C2770"/>
<path
d="M234.813 339.669L238.762 320.144L318.981 317.745L323.936 355.358C325.433 366.723 325.917 378.198 325.381 389.649L314.658 618.564H286.317L280.553 389.231L238.762 618.564H208.5L232.037 398.347L231.822 396.055C230.054 377.215 231.061 358.217 234.813 339.669Z"
fill="#2C2770"/>
<mask id="mask6" mask-type="alpha" maskUnits="userSpaceOnUse" x="207" y="317" width="118"
height="302">
<path
d="M233.852 339.669L237.802 320.144L318.021 317.745L322.976 355.358C324.473 366.723 324.956 378.198 324.42 389.649L313.698 618.564H285.357L279.592 389.231L237.802 618.564H207.539L231.077 398.347L230.861 396.055C229.093 377.215 230.101 358.217 233.852 339.669Z"
fill="white"/>
</mask>
<g mask="url(#mask6)">
<path
d="M243.566 318.225C241.004 331.019 235.88 367.737 235.88 412.26C235.88 412.26 219.869 568.987 211.863 619.523"
stroke="white" stroke-width="1.053"/>
<path
d="M281.034 389.711C283.916 396.908 289.776 420.896 290.16 459.278C290.545 497.661 290.32 581.461 290.16 618.564"
stroke="white" stroke-width="1.053"/>
</g>
<path
d="M378.719 192.924L355.485 204.712L358.039 230.283L382.401 219.544C395.64 213.707 404.953 201.493 407.064 187.194L415.407 130.703L422.123 123.064L427.336 116.313C429.122 114 429.782 111.012 429.137 108.164C428.669 106.097 427.271 104.362 425.35 103.464L415.653 98.9286L422.041 83.5395C422.662 82.045 421.801 80.3499 420.227 79.9672C419.08 79.6881 417.887 80.2039 417.305 81.2303L407.51 98.5094L404.769 98.0523C403.406 97.8248 402.051 98.5103 401.429 99.7439L398.25 106.04C397.369 107.784 397.124 109.78 397.555 111.685L400.057 122.731L378.719 192.924Z"
fill="#FF9330"/>
<path
d="M422.313 102.109L416.552 113.152C415.98 114.249 414.656 114.715 413.522 114.22C412.445 113.749 411.881 112.555 412.202 111.425L413.157 108.062"
stroke="black" stroke-width="1.053"/>
<path
d="M427.57 104.689L420.619 115.143C419.964 116.127 418.658 116.437 417.63 115.852C416.586 115.257 416.194 113.946 416.742 112.876L417.017 112.338"
stroke="black" stroke-width="1.053"/>
<path
d="M428.461 112.699L422.809 120.618C422.093 121.622 420.732 121.922 419.658 121.311C418.622 120.721 418.175 119.461 418.609 118.35L419.654 115.67"
stroke="black" stroke-width="1.053"/>
<path
d="M409.459 99.3599L419.27 103.524C420.097 103.875 420.509 104.806 420.213 105.653C419.574 107.484 417.779 108.652 415.843 108.496L406.869 107.771C408.608 108.894 410.978 115.099 411.22 121.53"
stroke="black" stroke-width="1.053"/>
<path
d="M237.552 215.097L259.298 209.397H315.578C322.361 209.397 329.111 208.462 335.637 206.618L368.551 197.322L374.315 226.814L325.801 256.206L321.44 274.167L325.256 301.206C326.799 312.132 318.162 321.833 307.118 321.578L242.002 320.078C237.235 319.969 233.51 315.932 233.786 311.178L235.913 274.651L228.227 289.406L200.427 283.419L210.341 242.761C213.636 229.248 224.083 218.627 237.552 215.097Z"
fill="#54B7FF"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M259.298 209.397L237.552 215.097C224.083 218.627 213.636 229.248 210.341 242.761L200.427 283.419L228.227 289.406L235.913 274.651L201.395 486.704C200.883 489.851 203.27 492.726 206.461 492.806L231.97 493.441L264.031 374.856V216.563H291.937L298.255 374.856L316.099 495.535L342.187 496.185C345.397 496.265 347.931 493.482 347.546 490.298L321.44 274.167L325.801 256.206L374.315 226.814L368.551 197.322L335.637 206.618C329.111 208.462 322.361 209.397 315.578 209.397H259.298Z"
fill="#FFE071"/>
<path d="M301.941 166.078L288.251 163.974" stroke="black" stroke-width="1.053"/>
<path
d="M259.448 204.591C261.426 199.656 261.097 183.615 260.684 176.211C269.752 177.857 287.516 183.985 286.032 195.337C284.549 206.688 288.711 209.527 290.979 209.527C293.451 214.873 295.058 226.184 281.704 228.651C268.351 231.119 257.181 219.396 253.266 213.228C254.502 212.405 257.47 209.527 259.448 204.591Z"
fill="#FF9330"/>
<mask id="mask7" mask-type="alpha" maskUnits="userSpaceOnUse" x="253" y="176" width="40"
height="53">
<path
d="M259.779 204.582C261.745 199.708 261.417 183.866 261.008 176.555C270.018 178.179 287.669 184.232 286.195 195.443C284.72 206.654 288.857 209.457 291.109 209.457C293.567 214.737 295.163 225.908 281.895 228.345C268.625 230.782 257.527 219.206 253.636 213.113C254.865 212.3 257.813 209.457 259.779 204.582Z"
fill="white"/>
</mask>
<g mask="url(#mask7)">
<path
d="M287.883 205.331C267.879 203.159 261.428 186.328 260.703 178.184L287.883 193.386C296.218 198.273 307.887 207.503 287.883 205.331Z"
fill="#D76767"/>
</g>
<path
d="M261.228 177.896C260.825 184.659 269.081 196.955 284.185 196.34C293.003 195.982 295.23 185.913 297.476 174.822C299.078 166.915 302.106 158.489 301.101 152.688L287.81 148.386C289.622 150.845 289.027 150.301 282.986 150.301C277.008 150.301 274.671 146.887 272.455 145.042C271.248 152.42 261.925 144.696 261.925 148.386C261.925 151.336 264.031 170.66 264.031 172.914C261.925 172.914 263.018 166.078 261.228 164.985C259.214 163.755 255.187 162.895 255.187 169.289C255.187 175.682 259.214 177.691 261.228 177.896Z"
fill="#FF9330"/>
<path
d="M261.925 172.914C261.925 172.914 262.978 164.5 259.819 164.5C253.159 164.5 255.607 172.388 255.607 172.388C249.464 160.819 251.394 139.257 269.296 133.998C296.315 126.061 308.786 151.132 300.888 152.404C284.565 155.034 272.455 147.145 270.349 148.723C268.243 150.301 266.137 165.026 265.084 170.285C264.241 174.492 262.802 174.141 261.925 172.914Z"
fill="#2C2770"/>
<path
d="M279.641 180.967L286.366 184.325C285.886 185.284 284.348 186.532 282.043 185.764C279.737 184.996 279.481 182.565 279.641 180.967Z"
fill="white"/>
<path
d="M294.051 160.815H298.858C299.653 160.815 300.297 161.46 300.297 162.255C300.297 163.05 299.653 163.694 298.858 163.694H294.051C293.256 163.694 292.611 163.05 292.611 162.255C292.611 161.46 293.256 160.815 294.051 160.815Z"
fill="#2C2770"/>
<path
d="M274.837 160.815H279.644C280.439 160.815 281.083 161.46 281.083 162.255C281.083 163.05 280.439 163.694 279.644 163.694H274.837C274.042 163.694 273.397 163.05 273.397 162.255C273.397 161.46 274.042 160.815 274.837 160.815Z"
fill="#2C2770"/>
<path
d="M277.72 169.544C276.924 169.544 276.279 168.899 276.279 168.104C276.279 167.309 276.924 166.665 277.72 166.665C278.515 166.665 279.161 167.309 279.161 168.104C279.161 168.899 278.515 169.544 277.72 169.544Z"
fill="black"/>
<path d="M288.768 162.735V180.006H284.445" stroke="black" stroke-width="1.053"/>
<path
d="M293.867 169.544C293.071 169.544 292.426 168.899 292.426 168.104C292.426 167.309 293.071 166.665 293.867 166.665C294.663 166.665 295.308 167.309 295.308 168.104C295.308 168.899 294.663 169.544 293.867 169.544Z"
fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M286.145 167.655C286.145 171.141 283.317 173.966 279.827 173.966C276.338 173.966 273.508 171.141 273.508 167.655C273.508 166.782 273.685 166.147 273.971 165.677C274.254 165.21 274.667 164.866 275.21 164.613C276.33 164.089 277.928 163.974 279.827 163.974C281.726 163.974 283.325 164.089 284.443 164.613C284.987 164.866 285.401 165.21 285.684 165.677C285.968 166.147 286.145 166.782 286.145 167.655Z"
stroke="black" stroke-width="1.053"/>
<path d="M289.767 165.677C288.587 164.58 287.226 164.58 285.684 165.677" stroke="black"
stroke-width="1.053"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M301.941 167.655C301.941 171.141 299.112 173.966 295.623 173.966C292.134 173.966 289.304 171.141 289.304 167.655C289.304 166.782 289.481 166.147 289.766 165.677C290.05 165.21 290.463 164.866 291.006 164.613C292.125 164.089 293.724 163.974 295.623 163.974C297.522 163.974 299.121 164.089 300.239 164.613C300.782 164.866 301.196 165.21 301.48 165.677C301.764 166.147 301.941 166.782 301.941 167.655Z"
stroke="black" stroke-width="1.053"/>
<path d="M273.508 166.078L259.819 163.974" stroke="black" stroke-width="1.053"/>
<path
d="M367.471 266.481L313.731 273.985C312.968 274.091 312.325 274.604 312.052 275.324L297.379 314.084C296.815 315.573 298.049 317.125 299.629 316.912L352.76 309.783C353.497 309.684 354.128 309.204 354.419 308.52L370.742 270.079C371.137 269.148 370.814 268.069 369.971 267.507L368.931 266.815C368.501 266.529 367.982 266.41 367.471 266.481Z"
fill="#2C2770"/>
<path
d="M368.604 268.71L317.691 275.829C315.785 276.095 314.177 277.379 313.496 279.177L301.357 311.24C299.947 314.964 303.033 318.842 306.984 318.312L353.041 312.132C354.906 311.882 356.496 310.656 357.211 308.918L371.965 273.034C372.891 270.78 371.02 268.373 368.604 268.71Z"
fill="#6B8CFF"/>
<path
d="M339.734 297.607C342.056 295.858 342.661 292.746 341.087 290.658C339.513 288.57 336.355 288.295 334.034 290.044C331.713 291.794 331.107 294.905 332.681 296.993C334.256 299.082 337.413 299.357 339.734 297.607Z"
fill="#2C2770"/>
<path
d="M283.391 298.021L226.648 289.136L200.848 283.351L198.171 297.062C196.272 306.786 203.71 315.83 213.629 315.858L279.17 316.046L285.646 319.763C285.838 319.872 286.038 319.967 286.244 320.046L295.107 323.421C295.211 323.46 295.317 323.491 295.425 323.514L310.126 326.643C310.892 326.806 311.681 326.506 312.142 325.875C312.976 324.734 312.382 323.113 311.008 322.78L300.918 320.334H316.606C317.206 320.334 317.758 320.005 318.042 319.478C318.598 318.451 317.925 317.191 316.761 317.078L304.181 315.872L322.734 314.236C323.45 314.172 324.074 313.72 324.355 313.06C324.883 311.822 324.023 310.435 322.679 310.356L305.92 309.362L321.532 306.904C322.598 306.735 323.425 305.883 323.561 304.814C323.741 303.394 322.655 302.127 321.222 302.086L300.918 301.511L302.448 300.553L305.608 292.139L296.113 293.724C295.644 293.803 295.181 293.92 294.732 294.077L283.391 298.021Z"
fill="#FF9330"/>
<path
d="M724.215 119.799H566.257C562.186 119.799 558.886 123.099 558.886 127.17V206.037C558.886 210.108 562.186 213.408 566.257 213.408H724.215C728.286 213.408 731.586 210.108 731.586 206.037V127.17C731.586 123.099 728.286 119.799 724.215 119.799Z"
fill="white" stroke="#E5E5E5" stroke-width="2.106"/>
<path d="M608.379 161.87H635.759C634.758 146.66 623.044 134.509 608.379 133.472V161.87Z"
fill="#1BE3B3"/>
<path d="M633.09 187.113C637.616 182.343 640.541 176.115 641.024 169.233H614.697L633.09 187.113Z"
fill="#FFAE64"/>
<path
d="M603.926 166.04V133.472C588.179 134.555 575.734 147.774 575.734 163.934C575.734 180.801 589.292 194.476 606.015 194.476C613.313 194.476 620.002 191.865 625.228 187.526L603.926 166.04Z"
fill="#FF8282"/>
<path d="M702.101 174.492H655.766V182.906H702.101V174.492Z" fill="#A3B7FF"/>
<path d="M689.464 159.767H654.714V168.181H689.464V159.767Z" fill="#A3B7FF"/>
<path d="M721.056 143.99H655.766V152.404H721.056V143.99Z" fill="#A3B7FF"/>
<path d="M856.9 524.735H841.104V635.172H856.9V524.735Z" fill="#CCD9FF"/>
<path d="M830.573 498.44H868.483L864.271 535.253H834.786L830.573 498.44Z" fill="#A3BAFF"/>
<mask id="mask8" mask-type="alpha" maskUnits="userSpaceOnUse" x="830" y="498" width="39"
height="38">
<path d="M830.573 498.44H868.483L864.271 535.253H834.786L830.573 498.44Z" fill="white"/>
</mask>
<g mask="url(#mask8)">
<path d="M874.802 499.492L822.149 496.863L827.414 507.906L870.589 517.373L874.802 499.492Z"
fill="black"/>
</g>
<path d="M897.969 479.508H798.982V503.699H897.969V479.508Z" fill="#CCD9FF"/>
<path d="M906.393 632.016H790.557V641.482H906.393V632.016Z" fill="#CCD9FF"/>
<path d="M906.393 638.327H790.557V641.482H906.393V638.327Z" fill="#2C2770"/>
<path
d="M621.18 605.706L637.237 602.537C645.778 600.851 653.179 585.174 653.179 585.174L669.132 593.059L659.692 609.775L657.247 619.965L621.491 608.084L621.18 605.706Z"
fill="#FFAE64"/>
<path
d="M624.078 608.186L656.753 616.933C657.997 620.625 656.859 624.703 653.882 627.221L648.258 631.976L635.652 625.977L636.467 622.581L628.977 621.413C625.886 620.932 622.906 619.899 620.181 618.363L610.535 612.93C609.215 612.186 608.39 610.798 608.368 609.284C608.334 606.948 610.193 605.02 612.531 604.968L628.979 604.591L624.302 605.132C623.351 605.242 622.722 606.176 622.979 607.099C623.127 607.63 623.545 608.043 624.078 608.186Z"
fill="#2C2770"/>
<path
d="M901.669 411.982L900.032 406.935H854.224L846.23 429.353L765.917 453.277C742.196 458.293 721.389 472.395 707.958 492.557L642.034 591.523L664.674 604.144L764.714 497.914L854.533 486.325C867.614 484.637 879.711 478.49 888.779 468.923C903.181 453.728 908.125 431.889 901.669 411.982Z"
fill="#2C2770"/>
<path
d="M893.714 406.935V430.527C893.714 454.91 875.548 475.483 851.327 478.53L755.763 490.552L662.042 603.092"
stroke="white" stroke-width="1.053"/>
<path
d="M744.722 627.888L754.036 614.446C758.992 607.295 753.373 590.897 753.373 590.897L770.32 585.461L775.2 604.023L780.548 613.037L746.604 629.378L744.722 627.888Z"
fill="#FFAE64"/>
<path
d="M748.531 627.652L778.08 611.204C781.548 612.99 783.574 616.708 783.192 620.585L782.472 627.91L769.242 632.371L767.459 629.366L761.269 633.738C758.714 635.543 755.856 636.873 752.83 637.666L742.118 640.476C740.652 640.86 739.093 640.438 738.022 639.367C736.368 637.714 736.358 635.038 738 633.373L749.542 621.664L746.563 625.304C745.956 626.045 746.157 627.154 746.984 627.637C747.461 627.914 748.049 627.921 748.531 627.652Z"
fill="#2C2770"/>
<path
d="M739.08 440.618L820.526 407.987H855.804L845.8 436.385L763.135 454.265L776.298 596.782H750.498L718.791 481.45C716.497 473.104 717.453 464.2 721.468 456.53C725.251 449.299 731.498 443.656 739.08 440.618Z"
fill="#2C2770"/>
<path d="M765.767 455.317L753.657 462.154L769.453 598.359" stroke="white" stroke-width="1.053"/>
<path d="M850.538 419.031L854.224 407.987H841.587L765.767 455.317L850.538 419.031Z" fill="black"
stroke="black" stroke-width="1.053"/>
<path
d="M943.937 346.555L921.943 336.144L904.457 352.576L924.918 363.656C937.67 370.561 953.088 370.465 965.752 363.401L1010.97 338.181L1020.47 338.781H1028.44C1031.17 338.781 1033.77 337.567 1035.51 335.471C1036.77 333.95 1037.26 331.933 1036.83 330.006L1034.65 320.281L1049.7 316.232C1051.16 315.839 1051.93 314.24 1051.31 312.862C1050.86 311.859 1049.8 311.273 1048.71 311.429L1030.32 314.044L1029.17 311.904C1028.51 310.686 1027.14 310.035 1025.77 310.296L1019.65 311.467C1017.73 311.836 1015.99 312.861 1014.74 314.366L1008.12 322.334L943.937 346.555Z"
fill="#FFAE64"/>
<path
d="M1036.1 326.998L1024.63 329.037C1023.49 329.239 1022.39 328.529 1022.11 327.412C1021.84 326.35 1022.4 325.255 1023.42 324.849L1026.46 323.64"
stroke="black" stroke-width="1.053"/>
<path
d="M1037.19 332.339L1025.47 333.166C1024.37 333.243 1023.4 332.458 1023.24 331.368C1023.09 330.26 1023.84 329.225 1024.94 329.021L1025.5 328.917"
stroke="black" stroke-width="1.053"/>
<path
d="M1031.76 337.553L1022.67 337.895C1021.52 337.939 1020.52 337.107 1020.36 335.969C1020.2 334.868 1020.88 333.822 1021.95 333.51L1024.53 332.755"
stroke="black" stroke-width="1.053"/>
<path
d="M1030.8 315.963L1033.31 325.561C1033.52 326.37 1033.07 327.203 1032.27 327.467C1030.55 328.038 1028.66 327.38 1027.67 325.866L1023.09 318.841C1023.25 320.761 1020 326.039 1015.38 329.877"
stroke="black" stroke-width="1.053"/>
<path
d="M771.586 297.811L794.912 304.775L796.816 328.642L774.176 323.197C760.066 319.804 748.694 309.435 744.057 295.74L727.501 246.834L720.056 240.941L714.139 235.614C712.112 233.789 711.008 231.159 711.127 228.441C711.214 226.469 712.211 224.647 713.828 223.506L721.993 217.745L713.558 204.693C712.739 203.425 713.248 201.729 714.632 201.118C715.641 200.672 716.824 200.948 717.53 201.792L729.41 216.014L731.704 215.195C733.011 214.729 734.47 215.164 735.304 216.267L739.054 221.222C740.234 222.782 740.833 224.702 740.747 226.654L740.292 236.987L771.586 297.811Z"
fill="#FFAE64"/>
<path
d="M716.396 221.762L723.531 230.934C724.238 231.844 725.533 232.053 726.494 231.411C727.407 230.802 727.727 229.614 727.244 228.63L725.806 225.706"
stroke="black" stroke-width="1.053"/>
<path
d="M711.99 224.996L720.122 233.431C720.887 234.225 722.139 234.293 722.988 233.587C723.85 232.868 723.991 231.602 723.31 230.712L722.967 230.264"
stroke="black" stroke-width="1.053"/>
<path
d="M712.504 232.487L719.017 238.813C719.843 239.615 721.145 239.664 722.03 238.927C722.885 238.214 723.087 236.985 722.504 236.039L721.096 233.755"
stroke="black" stroke-width="1.053"/>
<path
d="M727.76 217.116L719.43 222.556C718.729 223.014 718.504 223.936 718.917 224.663C719.808 226.236 721.655 227.011 723.408 226.547L731.54 224.399C730.128 225.715 728.98 231.797 729.826 237.733"
stroke="black" stroke-width="1.053"/>
<path
d="M899.247 311.13L879.427 304.094C873.536 304.094 858.218 303.798 844.079 302.613C829.941 301.428 804.314 302.613 783.695 299.651L779.277 326.313C788.113 330.757 801.368 335.201 819.041 338.164C819.041 351.467 821.988 358.553 821.988 364.825L826.406 384.082L815.699 409.039H902.991L893.954 381.693L899.255 351.467L917.719 363.344L935.393 341.125L908.552 316.702C905.855 314.247 902.691 312.353 899.247 311.13Z"
fill="#6B8CFF"/>
<mask id="mask9" mask-type="alpha" maskUnits="userSpaceOnUse" x="779" y="299" width="157"
height="111">
<path
d="M899.247 311.13L879.427 304.094C873.536 304.094 858.218 303.798 844.079 302.613C829.94 301.428 804.314 302.613 783.695 299.651L779.277 326.313C788.113 330.757 801.368 335.201 819.041 338.164C813.15 355.939 817.078 358.901 821.988 364.825L826.406 384.082L815.699 409.039H902.991L893.954 381.693L899.255 351.467L917.719 363.344L935.393 341.125L908.552 316.702C905.855 314.247 902.691 312.353 899.247 311.13Z"
fill="white"/>
</mask>
<g mask="url(#mask9)">
<path
d="M810.113 286.217C817.342 315.003 738.802 333.235 744.101 367.779C749.402 402.322 883.354 483.884 931.056 458.935C978.758 433.987 976.831 390.328 886.245 392.247C795.659 394.167 774.458 354.345 796.141 333.235C817.824 312.125 881.426 376.894 883.354 350.027C885.281 323.16 803.85 315.003 836.615 295.333C863.466 279.213 874.199 329.397 903.591 335.634C927.105 340.624 940.051 331.956 943.584 326.998C965.909 337.712 999.863 362.117 957.076 374.016C914.288 385.914 841.273 370.657 810.113 361.542"
stroke="#A3B7FF" stroke-width="7.371"/>
</g>
<path
d="M833.91 410.461L840.452 301.757L773.376 295.447L742.743 236.021L724.257 246.539L740.317 297.781C744.794 312.067 756.232 323.124 770.706 327.16L813.516 339.096L820.91 382.219L814.413 410.423L833.91 410.461Z"
fill="#FF6767"/>
<path
d="M884.817 440.067L888.514 305.439L899.161 308.7C905.714 310.708 911.739 314.135 916.806 318.734L945.027 344.355L1002.6 321.741L1008.93 341.199L968.178 366.442C952.072 376.418 931.665 376.384 915.592 366.356L902.246 358.028L898.021 382.745L909.112 445.325L884.817 440.067Z"
fill="#FF6767"/>
<path d="M832.001 301.231L825.135 323.319L838.867 322.267" stroke="black" stroke-width="1.053"/>
<path d="M895.908 307.542L902.774 324.371L887.986 322.267" stroke="black" stroke-width="1.053"/>
<path
d="M834.133 243.2C835.319 233.751 844.511 232.373 848.959 232.866C851.43 231.389 856.373 225.484 866.751 226.96C877.13 228.437 888.992 234.342 886.026 247.628C886.026 247.628 888.514 273.213 869.538 273.213C869.538 279.144 854.712 309.266 854.712 291.24C854.712 273.213 857.251 286.792 845.205 273.213C837.811 270.584 832.65 255.01 834.133 243.2Z"
fill="#2C2770"/>
<path
d="M846.262 234.444C836.636 234.444 828.832 226.674 828.832 217.089C828.832 207.505 836.636 199.735 846.262 199.735C855.887 199.735 863.691 207.505 863.691 217.089C863.691 226.674 855.887 234.444 846.262 234.444Z"
fill="#2C2770"/>
<path
d="M841.546 253.534V257.963C839.569 257.471 835.615 257.372 835.615 260.915C835.615 265.344 838.581 268.297 841.546 266.821C841.931 271.422 844.417 280.505 851.809 284.292C851.544 287.761 850.77 292.742 848.959 296.346C846.587 301.071 842.04 303.235 840.063 303.727C843.028 307.172 851.332 314.062 860.821 314.062C870.31 314.062 877.625 307.172 880.095 303.727C878.612 303.235 874.758 301.366 871.199 297.823C868.398 295.033 867.863 288.402 868.004 283.546C874.165 279.419 876.044 271.142 877.13 266.821C879.107 267.313 883.06 267.116 883.06 262.391C883.06 257.667 878.612 257.471 877.13 257.963V253.534C874.164 254.026 866.455 253.829 859.338 249.105C852.221 244.381 850.442 242.216 850.442 241.723C850.442 244.184 849.352 248.714 847.476 250.581C844.511 253.534 842.535 253.042 841.546 253.534Z"
fill="#FFAE64"/>
<path
d="M853.961 273.743L860.708 277.102C860.225 278.061 858.684 279.308 856.371 278.54C854.058 277.774 853.801 275.342 853.961 273.743Z"
fill="white"/>
<path
d="M868.411 253.593H873.241C874.036 253.593 874.681 254.237 874.681 255.032C874.681 255.827 874.036 256.471 873.241 256.471H868.411C867.616 256.471 866.971 255.827 866.971 255.032C866.971 254.237 867.616 253.593 868.411 253.593Z"
fill="#2C2770"/>
<path
d="M849.137 253.593H853.968C854.763 253.593 855.407 254.237 855.407 255.032C855.407 255.827 854.763 256.471 853.968 256.471H849.137C848.342 256.471 847.698 255.827 847.698 255.032C847.698 254.237 848.342 253.593 849.137 253.593Z"
fill="#2C2770"/>
<path
d="M852.034 261.269C851.236 261.269 850.589 260.624 850.589 259.829C850.589 259.035 851.236 258.39 852.034 258.39C852.833 258.39 853.48 259.035 853.48 259.829C853.48 260.624 852.833 261.269 852.034 261.269Z"
fill="black"/>
<path d="M863.116 255.512V272.784H858.78" stroke="black" stroke-width="1.053"/>
<path
d="M870.345 261.269C869.546 261.269 868.899 260.624 868.899 259.829C868.899 259.035 869.546 258.39 870.345 258.39C871.143 258.39 871.79 259.035 871.79 259.829C871.79 260.624 871.143 261.269 870.345 261.269Z"
fill="black"/>
<path d="M839.025 260.789C838.543 261.909 838.446 264.244 841.916 264.628" stroke="black"
stroke-width="1.053"/>
<path d="M880.219 260.789C880.701 261.909 880.797 264.244 877.328 264.628" stroke="black"
stroke-width="1.053"/>
<mask id="mask10" mask-type="alpha" maskUnits="userSpaceOnUse" x="835" y="241" width="49"
height="74">
<path
d="M841.546 253.534V257.963C839.569 257.471 835.615 257.372 835.615 260.915C835.615 265.344 838.581 268.297 841.546 266.821C841.931 271.422 844.417 280.505 851.809 284.292C851.544 287.761 850.77 292.742 848.959 296.346C846.587 301.071 842.04 303.235 840.063 303.728C843.028 307.172 851.332 314.062 860.821 314.062C870.31 314.062 877.625 307.172 880.095 303.728C878.612 303.235 874.758 301.366 871.199 297.823C868.398 295.033 867.863 288.402 868.004 283.546C874.165 279.419 876.044 271.142 877.13 266.821C879.107 267.313 883.06 267.116 883.06 262.391C883.06 257.667 878.612 257.471 877.13 257.963V253.534C874.164 254.026 866.455 253.829 859.338 249.105C852.221 244.381 850.442 242.216 850.442 241.723C850.442 244.184 849.352 248.714 847.476 250.581C844.511 253.534 842.535 253.042 841.546 253.534Z"
fill="white"/>
</mask>
<g mask="url(#mask10)">
<path
d="M869.38 282.859C868.095 283.658 863.983 285.738 859.743 285.738C855.719 285.738 851.954 284.441 850.758 283.898L850.588 283.818C850.638 283.843 850.694 283.87 850.758 283.898L869.863 292.934L869.38 282.859Z"
fill="#FF8282"/>
</g>
<path
d="M211.002 351.121C218.795 341.921 238.471 345.3 245.605 348.881C226.761 383.129 210.138 358.943 204.364 394.166C201.893 409.241 183.472 394.904 184.889 421.359C186.118 444.313 170.367 446.303 168.529 458.675C166.692 471.046 161.718 481.552 151.015 485.153C135.374 490.414 123.357 471.394 135.169 462.449C146.981 453.506 123.973 440.259 142.083 429.681C160.195 419.103 140.781 394.609 163.6 388.277C186.419 381.943 178.027 368.444 185.4 361.34C192.774 354.235 201.26 362.622 211.002 351.121Z"
fill="#6B8CFF"/>
<path
d="M129.734 279.587C133.699 266.522 152.104 268.017 160.002 273.802C148.945 283.794 132.324 283.411 142.046 322.78C146.207 339.629 123.104 332.605 136.359 359.627C147.859 383.075 129.537 392.058 133.149 405.8C136.76 419.544 139.205 432.535 129.734 441.035C115.893 453.458 99.5566 437.731 107.795 423.155C116.033 408.579 89.1912 405.94 103.219 386.868C117.247 367.797 87.0038 354.68 107.795 337.961C128.586 321.241 106.57 312.889 111.035 302.2C115.499 291.512 124.777 295.919 129.734 279.587Z"
fill="#1BE3B3"/>
<path
d="M29.6166 351.997C22.49 344.585 5.89418 348.5 0 351.922C17.8609 380.054 30.7637 358.604 37.5336 388.257C40.4312 400.947 55.383 387.779 55.5636 410.355C55.7203 429.943 69.2517 430.802 71.4672 441.227C73.6826 451.652 78.4739 460.325 87.7874 462.82C101.398 466.468 110.644 449.654 100.105 442.673C89.5659 435.691 108.485 423.206 92.4912 415.167C76.4967 407.129 91.7613 385.27 71.9759 381.091C52.1907 376.911 58.6363 364.986 51.9776 359.334C45.319 353.681 38.5248 361.263 29.6166 351.997Z"
fill="#54B7FF"/>
<path
d="M121.354 501.296C119.515 459.863 111.884 345.785 135.255 289.694C137.293 284.804 141.573 274.328 152.63 274.328"
stroke="#4F4F4F" stroke-width="1.053"/>
<path d="M99.6992 512.335C98.6375 472.751 81.7297 386.336 22.5924 357.345" stroke="#4F4F4F"
stroke-width="1.053"/>
<path d="M139.647 512.669C143.381 469.708 167.393 376.868 233.568 349.198" stroke="#4F4F4F"
stroke-width="1.053"/>
<path
d="M86.8145 493.624H149.998L166.29 615.667C168.815 634.584 154.082 651.391 134.975 651.391H101.837C82.7306 651.391 67.9972 634.584 70.5224 615.667L86.8145 493.624Z"
fill="#A3BAFF"/>
<path d="M158.422 493.624H77.337V505.194H158.422V493.624Z" fill="#CCD9FF"/>
</svg>

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,242 @@
// src/components/icon/IconifyPicker.tsx
import React, { useState, useEffect, useRef } from 'react';
import { Tabs, Divider, Row, Col, Typography, Card, Input } from 'antd';
import styled from 'styled-components';
const { TabPane } = Tabs;
const { Text } = Typography;
const { Search } = Input;
// 一级分类类型定义
type MainCategoryKey = string;
// 二级分类类型定义
type SubCategoryKey = string;
// 本地 SVG 图标组件
interface IconProps {
mainCategory: MainCategoryKey;
subCategory: SubCategoryKey;
name: string;
size?: string;
}
const LocalSvgIcon = ({ mainCategory, subCategory, name, size = '32px' }: IconProps) => {
return (
<img
src={`/icons/${mainCategory}/${subCategory}/${name}.svg`}
alt={name}
width={size}
height={size}
style={{ display: 'block', margin: 'auto' }}
/>
);
};
// IconifyPicker 组件的 Props 定义
interface IconifyPickerProps {
onIconSelect: (icon: { mainCategory: string; subCategory: string; name: string }) => void; // 回调函数
}
export const IconifyPicker = ({ onIconSelect }: IconifyPickerProps) => {
const [mainCategories, setMainCategories] = useState<string[]>([]); // 一级分类
const [activeMainCategory, setActiveMainCategory] = useState<MainCategoryKey | null>(null); // 当前选中的一级分类
const [subCategories, setSubCategories] = useState<SubCategoryKey[]>([]); // 二级分类
const [activeSubCategory, setActiveSubCategory] = useState<SubCategoryKey | null>(null); // 当前选中的二级分类
const [iconsData, setIconsData] = useState<string[]>([]); // 图标列表
const [filteredIcons, setFilteredIcons] = useState<string[]>([]); // 用于存储过滤后的图标
const [searchTerm, setSearchTerm] = useState(''); // 搜索关键词
const subCategoriesCache = useRef<Record<MainCategoryKey, SubCategoryKey[]>>({}); // 缓存二级分类
const iconsCache = useRef<Record<MainCategoryKey, Record<SubCategoryKey, string[]>>>({}); // 缓存图标数据
// 获取一级分类
const fetchMainCategories = async () => {
try {
const response = await fetch(`/api/get-icons`);
const data = await response.json();
if (response.ok) {
setMainCategories(data.mainCategories); // 使用正确的 API 字段
if (data.mainCategories.length > 0) {
setActiveMainCategory(data.mainCategories[0]); // 自动选择第一个一级分类
}
}
} catch (error) {
console.error('Failed to fetch main categories:', error);
}
};
// 获取二级分类
const fetchSubCategories = async (mainCategory: MainCategoryKey) => {
// 如果缓存中有数据,直接使用缓存
if (subCategoriesCache.current[mainCategory]) {
setSubCategories(subCategoriesCache.current[mainCategory]);
setActiveSubCategory(subCategoriesCache.current[mainCategory][0]);
return;
}
try {
const response = await fetch(`/api/get-icons?category=${mainCategory}`);
const data = await response.json();
if (response.ok) {
subCategoriesCache.current[mainCategory] = data.subcategories; // 缓存二级分类
setSubCategories(data.subcategories); // 设置二级分类
if (data.subcategories.length > 0) {
setActiveSubCategory(data.subcategories[0]); // 自动选择第一个二级分类
}
}
} catch (error) {
console.error('Failed to fetch subcategories:', error);
}
};
// 获取图标列表
const fetchIcons = async (mainCategory: MainCategoryKey, subCategory: SubCategoryKey) => {
// 如果缓存中有图标数据,直接使用缓存
if (iconsCache.current[mainCategory] && iconsCache.current[mainCategory][subCategory]) {
setIconsData(iconsCache.current[mainCategory][subCategory]);
setFilteredIcons(iconsCache.current[mainCategory][subCategory]);
return;
}
try {
const response = await fetch(`/api/get-icons?category=${mainCategory}&subcategory=${subCategory}`);
const data = await response.json();
if (response.ok) {
if (!iconsCache.current[mainCategory]) {
iconsCache.current[mainCategory] = {};
}
iconsCache.current[mainCategory][subCategory] = data.icons; // 缓存图标数据
setIconsData(data.icons); // 设置图标列表
setFilteredIcons(data.icons); // 初始化时显示所有图标
}
} catch (error) {
console.error('Failed to fetch icons:', error);
}
};
// 当页面加载时,获取一级分类
useEffect(() => {
fetchMainCategories();
}, []);
// 当用户切换一级分类时,获取二级分类
useEffect(() => {
if (activeMainCategory) {
fetchSubCategories(activeMainCategory);
}
}, [activeMainCategory]);
// 当用户切换二级分类时,获取该分类下的图标
useEffect(() => {
if (activeMainCategory && activeSubCategory) {
fetchIcons(activeMainCategory, activeSubCategory);
}
}, [activeMainCategory, activeSubCategory]);
// 当搜索内容变化时,过滤当前分类的图标
useEffect(() => {
const filtered = iconsData.filter((icon) =>
icon.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredIcons(filtered);
}, [searchTerm, iconsData]);
// 根据不同的图标库,生成带有前缀的图标名称
const formatIconName = (mainCategory: string, iconName: string) => {
if (mainCategory === 'font-awesome6') {
return `fa6-brands:${iconName}`; // 根据 font-awesome6 格式化图标名称
}
// 默认返回 icon 名称
return iconName;
};
// 处理图标选择
const handleIconSelect = (name: string) => {
if (activeMainCategory && activeSubCategory) {
const formattedName = formatIconName(activeMainCategory, name); // 格式化图标名称
onIconSelect({
mainCategory: activeMainCategory,
subCategory: activeSubCategory,
name: formattedName, // 传递格式化后的名称
});
}
};
return (
<Card title="图标选择" style={{ width: 500, margin: 'auto', marginTop: 20 }}>
{/* 一级分类 */}
<Tabs
activeKey={activeMainCategory || undefined} // 确保 Tabs 激活
onChange={(key) => setActiveMainCategory(key as MainCategoryKey)}
>
{mainCategories.map((mainCategory) => (
<TabPane tab={<span>{mainCategory}</span>} key={mainCategory}>
{/* 二级分类 */}
{subCategories.length > 0 && (
<Tabs
activeKey={activeSubCategory || undefined} // 确保 Tabs 激活
onChange={(key) => setActiveSubCategory(key as SubCategoryKey)}
>
{subCategories.map((subCategory) => (
<TabPane tab={<span>{subCategory}</span>} key={subCategory}>
{/* 搜索输入框 */}
<Search
placeholder="搜索图标"
onChange={(e) => setSearchTerm(e.target.value)} // 更新搜索关键词
style={{ marginBottom: '16px' }}
allowClear
/>
{/* 图标列表 */}
<StyledIconList>
<Row gutter={[8, 8]}>
{filteredIcons.length > 0 ? (
filteredIcons.map((iconName) => (
<Col span={4} key={iconName}>
<StyledIconWrapper
onClick={() => handleIconSelect(iconName)}
>
<LocalSvgIcon
mainCategory={activeMainCategory!}
subCategory={activeSubCategory!}
name={iconName}
size="32px"
/>
</StyledIconWrapper>
</Col>
))
) : (
<Text></Text>
)}
</Row>
</StyledIconList>
</TabPane>
))}
</Tabs>
)}
</TabPane>
))}
</Tabs>
<Divider />
</Card>
);
};
// 样式组件
const StyledIconList = styled.div`
padding: 16px;
max-height: calc(32px * 7 + 16px * 7); /* 限制7行的高度 */
overflow-y: auto; /* 超出的部分滚动显示 */
`;
const StyledIconWrapper = styled.div`
padding: 8px;
text-align: center;
cursor: pointer;
border: 2px solid transparent;
border-radius: 4px;
transition: border-color 0.3s;
&:hover {
border-color: #1890ff;
}
`;

View File

@@ -0,0 +1,35 @@
import { Button, ButtonProps } from 'antd';
import { CSSProperties, ReactNode } from 'react';
type Props = {
children: ReactNode;
className?: string;
style?: CSSProperties;
title?: string; // 新增 title 属性,用于鼠标悬停显示提示
} & ButtonProps;
export default function IconButton({ children, className, style, onClick, title, ...other }: Props) {
return (
<Button
title={title}
type="text"
shape="circle"
style={{
...style,
border: 'none',
backgroundColor: 'transparent',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '8px',
width: '32px',
height: '32px'
}}
className={`flex items-center justify-center transition-colors ${className}`}
onClick={onClick}
{...other}
>
{children}
</Button>
);
}

View File

@@ -0,0 +1,24 @@
//src\components\icon\iconify-icon.tsx
import { Icon } from '@iconify/react';
import styled from 'styled-components';
import type { IconProps } from '@iconify/react';
interface Props extends IconProps {
size?: IconProps['width'];
}
export default function Iconify({ icon, size = '1em', className = '', ...other }: Props) {
return (
<StyledIconify className="anticon">
<Icon icon={icon} width={size} height={size} className={`m-auto ${className}`} {...other} />
</StyledIconify>
);
}
const StyledIconify = styled.div`
display: inline-flex;
vertical-align: middle;
svg {
display: inline-block;
}
`;

View File

@@ -0,0 +1,5 @@
import IconButton from './icon-button';
import Iconify from './iconify-icon';
import SvgIcon from './svg-icon';
export { Iconify, SvgIcon, IconButton };

View File

@@ -0,0 +1,39 @@
// src\components\icon\svg-icon.tsx
import { CSSProperties } from 'react';
interface SvgIconProps {
prefix?: string;
icon: string;
color?: string;
size?: string | number;
className?: string;
style?: CSSProperties;
}
export default function SvgIcon({
icon,
prefix = 'icon',
color = 'currentColor',
size = '1em',
className = '',
style = {},
}: SvgIconProps) {
const symbolId = `#${prefix}-${icon}`;
const svgStyle: CSSProperties = {
verticalAlign: 'middle',
width: size,
height: size,
color,
...style,
};
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
className={`anticon fill-current inline-block h-[1em] w-[1em] overflow-hidden outline-none ${className}`}
style={svgStyle}
>
<use xlinkHref={symbolId} fill="currentColor" />
</svg>
);
}

View File

@@ -0,0 +1,398 @@
/**
* Layout主布局组件
* 作者: 阿瑞
* 功能: 应用主布局,包含导航菜单、用户信息、主题切换等功能
* 版本: v2.0 - 性能优化版本
*/
import React, { useEffect, useMemo, useState, useCallback } from 'react';
import { useRouter } from 'next/router';
import { ConfigProvider, Divider, Dropdown, message, Modal, Button } from 'antd';
import { LogoutOutlined, TeamOutlined, UserOutlined } from '@ant-design/icons';
import { PageContainer, ProConfigProvider, ProLayout, type MenuDataItem, type ProSettings } from '@ant-design/pro-components';
import { Icon } from '@iconify/react';
import { MdDarkMode, MdLightMode } from "react-icons/md";
import { useUserActions, useUserInfo } from '@/store/userStore';
import PersonalInfo from './PersonalInfo';
import { IPermission } from '@/models/types';
import { useTheme } from '@/utils/theme';
import ThemeSwitcher from './ThemeSwitcher';
// Layout组件的Props类型定义
interface LayoutProps {
children: React.ReactNode;
}
// 生成动态路由的函数 - 优化版本
const generateDynamicRoutes = (permissions: IPermission[]): MenuDataItem[] => {
if (!permissions?.length) return [];
// 对权限进行排序
const sortedPermissions = permissions.sort((a, b) => (a. ?? 0) - (b. ?? 0));
// 映射权限数据到菜单项
return sortedPermissions.map(permission => ({
path: permission.路径,
name: permission.名称,
icon: <Icon icon={permission.Icon} width="20" height="20" />,
component: './DynamicComponent',
routes: permission.子级 ? generateDynamicRoutes(permission.) : undefined
}) as Partial<MenuDataItem>);
};
// 默认props配置
const defaultProps = {
route: {
path: '/',
routes: [],
},
location: {
pathname: '/',
},
};
// 头部标题渲染组件 - 提取为独立组件避免重复渲染
const HeaderTitle: React.FC = React.memo(() => (
<a>
<img
src="/aoun.png"
alt="logo"
style={{
height: '32px',
width: '32px',
marginRight: '8px',
borderRadius: '50%'
}}
/>
</a>
));
// 菜单底部渲染组件
const MenuFooter: React.FC<{ collapsed?: boolean; userInfo: any; }> = React.memo(({
collapsed,
userInfo,
//onShowScriptLibrary
}) => {
if (collapsed) return undefined;
return (
<div style={{ textAlign: 'center', padding: '0px', borderRadius: '8px' }}>
{/* 团队信息部分 */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 12
}}>
<TeamOutlined style={{ fontSize: 18, color: '#1890ff', marginRight: 8 }} />
<span style={{ fontSize: '14px', fontWeight: 500 }}>
{userInfo?.?. || 'N/A'}
</span>
</div>
{/* 角色信息部分 */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 12
}}>
<UserOutlined style={{ fontSize: 18, color: '#52c41a', marginRight: 8 }} />
<span style={{ fontSize: '14px', fontWeight: 500 }}>
{userInfo?.?. || 'N/A'}
</span>
</div>
{/* 任务控制器 */}
{/* 话术库按钮 */}
<div style={{ marginTop: '12px', marginBottom: '12px' }}>
<Button
type="primary"
//onClick={onShowScriptLibrary}
style={{
width: '100%',
height: '36px',
fontSize: '14px',
fontWeight: 500,
borderRadius: '6px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}
>
📚
</Button>
</div>
{/* 分割线 */}
<Divider style={{ margin: '20px 0', borderColor: '#e8e8e8' }} />
{/* 底部版权信息 */}
<div style={{ fontSize: '12px', color: '#999' }}>
© 2024 AOUN
</div>
</div>
);
});
// Layout组件主体
const Layout: React.FC<LayoutProps> = ({ children }) => {
// 状态管理
const router = useRouter();
const userInfo = useUserInfo();
const userActions = useUserActions();
const [isClient, setIsClient] = useState(false);
const [dynamicRoutes, setDynamicRoutes] = useState<MenuDataItem[]>([]);
const [showPersonalInfo, setShowPersonalInfo] = useState<boolean>(false);
//const [showScriptLibrary, setShowScriptLibrary] = useState<boolean>(false);
const { navTheme, toggleTheme, changePrimaryColor, themeToken } = useTheme(() => { });
// 使用 useMemo 优化 settings 计算
const settings = useMemo<Partial<ProSettings>>(() => ({
fixSiderbar: true,
layout: "mix",
splitMenus: false,
navTheme: navTheme,
contentWidth: "Fluid",
colorPrimary: themeToken.colorPrimary,
title: "私域管理系统V3",
siderMenuType: "sub",
fixedHeader: true,
menuHeaderRender: false,
}), [navTheme, themeToken.colorPrimary]);
// 使用 useCallback 优化事件处理函数
const handleLogout = useCallback(() => {
router.push('/');
userActions.clearUserInfoAndToken();
}, [router, userActions]);
const handleShowPersonalInfo = useCallback(() => {
setShowPersonalInfo(true);
}, []);
const handleClosePersonalInfo = useCallback(() => {
setShowPersonalInfo(false);
}, []);
const handleMenuItemClick = useCallback((path: string) => {
router.push(path || '/');
}, [router]);
// 用户信息更新逻辑
const { fetchAndSetUserInfo } = useUserActions();
useEffect(() => {
const updateUserInfo = async () => {
try {
await fetchAndSetUserInfo();
} catch (error) {
message.error('无法更新用户信息');
console.error('用户信息更新失败:', error);
}
};
updateUserInfo();
}, [fetchAndSetUserInfo]);
// 客户端渲染检测
useEffect(() => {
setIsClient(true);
}, []);
// 动态路由生成 - 使用 useMemo 优化
const memoizedRoutes = useMemo(() => {
if (userInfo?.?.) {
return generateDynamicRoutes(userInfo..);
}
return [];
}, [userInfo?.?.]);
// 更新动态路由
useEffect(() => {
setDynamicRoutes(memoizedRoutes);
}, [memoizedRoutes]);
// 下拉菜单配置 - 使用 useMemo 优化
const dropdownMenuItems = useMemo(() => [
{
key: 'profile',
icon: <UserOutlined />,
label: '个人资料',
onClick: handleShowPersonalInfo,
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
onClick: handleLogout
},
], [handleShowPersonalInfo, handleLogout]);
// 菜单项渲染函数 - 使用 useCallback 优化
const renderMenuItem = useCallback((item: any, dom: React.ReactNode) => (
<div
onClick={() => handleMenuItemClick(item.path)}
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
>
{item.icon && (
<span
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: 4,
lineHeight: 1,
}}
>
{item.icon}
</span>
)}
<span>{dom}</span>
</div>
), [handleMenuItemClick]);
// 头像渲染函数 - 使用 useCallback 优化
const renderAvatar = useCallback((_props: any, dom: React.ReactNode) => (
<>
{/* 主题切换按钮 */}
<Button
type="text"
shape="circle"
onClick={toggleTheme}
className="theme-switch-ripple"
title={navTheme === 'realDark' ? '切换到明亮模式' : '切换到暗黑模式'}
aria-label={navTheme === 'realDark' ? '切换到明亮模式' : '切换到暗黑模式'}
style={{
marginRight: '8px',
border: 'none',
background: 'var(--glass-bg)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
transition: 'all 0.3s ease',
fontSize: '16px',
color: navTheme === 'realDark' ? 'rgba(255, 255, 255, 0.8)' : 'rgba(10, 17, 40, 0.8)',
}}
>
{navTheme === 'realDark' ? <MdLightMode /> : <MdDarkMode />}
</Button>
{/* 主题色选择器 */}
<ThemeSwitcher
value={themeToken.colorPrimary}
onChange={changePrimaryColor}
toggleTheme={toggleTheme}
navTheme={navTheme}
/>
{/* 下拉菜单按钮 */}
<Dropdown menu={{ items: dropdownMenuItems }}>
{dom}
</Dropdown>
</>
), [themeToken.colorPrimary, changePrimaryColor, toggleTheme, navTheme, dropdownMenuItems]);
// 如果不在客户端,不渲染任何内容
if (!isClient) {
return null;
}
return (
<div id="test-pro-layout" style={{
height: '100vh',
display: 'flex',
flexDirection: 'column',
//overflow: 'hidden'
}}>
<ProConfigProvider hashed={false}>
<ConfigProvider
getTargetContainer={() => document.getElementById('test-pro-layout') || document.body}
>
<ProLayout
prefixCls="my-prefix"
{...defaultProps}
location={{ pathname: router.pathname }}
token={{ header: { colorBgMenuItemSelected: 'rgba(0,0,0,0.04)' } }}
siderMenuType="group"
menu={{
request: async () => dynamicRoutes,
collapsedShowGroupTitle: true
}}
avatarProps={{
src: 'https://gw.alipayobjects.com/zos/antfincdn/efFD$IOql2/weixintupian_20170331104822.jpg',
size: 'small',
title: userInfo?.姓名 || 'N/A',
render: renderAvatar,
}}
headerTitleRender={() => <HeaderTitle />}
menuFooterRender={(props) => (
<MenuFooter
collapsed={props?.collapsed}
userInfo={userInfo}
//onShowScriptLibrary={handleShowScriptLibrary}
/>
)}
menuItemRender={renderMenuItem}
{...settings}
style={{
height: '100vh',
background: 'transparent',
}}
contentStyle={{
background: 'var(--glass-bg)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
display: 'flex',
flexDirection: 'column',
padding: 0,
margin: 0,
}}
contentWidth="Fluid"
locale="zh-CN"
>
<PageContainer
header={{ title: null }}
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
padding: 0,
margin: 0,
}}
>
<div style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
padding: '16px',
margin: 0,
overflow: 'auto',
boxSizing: 'border-box',
}}>
{children}
</div>
</PageContainer>
</ProLayout>
</ConfigProvider>
</ProConfigProvider>
{/* 个人资料模态框 */}
<Modal
title="个人资料"
open={showPersonalInfo}
onCancel={handleClosePersonalInfo}
footer={null}
width={800}
destroyOnHidden
>
<PersonalInfo />
</Modal>
{/* 话术库模态框 */}
</div>
);
};
// 移除React.memo包装组件避免不必要的重新渲染阻止
export default Layout;

View File

@@ -0,0 +1,41 @@
// src/components/layout/Navbar.tsx
import React from 'react';
import Link from 'next/link';
import { HomeOutlined, LockOutlined, MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined } from '@ant-design/icons';
interface NavbarProps {
isCollapsed: boolean;
setIsCollapsed: (collapsed: boolean) => void;
}
const Navbar: React.FC<NavbarProps> = ({ isCollapsed, setIsCollapsed }) => {
return (
<nav className={`bg-gray-800 text-white fixed inset-y-0 left-0 z-10 ${isCollapsed ? 'w-20' : 'w-64'} transition-all duration-300 flex flex-col items-center`}>
<div className="text-xl font-bold mt-5 mb-4">
AOUN
</div>
<button onClick={() => setIsCollapsed(!isCollapsed)} className="text-lg p-2">
{isCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</button>
<ul className={`flex flex-col space-y-2 w-full p-4 ${isCollapsed ? 'items-center' : 'items-start'}`}>
<li className="w-full">
<Link href="/home" className={`hover:text-gray-300 flex ${isCollapsed ? 'justify-center' : 'justify-start'}`}>
{isCollapsed ? <HomeOutlined /> : '首页'}
</Link>
</li>
<li className="w-full">
<Link href="/management/permission" className={`hover:text-gray-300 flex ${isCollapsed ? 'justify-center' : 'justify-start'}`}>
{isCollapsed ? <LockOutlined /> : '权限管理'}
</Link>
</li>
<li className="w-full">
<Link href="/management/role" className={`hover:text-gray-300 flex ${isCollapsed ? 'justify-center' : 'justify-start'}`}>
{isCollapsed ? <UserOutlined /> : '角色管理'}
</Link>
</li>
</ul>
</nav>
);
};
export default Navbar;

View File

@@ -0,0 +1,51 @@
/**
* 性能监控组件
* 作者: 阿瑞
* 功能: 监控组件渲染性能,开发环境下显示性能指标
* 版本: v1.0
*/
import React, { useEffect, useRef } from 'react';
interface PerformanceMonitorProps {
componentName: string;
children: React.ReactNode;
enableLogging?: boolean;
}
const PerformanceMonitor: React.FC<PerformanceMonitorProps> = ({
componentName,
children,
enableLogging = process.env.NODE_ENV === 'development'
}) => {
const renderStartTime = useRef<number>(0);
const renderCount = useRef<number>(0);
useEffect(() => {
if (!enableLogging) return;
renderCount.current += 1;
const renderEndTime = performance.now();
const renderDuration = renderEndTime - renderStartTime.current;
// 记录渲染性能
if (renderDuration > 100) {
console.warn(`⚠️ ${componentName} 渲染耗时过长: ${renderDuration.toFixed(2)}ms`);
} else if (renderDuration > 50) {
console.log(`${componentName} 渲染耗时: ${renderDuration.toFixed(2)}ms`);
}
// 记录渲染次数
if (renderCount.current > 10) {
console.log(`🔄 ${componentName} 已渲染 ${renderCount.current}`);
}
});
// 记录渲染开始时间
if (enableLogging) {
renderStartTime.current = performance.now();
}
return <>{children}</>;
};
export default React.memo(PerformanceMonitor);

View File

@@ -0,0 +1,231 @@
/**
* 文件: src/components/layout/PersonalInfo.tsx
* 作者: 阿瑞
* 功能: 个人信息组件 - 毛玻璃风格
* 版本: v2.0.0
* @updated 使用原生fetch替代axios符合项目技术规范
*/
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
Form,
Input,
Button,
message,
Card,
Descriptions,
Tree,
Avatar,
Row,
Col,
Modal,
Tabs,
Skeleton,
} from 'antd';
import { useUserInfo } from '@/store/userStore';
import {
EditOutlined,
MailOutlined,
PhoneOutlined,
UserOutlined,
TeamOutlined,
IdcardOutlined,
} from '@ant-design/icons';
import { PageHeader } from '@ant-design/pro-components';
const { TabPane } = Tabs;
interface IUser {
_id: string;
姓名: string;
电话: string;
邮箱: string;
微信昵称?: string;
头像?: string;
unionid?: string;
openid?: string;
?: {
名称: string;
描述: string;
权限: Array<{
_id: string;
名称: string;
子级: Array<{ _id: string; 名称: string }>;
}>;
};
?: {
名称: string;
: { 姓名: string };
};
}
const PersonalInfo: React.FC = () => {
const userInfo = useUserInfo(); // 从store获取用户信息
const [form] = Form.useForm<IUser>();
const [loading, setLoading] = useState<boolean>(false);
const [userData, setUserData] = useState<IUser | null>(null); // 保存用户数据
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
const userId = userInfo._id;
// 获取用户信息的函数,使用 useCallback 优化性能
const fetchUserInfo = useCallback(async () => {
if (!userId) return;
try {
const response = await fetch(`/api/backstage/mine/info/${userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('获取个人信息失败');
}
const data = await response.json();
setUserData(data);
form.setFieldsValue(data);
} catch (error: any) {
message.error(error.message || '获取个人信息失败');
}
}, [userId, form]);
// 模块级注释:组件初始化时获取用户信息
useEffect(() => {
fetchUserInfo();
}, [fetchUserInfo]);
// 更新个人信息的处理函数
const onFinish = async (values: IUser) => {
if (!userId) return;
setLoading(true);
try {
const response = await fetch(`/api/backstage/mine/info/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '更新失败');
}
message.success('更新成功');
setEditModalVisible(false);
await fetchUserInfo(); // 更新信息后重新获取
} catch (error: any) {
message.error(error.message || '更新失败');
} finally {
setLoading(false);
}
};
// 生成权限树数据
const permissionsTreeData = useMemo(() => {
if (!userData?.?.) return [];
return userData...map((item) => ({
title: item.名称,
key: item._id,
children: item.子级.map((child) => ({
title: child.名称,
key: child._id,
})),
}));
}, [userData]);
return (
<div>
<PageHeader
title="个人信息"
subTitle="查看和更新您的个人信息"
extra={[
<Button key="edit" type="primary" icon={<EditOutlined />} onClick={() => setEditModalVisible(true)}>
</Button>,
]}
/>
<Card style={{ marginBottom: 24 }}>
{userData ? (
<Row gutter={24}>
<Col xs={24} sm={24} md={8} lg={6} style={{ textAlign: 'center' }}>
<Avatar size={120} src={userData?.} icon={<UserOutlined />} />
<h2 style={{ marginTop: 16 }}>{userData?.}</h2>
<p>{userData?.}</p>
</Col>
<Col xs={24} sm={24} md={16} lg={18}>
<Descriptions title="基本信息" column={1} bordered>
<Descriptions.Item label={<MailOutlined />}> {userData?.}</Descriptions.Item>
<Descriptions.Item label={<PhoneOutlined />}> {userData?.}</Descriptions.Item>
<Descriptions.Item label={<TeamOutlined />}> {userData?.?.}</Descriptions.Item>
<Descriptions.Item label={<IdcardOutlined />}> {userData?.?.}</Descriptions.Item>
</Descriptions>
</Col>
</Row>
) : (
<Skeleton active />
)}
</Card>
<Card>
<Tabs defaultActiveKey="1">
<TabPane tab="团队与角色信息" key="1">
<Descriptions column={1} bordered>
<Descriptions.Item label="团队名称">{userData?.?.}</Descriptions.Item>
<Descriptions.Item label="团队拥有者">{userData?.?.?.}</Descriptions.Item>
<Descriptions.Item label="角色名称">{userData?.?.}</Descriptions.Item>
<Descriptions.Item label="角色描述">{userData?.?.}</Descriptions.Item>
</Descriptions>
</TabPane>
<TabPane tab="权限列表" key="2">
<Tree
treeData={permissionsTreeData}
//defaultExpandAll//默认展开所有节点
//默认收起所有节点
defaultExpandParent={false}
/>
</TabPane>
</Tabs>
</Card>
{/* 编辑信息的 Modal */}
<Modal
title="编辑个人信息"
open={editModalVisible}
onCancel={() => setEditModalVisible(false)}
footer={null}
destroyOnClose
>
<Form form={form} onFinish={onFinish} layout="vertical">
<Form.Item name="微信昵称" label="微信昵称">
<Input />
</Form.Item>
<Form.Item name="姓名" label="姓名" rules={[{ required: true, message: '请输入姓名' }]}>
<Input />
</Form.Item>
<Form.Item name="电话" label="电话" rules={[{ required: true, message: '请输入电话' }]}>
<Input />
</Form.Item>
<Form.Item name="邮箱" label="邮箱" rules={[{ required: true, message: '请输入邮箱' }]}>
<Input />
</Form.Item>
<Form.Item style={{ textAlign: 'right' }}>
<Button onClick={() => setEditModalVisible(false)} style={{ marginRight: 8 }}>
</Button>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default PersonalInfo;

View File

@@ -0,0 +1,123 @@
/**
* 主题切换器组件
* 作者: 阿瑞
* 功能: 主题模式切换和主题色选择
* 版本: v2.0 - 性能优化版本
*/
import React, { useCallback, useMemo } from 'react';
import { Button, Popover, Tag, Tooltip, Space, Typography } from 'antd';
import { BgColorsOutlined } from '@ant-design/icons';
const { Text } = Typography;
interface ThemeSwitcherProps {
value: string;
onChange: (color: string) => void;
toggleTheme: () => void;
navTheme: 'light' | 'realDark';
}
// 预设主题色配置 - 使用更丰富的色彩
const presetColors = [
{ color: '#078DEE', name: '天空蓝' },
{ color: '#7635DC', name: '紫罗兰' },
{ color: '#2065D1', name: '深海蓝' },
{ color: '#FDA92D', name: '橙黄色' },
{ color: '#FF4842', name: '珊瑚红' },
{ color: '#FFC107', name: '金黄色' },
{ color: '#00AB55', name: '翡翠绿' },
{ color: '#1890FF', name: 'Ant蓝' },
{ color: '#722ED1', name: 'Ant紫' },
{ color: '#EB2F96', name: 'Ant粉' },
];
const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({
value,
onChange }) => {
// 使用 useCallback 优化颜色选择处理函数
const handleColorChange = useCallback((color: string) => {
onChange(color);
}, [onChange]);
// 使用 useMemo 优化颜色选择器内容
const colorPickerContent = useMemo(() => (
<div style={{ width: 280 }}>
<div style={{ marginBottom: 12 }}>
<Space>
<BgColorsOutlined style={{ color: '#1890ff' }} />
<Text strong></Text>
</Space>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 8 }}>
{presetColors.map(({ color, name }) => (
<Tooltip key={color} title={name} placement="top">
<Tag
color={color}
onClick={() => handleColorChange(color)}
style={{
cursor: 'pointer',
borderRadius: '8px',
width: 48,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: 0,
border: value === color ? '2px solid #1890ff' : '1px solid #d9d9d9',
boxShadow: value === color ? '0 2px 8px rgba(24, 144, 255, 0.3)' : 'none',
transition: 'all 0.2s ease',
}}
>
{value === color && (
<span style={{
color: '#fff',
fontSize: '12px',
fontWeight: 'bold',
textShadow: '0 1px 2px rgba(0,0,0,0.5)'
}}>
</span>
)}
</Tag>
</Tooltip>
))}
</div>
<div style={{ marginTop: 12, textAlign: 'center' }}>
<Text type="secondary" style={{ fontSize: '12px' }}>
: {presetColors.find(c => c.color === value)?.name || '自定义'}
</Text>
</div>
</div>
), [value, handleColorChange]);
return (
<Space size="middle">
{/* 主题色选择器 */}
<Popover
trigger="click"
placement="bottomRight"
content={colorPickerContent}
title={null}
overlayStyle={{ padding: 0 }}
>
<Tooltip title="选择主题色" placement="bottom">
<Button
shape="circle"
size="small"
icon={<BgColorsOutlined />}
style={{
backgroundColor: value,
borderColor: value,
color: '#fff',
boxShadow: `0 2px 4px ${value}40`,
transition: 'all 0.2s ease',
}}
/>
</Tooltip>
</Popover>
</Space>
);
};
// 使用 React.memo 优化组件性能
export default React.memo(ThemeSwitcher);

View File

@@ -0,0 +1,51 @@
// src/components/layout/TopBar.tsx
import { Button, Popover } from 'antd';
import React from 'react';
import styles from '@/pages/index.module.css';
import { useTheme } from '@/hooks/useTheme'; // 关键代码行注释使用独立的useTheme Hook
import { MdDarkMode, MdLightMode } from "react-icons/md";
const TopBar = () => {
const handleLogout = () => {
//clearUserInfoAndToken();
window.location.href = '/start/login';
};
const content = (
<div>
<Button type="link" onClick={handleLogout}>退</Button>
</div>
);
const { isDark, toggleTheme, isTransitioning } = useTheme(); // 关键代码行注释:解构获取所需的状态
return (
<div className={`bg-gray-100 p-4 flex justify-between items-center`}>
<span className="text-gray-600 mr-2"></span>
<div className="flex items-center">
<Popover content={content} trigger="click">
<div className="w-8 h-8 rounded-full bg-gray-300 cursor-pointer"></div>
</Popover>
{/* 右侧操作区域 */}
<div className={styles.headerActions}>
{/* 关键代码行注释主题切换按钮使用React Icons替代Ant Design Switch添加增强的视觉效果 */}
<Button
type="text"
shape="round"
onClick={toggleTheme}
className={`${styles.themeSwitch} theme-switch-ripple`}
disabled={isTransitioning}
title={isDark ? '切换到明亮模式' : '切换到暗黑模式'}
aria-label={isDark ? '切换到明亮模式' : '切换到暗黑模式'}
>
{isDark ? <MdLightMode /> : <MdDarkMode />}
</Button>
</div>
{/* 主题切换按钮 */}
</div>
</div>
);
};
export default TopBar;

View File

@@ -0,0 +1,26 @@
// src/components/layout/_defaultProps.tsx
import { MenuDataItem } from '@ant-design/pro-components';
import { Icon } from '@iconify/react';
// 假设这个函数从外部传入,用于根据用户权限生成菜单
export const generateDynamicRoutes = (permissions: any[]): MenuDataItem[] => {
//打印权限,输出为数组
//console.log("permissions",permissions);
return permissions.map(permission => ({
path: permission.路径,
name: permission.名称,
icon: <Icon icon={permission.Icon} width="24" height="24" />,
component: './DynamicComponent', // 这里应指向实际组件路径
routes: permission.子级 && generateDynamicRoutes(permission.)
}));
};
export default {
route: {
path: '/',
routes: [], // 初始化时不包含任何静态路由
},
location: {
pathname: '/',
},
};

View File

@@ -0,0 +1,79 @@
/**
* Layout布局配置文件
* 作者: 阿瑞
* 功能: 集中管理Layout组件的配置项和常量
* 版本: v1.0
*/
import { type ProSettings } from '@ant-design/pro-components';
// 默认头像配置
export const DEFAULT_AVATAR = {
src: 'https://gw.alipayobjects.com/zos/antfincdn/efFD$IOql2/weixintupian_20170331104822.jpg',
size: 'small' as const,
};
// 默认路由配置
export const DEFAULT_PROPS = {
route: {
path: '/',
routes: [],
},
location: {
pathname: '/',
},
};
// ProLayout基础设置
export const getLayoutSettings = (navTheme: string, colorPrimary: string): Partial<ProSettings> => ({
fixSiderbar: true,
layout: "mix",
splitMenus: false,
navTheme: navTheme as any,
contentWidth: "Fluid",
colorPrimary,
title: "私域管理系统V3",
siderMenuType: "sub",
fixedHeader: true,
menuHeaderRender: false,
});
// 菜单配置
export const MENU_CONFIG = {
collapsedShowGroupTitle: true,
};
// 内容样式配置
export const getContentStyle = (navTheme: string) => ({
backgroundColor: navTheme === 'light' ? '#fff' : '',
height: '100%',
width: '100%',
});
// 模态框配置
export const MODAL_CONFIG = {
personalInfo: {
title: "个人资料",
width: 800,
destroyOnClose: true,
},
mySales: {
title: "我的业绩",
width: 800,
destroyOnClose: true,
},
};
// 性能优化配置
export const PERFORMANCE_CONFIG = {
enableMonitoring: process.env.NODE_ENV === 'development',
renderThreshold: 50, // 渲染时间阈值(ms)
warningThreshold: 100, // 警告阈值(ms)
};
// 主题配置
export const THEME_CONFIG = {
storageKey: 'navTheme',
colorStorageKey: 'colorPrimary',
defaultTheme: 'light' as const,
defaultColor: '#1677FF',
};

52
src/hooks/README.md Normal file
View File

@@ -0,0 +1,52 @@
# 🎨 自定义Hooks
## useTheme Hook
专为SaaS管理平台设计的主题管理Hook支持SSR和防闪烁优化。
### 📦 使用方法
```tsx
import { useTheme } from '@/hooks/useTheme';
// 或从统一入口导入
import { useTheme } from '@/hooks';
function MyComponent() {
const { isDark, toggleTheme, currentTheme, mounted } = useTheme();
// 确保组件已挂载再渲染内容避免SSR不匹配
if (!mounted) {
return <div>Loading...</div>;
}
return (
<div>
<p>: {isDark ? '暗黑模式' : '明亮模式'}</p>
<button onClick={toggleTheme}></button>
</div>
);
}
```
### 🔧 API
| 属性 | 类型 | 描述 |
|------|------|------|
| `isDark` | `boolean` | 是否为暗黑主题 |
| `toggleTheme` | `() => void` | 切换主题函数 |
| `currentTheme` | `ThemeMode` | 当前主题模式枚举值 |
| `mounted` | `boolean` | 组件是否已挂载SSR兼容 |
### 🚀 特性
-**SSR兼容**: 解决Next.js服务端渲染水合不匹配问题
-**防闪烁**: 页面刷新时无白色闪烁
-**localStorage持久化**: 主题设置自动保存
-**TypeScript支持**: 完整的类型定义
-**毛玻璃UI**: 专为毛玻璃风格优化
### 📋 注意事项
1. 必须在`ThemeProvider`内部使用
2. 使用`mounted`状态避免SSR不匹配
3. 主题切换会自动同步到localStorage

11
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,11 @@
/**
* Hooks统一导出文件
* @author 阿瑞
* @description 统一导出所有自定义hooks方便组件导入使用
* @version 1.0.0
* @created 2024-12-19
*/
// 关键代码行注释导出主题管理相关hooks和类型
export { useTheme, ThemeProvider } from './useTheme';
export type { ThemeContextType } from './useTheme';

118
src/hooks/useTheme.tsx Normal file
View File

@@ -0,0 +1,118 @@
/**
* 主题管理Hook
* @author 阿瑞
* @description 主题切换和状态管理的自定义Hook支持SSR和防闪烁优化性能优化
* @version 2.1.0
* @created 2024-12-19
* @updated 优化性能,简化动画逻辑
*/
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
import { useSettingActions, useThemeMode } from '@/store/settingStore';
import { ThemeMode } from '@/types/enum';
// 模块级注释:主题切换上下文类型定义
export interface ThemeContextType {
isDark: boolean;
toggleTheme: () => void;
currentTheme: ThemeMode;
mounted: boolean; // 关键代码行注释添加mounted状态避免SSR不匹配
isTransitioning: boolean; // 关键代码行注释:添加过渡状态追踪
}
// 模块级注释:创建主题上下文
const ThemeContext = createContext<ThemeContextType>({
isDark: false,
toggleTheme: () => {},
currentTheme: ThemeMode.Light,
mounted: false,
isTransitioning: false,
});
// 模块级注释主题切换Hook
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
// 模块级注释:主题提供者组件
export function ThemeProvider({ children }: { children: React.ReactNode }): React.ReactElement {
// 关键代码行注释使用zustand store管理主题状态
const themeMode = useThemeMode();
const { toggleThemeMode } = useSettingActions();
const isDark = themeMode === ThemeMode.Dark;
// 关键代码行注释添加mounted状态确保只在客户端应用主题
const [mounted, setMounted] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(false);
// 关键代码行注释:简化的主题应用函数
const applyTheme = useCallback((darkMode: boolean): void => {
if (typeof document !== 'undefined' && mounted) {
const root = document.documentElement;
const body = document.body;
// 关键代码行注释:清除所有主题相关的类名
body.classList.remove('light-theme', 'dark-theme', 'light', 'dark');
root.classList.remove('light-theme', 'dark-theme', 'light', 'dark');
// 关键代码行注释:应用新的主题类名
const themeClass = darkMode ? 'dark' : 'light';
body.classList.add(themeClass);
root.classList.add(themeClass);
// 关键代码行注释设置data-theme属性
body.setAttribute('data-theme', themeClass);
root.setAttribute('data-theme', themeClass);
}
}, [mounted]);
// 关键代码行注释:优化的主题切换函数
const enhancedToggleTheme = useCallback(() => {
if (!isTransitioning) {
setIsTransitioning(true);
// 关键代码行注释:触发主题切换
toggleThemeMode();
// 关键代码行注释:配合分层过渡策略的完成处理
setTimeout(() => {
setIsTransitioning(false);
}, 400); // 配合CSS主过渡时间
// 关键代码行注释:添加轻量触觉反馈(如果支持)
if ('vibrate' in navigator) {
navigator.vibrate(30);
}
}
}, [toggleThemeMode, isTransitioning]);
// 关键代码行注释组件挂载时设置mounted状态
useEffect(() => {
setMounted(true);
}, []);
// 关键代码行注释:只有在客户端挂载后才应用主题
useEffect(() => {
if (mounted) {
applyTheme(isDark);
}
}, [isDark, mounted, applyTheme]);
return (
<ThemeContext.Provider
value={{
isDark,
toggleTheme: enhancedToggleTheme,
currentTheme: themeMode,
mounted,
isTransitioning
}}
>
{children}
</ThemeContext.Provider>
);
}

492
src/models/index.ts Normal file
View File

@@ -0,0 +1,492 @@
// src/models/index.ts
import mongoose, { Schema } from 'mongoose';
// 用户/导购模型定义
const UserSchema: Schema = new Schema({
: { type: String, required: true }, // 用户姓名字段
: { type: String, required: true, unique: true }, // 用户电话字段
: { type: String, required: true, unique: true }, // 用户邮箱字段
: { type: String, required: true }, // 用户密码字段
: { type: Schema.Types.ObjectId, ref: 'Team' }, // 将团队字段定义为引用 Team 模型的 ObjectId
: { type: Schema.Types.ObjectId, ref: 'Role' }, // 将角色字段定义为引用 Role 模型的 ObjectId
: { type: String }, // 用户微信昵称字段
: { type: String }, // 用户头像字段
unionid: { type: String, unique: true },
openid: { type: String, unique: true }, // 添加openid字段
}, { timestamps: true }); // 自动添加创建时间和更新时间
UserSchema.index({ 团队: 1 }); // 对团队字段建立索引
// 团队模型定义
const TeamSchema: Schema = new Schema({
: { type: String, required: true }, // 团队名称字段
: [{ type: Schema.Types.ObjectId, ref: 'TagInfo' }], // 将标签字段定义为引用 TagInfo 模型的 ObjectId 数组
: { type: Schema.Types.ObjectId, ref: 'User', required: true }, // 将拥有者字段定义为引用 User 模型的 ObjectId
: { type: Date },
: { type: Date },
: { type: Boolean, default: false },
}, { timestamps: true }); // 自动添加创建时间和更新时间
TeamSchema.index({ 拥有者: 1 }); // 对拥有者字段建立索引
// 套餐模型定义
const PlaneSchema: Schema = new Schema(
{
: { type: String, required: true },
: { type: Number, required: true },
: { type: String }, //表示每个 月/年/季/周/天
: { type: Number, required: true }, //会员时长
//时长可以显示为 100/天100/月100/年。也可以显示为499/3个月499/半年499/年
/*时长:{
数量: { type: Number, required: true },
单位: { type: String, required: true },
}*/
: { type: String }, //简短描述
: { type: String }, //详细描述
: { type: String }, //套餐的功能列表,;例如:['✓ 无限的浏览者', '✓ 最多2名编辑', '✓ 最多3个项目', '✓ 商业许可']
//按钮1、显示按钮文字 2、按钮链接用于跳转到页面
: {
: { type: String },
: { type: String },
},
//推荐默认为false
: { type: Boolean, default: false },
},
{ timestamps: true },
);
export const Plane = mongoose.models.Plane || mongoose.model('Plane', PlaneSchema);
// 套餐订单模型定义
const OrderSchema: Schema = new Schema(
{
: { type: Schema.Types.ObjectId, ref: 'User', required: true },
: { type: Schema.Types.ObjectId, ref: 'Team', required: true },
: { type: Schema.Types.ObjectId, ref: 'Plane', required: true },
: { type: Number, required: true },
: { type: String, enum: ['待支付', '已完成', '已失败'], default: '待支付' },
},
{ timestamps: true }
);
export const Order = mongoose.models.Order || mongoose.model('Order', OrderSchema);
//角色模型定义
const RoleSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team' }, // 将团队字段定义为引用 Team 模型的 ObjectId
: { type: Number }, // 角色排序字段
: { type: String, required: true }, // 角色名称字段
: { type: String, required: true }, // 角色描述字段
: { type: Number, enum: [0, 1, 2, 3, 4], required: true }, // 角色状态字段
: { type: Boolean, default: false }, // 隐藏字段,默认为 false
: { type: String, required: true }, // 角色主页字段,用户登录后跳转的页面
: [{ type: Schema.Types.ObjectId, ref: 'Permission' }], // 将权限字段定义为引用 Permission 模型的 ObjectId 数组
//级别: { type: Number }, // 角色级别字段 增删改查
: { type: Number, enum: [0, 1, 2, 3, 4] }, // 角色级别字段
//关联TagInfo
: [{ type: Schema.Types.ObjectId, ref: 'TagInfo' }], // 将标签字段定义为引用 TagInfo 模型的 ObjectId 数组
}, { timestamps: true }); // 自动添加创建时间和更新时间
RoleSchema.index({ 权限: 1 }); // 对权限字段建立索引
// 权限模型定义
const PermissionSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team' }, // 将团队字段定义为引用 Team 模型的 ObjectId
: { type: Number }, // 权限排序字段
: { type: String, required: true }, // 权限名称字段
: { type: Number, enum: [0, 1, 2] }, //类型:0为目录1为菜单2为按钮
: { type: String, required: true }, // 权限描述字段
: { type: Number, enum: [0, 1, 2, 3, 4], required: true }, // 权限状态字段
Icon: { type: String }, // 权限图标字段
: { type: String, required: true }, // 权限路径字段
: { type: Schema.Types.ObjectId, ref: 'Permission' }, // 将父级字段定义为引用 Permission 模型的 ObjectId
: [{ type: Schema.Types.ObjectId, ref: 'Permission' }], // 将子级字段定义为引用 Permission 模型的 ObjectId 数组
: { type: Schema.Types.ObjectId, ref: 'User' }, // 将创建人字段定义为引用 User 模型的 ObjectId
: { type: Number, enum: [0, 1] }, // 权限 0为系统1为团队
}, { timestamps: true }); // 自动添加创建时间和更新时间
PermissionSchema.index({ 父级: 1 });
// Supplier 供应商模型
const SupplierSchema = new Schema(
{
: { type: Schema.Types.ObjectId, ref: 'Team' }, //将团队字段定义为引用 Team 模型的 ObjectId
order: Number, //排序
供应商名称: String,
: {
电话: String,
联系人: String,
地址: String,
},
: [
{
type: Schema.Types.ObjectId,
ref: 'Category',
},
],
// 供应商状态: 0 停用 1 正常 2 异常 3 备用,默认为 1
status: { type: Number, default: 1 },
供应商等级: String,
供应商类型: String,
供应商备注: String,
},
{ timestamps: true },
);
//品牌模型
const BrandSchema = new Schema(
{
: { type: Schema.Types.ObjectId, ref: 'Team' }, //将团队字段定义为引用 Team 模型的 ObjectId
order: Number, //排序
name: String, //品牌名称
description: String, //品牌描述
},
{ timestamps: true },
);
//tag-color标签和颜色模型
const TagInfoSchema = new Schema(
{
: { type: Schema.Types.ObjectId, ref: 'Team' }, //将团队字段定义为引用 Team 模型的 ObjectId
: { type: String, required: true },
: { type: String, required: true },
icon: { type: String, required: true },
},
{ timestamps: true },
);
export const TagInfo = mongoose.models.TagInfo || mongoose.model('TagInfo', TagInfoSchema);
// 目标模型
const TargetSchema = new Schema(
{
: { type: Schema.Types.ObjectId, ref: 'Team' }, //将团队字段定义为引用 Team 模型的 ObjectId
: { type: String, required: true },// 目标名称
: { type: String, required: true }, // 日目标、周目标、月目标、季目标、年目标
: { type: String, required: true, enum: ['日', '周', '月', '季', '年'] },
: { type: Number, required: true },
: { type: Schema.Types.ObjectId, ref: 'UserInfo', required: true },
: { type: String, required: true, enum: ['SalesRecord', 'DailyAccountGrowth'] }, // 保留这个字段来存储模型的类型
: { type: Schema.Types.ObjectId, refPath: '相关类型' }, // 使用 refPath 来动态决定引用哪个模型
: { type: Date, required: true },
: { type: Date, required: true }
},
{ timestamps: true }
);
export const Target = mongoose.models.Target || mongoose.model('Target', TargetSchema);
//品类模型
const CategorySchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team' }, //将团队字段定义为引用 Team 模型的 ObjectId
name: String, //品类名称
description: String, //品类描述
icon: String, //品类图标
}, { timestamps: true });//自动添加创建时间和更新时间
CategorySchema.index({ team: 1 }); //对团队字段建立索引
// 交易记录模型定义
const TransactionSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team' }, // 将团队字段定义为引用 Team 模型的 ObjectId
: { type: Schema.Types.ObjectId, ref: 'Customer', required: true }, // 引用客户模型
: { type: String, enum: ['充值', '消费', '退现'], required: true }, // 交易类型,限定为充值、消费 或 退现
: { type: Schema.Types.ObjectId, ref: 'SalesRecord' },// 引用销售记录模型
: { type: Number, required: true }, // 交易金额
: { type: Number }, // 交易后余额
: { type: Schema.Types.ObjectId, ref: 'PaymentPlatform' }, // 收款平台
: { type: Date, default: Date.now }, // 交易日期,默认为当前日期
: { type: String }, // 交易描述,可选
}, { timestamps: true });
TransactionSchema.index({ 团队: 1 }); // 对团队字段建立索引
TransactionSchema.index({ 客户: 1 }); // 对客户字段建立索引
// 优惠券模型定义
const CouponSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team' }, // 优惠券所属的团队
: { type: String, required: true }, // 优惠券名称
: { type: String, enum: ['折扣券', '代金券', '满减券'], required: true }, // 优惠券类型,限定为折扣券或代金券
: { type: String }, // 优惠券描述
: { type: Number }, // 优惠金额
: { type: Number }, // 折扣,范围 0-1
: { type: Number }, // 使用优惠券的最低消费金额
: { type: Date }, // 优惠券生效的开始日期
: { type: Date }, // 优惠券的失效日期
}, { timestamps: true });
CouponSchema.index({ 团队: 1 }); // 对团队字段建立索引
//优惠券使用记录模型
const CouponUsageSchema = new Schema({
_id: { type: Schema.Types.ObjectId, ref: 'Coupon' }, // 引用优惠券的 ID
: { type: String, required: true }, // 优惠券券码,唯一
使: { type: Boolean, default: false }, // 记录此优惠券是否已被使用
使: { type: Date }, // 优惠券使用日期
});
CouponUsageSchema.index({ 券码: 1 }, { unique: true, partialFilterExpression: { : { $exists: true, $ne: null } } });//对券码字段建立唯一索引
//客户模型定义
const CustomerSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team' }, // 将团队字段定义为引用 Team 模型的 ObjectId
姓名: String,
: { type: String, unique: true },//客户电话字段 唯一
: {
省份: String,
城市: String,
区县: String,
详细地址: String,
},
: { type: String, enum: ['男', '女'] },
微信: String,
生日: Date,
加粉日期: Date,
: { type: Number, default: 0 }, // 冗余字段,用于快速获取客户当前余额
: { type: Date }, // 软删除功能,记录删除时间
: [CouponUsageSchema], // 使用子文档结构来存储优惠券信息
}, { timestamps: true }); // 自动添加创建时间和更新时间
CustomerSchema.index({ 团队: 1 }); // 对团队字段建立索引
CustomerSchema.index({ 电话: 1 }); // 电话字段索引
//销售记录模型定义
const SalesRecordSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team' }, // 将团队字段定义为引用 Team 模型的 ObjectId
: { type: Schema.Types.ObjectId, ref: 'Customer' }, // 关联客户模型
: [{ type: Schema.Types.ObjectId, ref: 'Product' }],
: { type: Schema.Types.ObjectId, ref: 'Account' }, // 关联客户来源模型
: { type: Schema.Types.ObjectId, ref: 'User' }, // 关联导购数据模型
收款类型: String,
成交日期: Date,
应收金额: Number,
收款金额: Number,
待收款: Number,
: { type: Schema.Types.ObjectId, ref: 'PaymentPlatform' }, // 关联付款平台模型
待收已收: Number,
成交微信: String,
: {
type: [String],
default: ['正常'], // 默认状态为数组,包含单一元素 "正常"
},
: {
type: String,
default: '未付款', // 默认状态
},
: {
type: String, // 修改为单一字符串
default: '待结算', // 默认值
enum: ['可结算', '不可结算', '待结算', '已结算'], // 枚举限制
},
//余额抵用,关联交易记录
: { type: Schema.Types.ObjectId, ref: 'Transaction' },
回访日期: Date,
备注: String,
最新物流状态: String,
物流详情: String,
物流单号: String,
// 优惠券的使用记录
: [CouponUsageSchema],
: [{ type: Schema.Types.ObjectId, ref: 'AfterSalesRecord' }], // 关联售后记录
}, { timestamps: true }); // 自动添加创建时间和更新时间
SalesRecordSchema.index({ 团队: 1 }); // 对团队字段建立索引
SalesRecordSchema.index({ 客户: 1 }); // 对客户字段建立索引
SalesRecordSchema.index({ 产品: 1 }); // 对产品字段建立索引
SalesRecordSchema.index({ 订单来源: 1 }); // 对订单来源字段建立索引
SalesRecordSchema.index({ 导购: 1 }); // 对导购字段建立索引
//售后记录模型定义
const AfterSalesRecordSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team' }, // 将团队字段定义为引用 Team 模型的 ObjectId
: { type: Schema.Types.ObjectId, ref: 'SalesRecord' }, // 关联销售记录
: { type: String, enum: ['退货', '换货', '补发', '补差'], required: true }, // 售后类型
: [{ type: Schema.Types.ObjectId, ref: 'Product' }], // 原订单中的产品,可能多个
: [{ type: Schema.Types.ObjectId, ref: 'Product' }], // 用于替换的产品,可能多个
: { type: Schema.Types.ObjectId, ref: 'AfterSalesRecord' }, // 链接到前一个售后事件,形成链式售后,例如:换货->换货->退货
日期: Date, // 售后日期
原因: String, // 售后原因
产品价格: Number, // 售后产品价格
: { type: String, enum: ['待处理', '处理中', '已处理'], default: '待处理' }, // 售后进度
: { type: String, enum: ['收入', '支出'] },
: { type: Schema.Types.ObjectId, ref: 'PaymentPlatform' }, // 关联收支平台
收支金额: Number, // 统一的收支字段,包含退款金额
待收: Number,
//待收已收默认为0
: {
type: Number,
default: 0,
},
备注: String,
: { type: Schema.Types.ObjectId, ref: 'CustomerPaymentCode' },
: { type: Schema.Types.ObjectId, ref: 'LogisticsRecord' }, // 关联的物流信息
}, { timestamps: true }); // 自动添加创建时间和更新时间
AfterSalesRecordSchema.index({ 团队: 1 }); // 对团队字段建立索引
AfterSalesRecordSchema.index({ 物流信息: 1 }); //对物流信息字段建立索引
AfterSalesRecordSchema.index({ 销售记录: 1 }); // 对销售记录字段建立索引
AfterSalesRecordSchema.index({ 原产品: 1 }); // 对原产品字段建立索引
AfterSalesRecordSchema.index({ 替换产品: 1 }); // 对替换产品字段建立索引
//收款码模型定义
const CustomerPaymentCodeSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team' }, // 将团队字段定义为引用 Team 模型的 ObjectId
收款码: String,
}, { timestamps: true }); // 自动添加创建时间和更新时间
//产品模型定义
const ProductSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team' }, // 将团队字段定义为引用 Team 模型的 ObjectId 表示产品所属团队
: { type: Schema.Types.ObjectId, ref: 'Supplier' }, //关联供应商模型
: { type: Schema.Types.ObjectId, ref: 'Brand' }, //关联品牌模型
: { type: Schema.Types.ObjectId, ref: 'Category' }, //关联品类模型
名称: String,
描述: String,
编码: String,
图片: String,
货号: String,
别名: Array, //别名字段 Array表示数组
级别: String,
//成本,包含成本、包装费、运费
: {
成本价: Number,
包装费: Number,
运费: Number,
},
售价: Number,
库存: Number,
}, { timestamps: true }); // 自动添加创建时间和更新时间
ProductSchema.index({ 团队: 1 }); // 对团队字段建立索引
//收支平台模型定义
const PaymentPlatformSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team' }, // 将团队字段定义为引用 Team 模型的 ObjectId
: { type: Number, required: true }, // 收支平台排序字段
: { type: String, required: true }, // 收支平台名称字段
: { type: String, }, // 收支平台描述字段
: { type: Number, enum: [0, 1, 2, 3, 4], required: true }, // 收支平台状态字段
}, { timestamps: true }); // 自动添加创建时间和更新时间
PaymentPlatformSchema.index({ 团队: 1 }); // 对团队字段建立索引
//物流记录模型定义
const LogisticsRecordSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team' }, // 将团队字段定义为引用 Team 模型的 ObjectId
: { type: String },
: { type: Boolean, default: true }, // 设置默认值为true
: { type: String },
: { type: String },
: { type: String },
: { type: String },
: { type: Date, default: Date.now },
: { type: Schema.Types.ObjectId, required: true, refPath: '类型' },
: {
type: String,
required: true,
enum: [
'SalesRecord',
'AfterSalesRecord'
],
},
// 新增产品字段,可以对应多个产品
: [{ type: Schema.Types.ObjectId, ref: 'Product' }], // 定义为引用 Product 模型的数组,支持多个产品
//发货人
: { type: Schema.Types.ObjectId, ref: 'User' },
}, { timestamps: true }); // 自动添加创建时间和更新时间
LogisticsRecordSchema.index({ 团队: 1 }); // 对团队字段建立索引
LogisticsRecordSchema.index({ 关联记录: 1 }); // 对关联记录字段建立索引
// 日增长数据子文档模型
const DailyGrowthSchema = new Schema({
: { type: Date, required: true },
: { type: Number, required: true },
: { type: Number, required: true },
: { type: Number, required: true }, // 可在前端计算
});
//店铺账号模型定义
const AccountSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team' }, // 将团队字段定义为引用 Team 模型的 ObjectId
//账号负责人: // 关联用户信息模型
: { type: Schema.Types.ObjectId, ref: 'User' },
: { type: Schema.Types.ObjectId, ref: 'User' },
: [{ type: Schema.Types.ObjectId, ref: 'Category' }],
unionid: { type: String, sparse: true }, //sparse: true 表示该字段可以为空,但如果有值,那么值必须是唯一的
openid: { type: String }, // 添加openid字段
: { type: String },
: { type: String },
: { type: String },
: { type: String },
: { type: String },
: { type: String },
//账号状态 0停用 1正常 2异常 3备用
: { type: Number, default: 1 },
: { type: String },
: [DailyGrowthSchema], // 日增长数据数组
}, { timestamps: true }); // 自动添加创建时间和更新时间
AccountSchema.index({ unionid: 1 }, { unique: true, sparse: true });// 创建索引,确保 unionid 只在不为 null 时唯一
AccountSchema.index({ 团队: 1 }); // 对团队字段建立索引
AccountSchema.index({ 账号负责人: 1 }); // 对账号负责人字段建立索引
AccountSchema.index({ 前端引流人员: 1 }); // 对前端引流人员字段建立索引
AccountSchema.index({ '日增长数据.日期': 1 }); // 为日期建立索引,加快按日期查询的速度
// 定义聊天消息模型
const ChatMessageSchema: Schema = new Schema({
: { type: String, enum: ['Human', 'AI'], required: true }, // 消息角色人类或AI
: { type: String, required: true }, // 消息内容
: { type: Boolean, default: false }, // 是否被删除
}, { timestamps: true }); // 自动添加创建时间和更新时间
// 话术分类模型定义 - 用于管理不同类型的话术分类
const ScriptCategorySchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team', required: true }, // 将团队字段定义为引用 Team 模型的 ObjectId
: { type: String, required: true }, // 话术分类名称:如入场话术、营销话术、售后话术等
: { type: String }, // 分类描述,可选
: { type: Number, default: 0 }, // 分类排序字段,用于控制显示顺序
: { type: Schema.Types.ObjectId, ref: 'User' }, // 将创建人字段定义为引用 User 模型的 ObjectId
}, { timestamps: true }); // 自动添加创建时间和更新时间
ScriptCategorySchema.index({ 团队: 1 }); // 对团队字段建立索引
// 话术模型定义 - 用于存储具体的话术内容
const ScriptSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Team', required: true }, // 将团队字段定义为引用 Team 模型的 ObjectId
: { type: Schema.Types.ObjectId, ref: 'ScriptCategory', required: true }, // 将分类字段定义为引用 ScriptCategory 模型的 ObjectId
: { type: String, required: true }, // 具体的话术内容
: { type: Number, default: 0 }, // 在该分类下的排序,用于控制显示顺序
使: { type: Number, default: 0 }, // 记录话术的使用次数,用于统计分析
: { type: Schema.Types.ObjectId, ref: 'User' }, // 将创建人字段定义为引用 User 模型的 ObjectId
: [{ type: Schema.Types.ObjectId, ref: 'TagInfo' }], // 将标签字段定义为引用 TagInfo 模型的 ObjectId 数组,便于分类管理
}, { timestamps: true }); // 自动添加创建时间和更新时间
ScriptSchema.index({ 团队: 1 }); // 对团队字段建立索引
ScriptSchema.index({ 分类: 1 }); // 对分类字段建立索引
ScriptSchema.index({ 使: -1 }); // 对使用次数字段建立降序索引,便于查询热门话术
// 定义聊天会话模型
const ChatSessionSchema: Schema = new Schema({
ID: { type: Schema.Types.ObjectId, ref: 'User', required: true }, // 用户 ID
ID: { type: Schema.Types.ObjectId, ref: 'Model', required: true }, // 模型 ID
: { type: Number, default: -1 }, // 加载的消息数,默认 -1 表示无限
: { type: String, default: '未命名会话' }, // 会话标题
: { type: [ChatMessageSchema], default: [] }, // 消息列表
: { type: Date }, // 过期时间
}, { timestamps: true }); // 自动添加创建时间和更新时间
export const ChatSession = mongoose.models.ChatSession || mongoose.model('ChatSession', ChatSessionSchema);
export const ChatMessage = mongoose.models.ChatMessage || mongoose.model('ChatMessage', ChatMessageSchema);
// 导出模型,使用现有模型(如果已存在)或创建新模型
export const User = mongoose.models.User || mongoose.model('User', UserSchema); //导出用户模型
export const Team = mongoose.models.Team || mongoose.model('Team', TeamSchema); //导出团队模型
export const Role = mongoose.models.Role || mongoose.model('Role', RoleSchema); //导出角色模型
export const Permission = mongoose.models.Permission || mongoose.model('Permission', PermissionSchema); //导出权限模型
export const Customer = mongoose.models.Customer || mongoose.model('Customer', CustomerSchema); //导出客户模型
export const Product = mongoose.models.Product || mongoose.model('Product', ProductSchema); //导出产品模型
export const PaymentPlatform = mongoose.models.PaymentPlatform || mongoose.model('PaymentPlatform', PaymentPlatformSchema); //导出收支平台模型
export const SalesRecord = mongoose.models.SalesRecord || mongoose.model('SalesRecord', SalesRecordSchema); //导出销售记录模型
export const Account = mongoose.models.Account || mongoose.model('Account', AccountSchema); //导出店铺账号模型
export const Category = mongoose.models.Category || mongoose.model('Category', CategorySchema); //导出品类模型
export const Transaction = mongoose.models.Transaction || mongoose.model('Transaction', TransactionSchema);//导出交易记录模型
export const Coupon = mongoose.models.Coupon || mongoose.model('Coupon', CouponSchema);//导出优惠券模型
export const AfterSalesRecord = mongoose.models.AfterSalesRecord || mongoose.model('AfterSalesRecord', AfterSalesRecordSchema); //导出售后记录模型
export const CustomerPaymentCode = mongoose.models.CustomerPaymentCode || mongoose.model('CustomerPaymentCode', CustomerPaymentCodeSchema); //导出收款码模型
export const LogisticsRecord = mongoose.models.LogisticsRecord || mongoose.model('LogisticsRecord', LogisticsRecordSchema); //导出物流记录模型
export const Supplier = mongoose.models.Supplier || mongoose.model('Supplier', SupplierSchema); //导出供应商模型
export const Brand = mongoose.models.Brand || mongoose.model('Brand', BrandSchema); //导出品牌模型
export const ScriptCategory = mongoose.models.ScriptCategory || mongoose.model('ScriptCategory', ScriptCategorySchema); //导出话术分类模型
export const Script = mongoose.models.Script || mongoose.model('Script', ScriptSchema); //导出话术模型
/*
4、用户角色
4.1、平台管理员:可以管理团队成员、管理团队信息
4.2、团队管理员:可以管理团队成员、管理团队信息
4.3、团队成员:只能查看团队信息
4.4、老板
4.5、员工
4.6、财务
4.7、行政
4.8、技术
4.9、等等
*/

364
src/models/types.ts Normal file
View File

@@ -0,0 +1,364 @@
// src/models/types.ts
// 定义用户接口类型
export interface IUser {
_id: string;
姓名: string;
邮箱: string;
电话: string;
密码: string;
团队: ITeam; // 团队字段定义为引用 Team 模型
角色: IRole; // 角色字段定义为引用 Role 模型
微信昵称?: string;
头像?: string;
unionid?: string;
openid?: string;
}
// 定义团队接口类型
export interface ITeam {
_id: string;
名称?: string;
拥有者: IUser; // 引用 IUser 接口,表示拥有者是一个用户
会员开始日期?: Date;
会员结束日期?: Date;
是否永久会员?: boolean;
}
/*
// 套餐模型定义
const PlaneSchema: Schema = new Schema(
{
名称: { type: String, required: true },
价格: { type: Number, required: true },
单位: { type: String }, //表示每个 月/年/季/周/天
会员时长: { type: Number, required: true }, //会员时长,0表示永久会员,单位为天
简介: { type: String }, //简短描述
描述: { type: String }, //详细描述
功能: { type: String }, //套餐的功能列表,;例如:['✓ 无限的浏览者', '✓ 最多2名编辑', '✓ 最多3个项目', '✓ 商业许可']
//按钮1、显示按钮文字 2、按钮链接用于跳转到页面
按钮: {
文字: { type: String },
链接: { type: String },
},
},
{ timestamps: true },
);
export const Plane = mongoose.models.Plane || mongoose.model('Plane', PlaneSchema);
*/
export interface IPlane extends Document {
_id: string;
名称: string;
价格: number;
单位: string; // 表示每个 月/年/季/周/天
会员时长: number; // 以月为单位0表示永久
描述?: string;
功能?: string;
?: {
文字: string;
链接: string;
};
推荐: boolean;
}
// 定义订单接口类型
export interface IOrder extends Document {
用户: IUser;
团队: ITeam;
套餐: IPlane;
价格: number;
: '待支付' | '已完成' | '已失败';
createdAt?: Date;
updatedAt?: Date;
}
// 定义角色接口类型
export interface IRole {
_id: string;
团队: ITeam; // 团队字段定义为引用 Team 模型
排序: number;
名称: string;
描述: string;
状态: number;
主页: string;
权限: IPermission[]; // 权限字段定义为引用 IPermission 接口数组
}
// 定义权限接口类型
export interface IPermission {
_id: string;
团队?: ITeam | null; // 团队可以为 null 或 ITeam 类型
排序?: number;
名称: string;
类型?: number;
描述?: string;
状态: number;
Icon: string;
路径: string;
创建人?: IUser | null; // 创建人可以为 null 或 IUser 类型
//父级?: IPermission | null; // 父级权限,可以是 null 或 ObjectId
父级?: string | null;
子级?: IPermission[];
}
export interface ICustomer {
_id: string;
团队: ITeam;
姓名: string;
电话: string;
: {
省份: string;
城市: string;
区县: string;
详细地址: string;
};
微信?: string;
生日?: Date;
加粉日期?: Date;
createdAt?: Date;
优惠券: ICoupon[];
}
export interface IBrand {
_id: string;
团队: ITeam;
order: number;
name: string;
description: string;
}
export interface ICategory {
_id: string;
团队: ITeam;
name: string;
description: string;
icon: string;
}
export interface ISupplier {
_id: string;
团队: ITeam;
order: number;
供应商名称: string;
: {
电话: string;
联系人: string;
地址: string;
};
供应品类: ICategory[];
status: number;
供应商等级: string;
供应商类型: string;
供应商备注: string;
}
// 定义优惠券接口类型
export interface ICoupon {
_id: ICouponId; // 这里指定`_id`为ICouponId类型
团队: ITeam;
客户: ICustomer;
名称: string;
优惠券类型: string;
券码: string;
描述: string;
金额: number;
折扣: number;
最低消费: number;
生效日期: Date;
失效日期: Date;
已使用: boolean;
createdAt?: Date;
}
export interface ICouponId {
_id: string; // 唯一标识符
优惠券类型: string;
券码: string;
金额: number;
折扣: number;
}
export interface ICustomerCoupon {
_id: ICoupon; // 这里指向的是完整的 Coupon 文档
券码: string;
已使用: boolean;
使用日期: string | null;
}
// 定义产品接口类型
export interface IProduct {
_id: string;
团队: ITeam;
供应商: ISupplier;
品牌: IBrand;
品类: ICategory;
名称: string;
描述: string;
编码: string;
图片: string;
货号: string;
别名: string[];
级别: string;
: {
成本价: number;
包装费: number;
运费: number;
};
售价: number;
库存: number;
}
// 定义账号接口类型
export interface IAccount {
_id: string;
团队: ITeam;
账号负责人: IUser;
前端引流人员: IUser;
账号类型: ICategory[];
unionid: string;
openid: string;
账号编号: string;
微信号: string;
头像: string;
微信昵称: string;
手机编号: string;
项目分配: string;
账号状态: number;
备注: string;
日增长数据: IDailyGrowth[];
createdAt?: Date;
}
// 定义日增长数据接口类型
export interface IDailyGrowth {
日期: Date;
总人数: number;
扣除人数: number;
日增长人数?: number;
}
// 定义目标接口类型
export interface ITarget {
_id: string;
团队: ITeam;
目标名称: string;
目标类型: string;
目标值: number;
相关人员: IUser;
目标周期开始: Date;
目标周期结束: Date;
}
// 定义销售记录接口类型
export interface ISalesRecord {
_id: string;
团队: ITeam;
客户: ICustomer;
产品: IProduct[];
订单来源: IAccount;
收款平台?: IPaymentPlatform;
导购: IUser;
收款类型: string;
成交日期: Date;
产品售价: number;
应收金额: number,
收款金额: number,
客服收款金额: number;
待收款: number;
付款平台: string;
待收已收: number;
成交微信: string;
订单状态: string[];
首次审计: boolean;
二次审计: boolean;
收款状态: string;
货款状态: string;
售后收支: number;
回访日期: Date;
备注: string;
最新物流状态: string;
物流详情: string;
物流单号: string;
售后记录: IAfterSalesRecord[];
createdAt?: Date;
}
// 定义售后记录接口类型
export interface IAfterSalesRecord {
_id: string;
团队: ITeam;
销售记录: ISalesRecord;
: '退货' | '换货' | '补发' | '补差';
原产品: IProduct[];
替换产品: IProduct[];
前一次售后: IAfterSalesRecord;
日期: Date;
原因: string;
产品价格: number;
: '待处理' | '处理中' | '已处理';
: '收入' | '支出';
收支平台: IPaymentPlatform;
收支金额: number;
待收: number;
备注: string;
收款码: ICustomerPaymentCode;
物流信息: ILogisticsRecord;
createdAt?: Date;
}
// 定义收支平台接口类型
export interface IPaymentPlatform {
_id: string;
团队: ITeam;
排序: number;
名称: string;
描述: string;
状态: number;
}
// 定义标签接口类型
export interface ITagInfo {
_id: string;
团队: ITeam;
名称: string;
颜色: string;
icon: string;
}
// 定义物流信息接口类型
export interface ILogisticsRecord {
_id: string;
团队: ITeam;
物流单号: string;
是否查询: boolean;
客户尾号: string;
物流公司: string;
物流详情: string;
更新时间: Date;
物流状态: string;
关联记录: ISalesRecord | IAfterSalesRecord;
: 'SalesRecord' | 'AfterSalesRecord';
createdAt?: Date;
}
// 定义收款码接口类型
export interface ICustomerPaymentCode {
_id: string;
团队: ITeam;
收款码: string;
createdAt?: Date;
}
// 定义交易记录接口类型
export interface ITransaction {
_id: string;
团队: ITeam;
客户: ICustomer;
: '充值' | '消费' | '优惠' | '退款';
关联订单: ISalesRecord;
金额: number;
余额: number;
收款平台: IPaymentPlatform;
日期: Date;
描述: string;
createdAt?: Date;
}

View File

@@ -1,6 +1,105 @@
import "@/styles/globals.css"; /**
import type { AppProps } from "next/app"; * 毛玻璃UI应用程序主入口文件
* @author 阿瑞
* @description 应用程序主入口文件配置毛玻璃风格的Ant Design主题和样式支持主题切换
* @version 3.0.0
* @created 2024-12-19
* @updated 重构主题管理使用独立的useTheme hook
*/
export default function App({ Component, pageProps }: AppProps) { // 关键代码行注释导入React 19兼容包解决Ant Design在React 19中的兼容性问题
return <Component {...pageProps} />; import '@ant-design/v5-patch-for-react-19';
import "@/styles/globals.css";
import "@/styles/antd.css";
import React, { useEffect, useState } from "react";
import type { AppProps } from "next/app";
import { ConfigProvider, App, Spin } from "antd";
import zhCN from "antd/locale/zh_CN";
import { lightTheme, darkTheme } from '../styles/theme/themeConfig';
import { ThemeProvider, useTheme } from '@/hooks/useTheme';
import { useRouter } from "next/router";
import Layout from "@/components/layout/Layout";
import { useAccessToken } from "@/store/userStore"; // 引入用于获取用户Token的钩子
import { LoadingOutlined } from '@ant-design/icons';
// 模块级注释应用配置组件负责Ant Design主题配置
function AppConfigProvider({ children }: { children: React.ReactNode }): React.ReactElement {
const { isDark, mounted } = useTheme();
// 关键代码行注释服务端渲染时显示loading或默认主题避免闪烁
if (!mounted) {
return (
<ConfigProvider
theme={lightTheme} // 关键代码行注释:服务端默认使用明亮主题
locale={zhCN}
>
<App>
<div className="app-container">
{children}
</div>
</App>
</ConfigProvider>
);
}
return (
<ConfigProvider
theme={isDark ? darkTheme : lightTheme}
locale={zhCN}
>
<App>
<div
className="app-container"
data-theme={isDark ? 'dark' : 'light'}
>
{children}
</div>
</App>
</ConfigProvider>
);
}
// 模块级注释:应用主组件
export default function MyApp({ Component, pageProps }: AppProps): React.ReactElement {
const accessToken = useAccessToken(); // 获取用户的 accessToken
const [isLoading, setIsLoading] = useState(true); // 用于判断是否正在检查用户状态
// 检查当前路径是否需要使用 Layout
const router = useRouter();
const needsLayout = () => {
const path = router.pathname;
return !(path === '/' || path.startsWith('/start') || path.startsWith('/ui'));
//return !(path.startsWith('/') || path.startsWith('/start') || path.startsWith('/ui'));
};
// 保护路由:检查用户是否登录,如果需要使用 Layout则必须已登录
useEffect(() => {
if (needsLayout() && !accessToken) {
router.push("/start/login"); // 如果用户未登录且需要 Layout重定向到登录页面
} else {
setIsLoading(false); // 登录检查完成,允许渲染页面
}
}, [router, accessToken]);
// 自定义加载图标
const antIcon = <LoadingOutlined style={{ fontSize: 48 }} spin />;
// 在加载用户状态时,显示一个加载指示器或空白页面,防止渲染页面组件
if (isLoading) {
return (
<div >
<Spin indicator={antIcon} size="large">
<div style={{ padding: '50px' }}>...</div>
</Spin>
</div>
);
}
return (
<ThemeProvider>
<AppConfigProvider>
{needsLayout() ? (
<Layout>
<Component {...pageProps} />
</Layout>
) : (
<Component {...pageProps} />
)}
</AppConfigProvider>
</ThemeProvider>
);
} }

View File

@@ -1,13 +1,81 @@
import { Html, Head, Main, NextScript } from "next/document"; /**
* 文件头注释
* @author 阿瑞
* @description 自定义Document组件支持Ant Design服务端样式提取和Geist字体优化
* @version 2.0.0
* @created 2024-12-19
* @updated 修复字体预加载警告,优化字体加载方式
*/
export default function Document() { import React from 'react';
return ( import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs';
<Html lang="en"> import Document, { Head, Html, Main, NextScript } from 'next/document';
<Head /> import type { DocumentContext } from 'next/document';
<body className="antialiased">
<Main /> // 模块级注释自定义Document组件
<NextScript /> const MyDocument = () => (
</body> <Html lang="zh">
</Html> <Head>
); {/* 关键代码行注释:字体域名预连接,提升字体加载性能 */}
} <link
rel="preconnect"
href="https://fonts.googleapis.com"
/>
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
{/* 关键代码行注释直接加载Geist字体避免预加载警告 */}
<link
href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet"
/>
{/* 关键代码行注释Inter字体作为fallback */}
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet"
/>
</Head>
<body className="antialiased">
<Main />
<NextScript />
</body>
</Html>
);
// 模块级注释:服务端渲染时的样式提取处理
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
// 关键代码行注释创建CSS-in-JS缓存实例
const cache = createCache();
const originalRenderPage = ctx.renderPage;
// 关键代码行注释重写renderPage方法包装App组件以提供样式缓存
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => (
<StyleProvider cache={cache}>
<App {...props} />
</StyleProvider>
),
});
// 关键代码行注释获取初始props
const initialProps = await Document.getInitialProps(ctx);
// 关键代码行注释:从缓存中提取样式
const style = extractStyle(cache, true);
// 关键代码行注释返回包含内联样式的props
return {
...initialProps,
styles: (
<>
{initialProps.styles}
<style dangerouslySetInnerHTML={{ __html: style }} />
</>
),
};
};
export default MyDocument;

View File

@@ -0,0 +1,37 @@
//src\pages\api\buildPermissionTree.ts
import type { IPermission } from '@/models/types';
// 构建权限树的函数
export function buildPermissionTree(permissions: IPermission[]): IPermission[] {
const map: Record<string, IPermission> = {};
const result: IPermission[] = [];
// 首先将所有权限放入一个map中
permissions.forEach(permission => {
let formatted = {
_id: permission._id,
团队: permission.团队,
父级: permission.父级,
名称: permission.名称,
排序: permission.排序,
描述: permission.描述,
类型: permission.类型,
路径: permission.路径,
Icon: permission.Icon,
状态: permission.状态,
: [],
};
map[permission._id] = formatted;
if (!permission.) {
result.push(formatted);
}
else if (map[permission.]) {
map[permission.].!.push(formatted);
}
});
return result;
}

View File

@@ -1,13 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
type Data = {
name: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>,
) {
res.status(200).json({ name: "John Doe" });
}

86
src/pages/api/login.ts Normal file
View File

@@ -0,0 +1,86 @@
// src\pages\api\login.ts
import jwt from 'jsonwebtoken';
import type { NextApiRequest, NextApiResponse } from 'next';
import { User } from '@/models';
import bcrypt from 'bcryptjs';
import connectDB from '@/utils/ConnectDB';
import { buildPermissionTree } from './buildPermissionTree';
import type { IPermission } from '@/models/types';
const JWT_SECRET = process.env.JWT_SECRET || 'secret_key'; // 保证有回退值
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { identifier, } = req.body; // identifier 是邮箱或手机号
try {
// 邮箱正则表达式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
let user;
if (emailRegex.test(identifier)) {
// 如果匹配邮箱格式,用邮箱查找
user = await User.findOne({ 邮箱: identifier });
} else {
// 否则,认为是手机号,用电话字段查找
user = await User.findOne({ 电话: identifier });
}
if (!user) {
// 为了安全,统一返回"用户名或密码错误"
return res.status(401).json({ error: '用户名或密码错误' });
}
const isMatch = await bcrypt.compare(, user.);
if (!isMatch) {
return res.status(401).json({ error: '用户名或密码错误' });
}
// 在这里使用 .populate() 来填充团队和角色信息
await user.populate([
{ path: '团队' },
{
path: '角色',
populate: {
path: '权限',
populate: { path: '子级' },
},
},
]);
// 构建权限树
const permissionsTree = buildPermissionTree(
user...map((perm: IPermission) => perm as IPermission),
);
const userInfo = {
_id: user._id,
邮箱: user.邮箱,
姓名: user.姓名,
团队: user.团队, // 这将是一个完整的团队对象
: {
...user..toObject(), // 将 Mongoose 文档转换为普通对象
权限: permissionsTree, // 使用构建好的权限树替换原始权限列表
},
微信昵称: user.微信昵称,
头像: user.头像,
unionid: user.unionid,
openid: user.openid,
};
const token = jwt.sign({ id: user._id }, JWT_SECRET, { expiresIn: '1h' });
const refreshToken = jwt.sign({ id: user._id }, JWT_SECRET, { expiresIn: '7d' });
res.status(200).json({ userInfo, token, refreshToken });
// 打印用户信息
// console.log('用户信息:', userInfo);
} catch (error) {
console.error('Login error:', error); // 输出更详细的错误信息
res.status(500).json({ error: '服务器错误' });
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,59 @@
//src\pages\api\roles\[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Role } from '@/models';
import connectDB from '@/utils/ConnectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;
switch (method) {
case 'GET':
try {
const role = await Role.findById(id).populate('权限');
if (!role) {
return res.status(404).json({ message: '未找到角色' });
}
res.status(200).json(role);
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
case 'PUT':
try {
const {
,
,
,
,
,
} = req.body;
const updatedRole = await Role.findByIdAndUpdate(id, {
, , , , ,
}, { new: true });
if (!updatedRole) {
return res.status(404).json({ message: '未找到角色' });
}
res.status(200).json({ message: '角色更新成功', role: updatedRole });
} catch (error) {
res.status(400).json({ message: '更新角色失败' });
}
break;
case 'DELETE':
try {
const deletedRole = await Role.findByIdAndDelete(id);
if (!deletedRole) {
return res.status(404).json({ message: '未找到角色' });
}
res.status(200).json({ message: '角色删除成功' });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`不允许 ${method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,56 @@
//src\pages\api\roles\index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Role } from '@/models';
import connectDB from '@/utils/ConnectDB';
import { buildPermissionTree } from '@/pages/api/buildPermissionTree';
import { IPermission } from '@/models/types';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
try {
// 获取角色信息并同时获取关联的权限
const roles = await Role.find()
.populate({
path: '权限',
model: 'Permission',
populate: {
path: '子级',
model: 'Permission'
}
})
.lean();
// 对每个角色的权限进行树状结构处理,同时检查是否存在权限数据
const rolesWithPermissionTree = roles.map(role => ({
...role,
权限: role.权限 ? buildPermissionTree(role. as IPermission[]) : []
}));
// 返回处理后的角色数据
res.status(200).json({ roles: rolesWithPermissionTree });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
} else if (req.method === 'POST') {
try {
const { , , , , , } = req.body;
const newRole = new Role({
,
,
,
,
,
});
await newRole.save();
res.status(201).json({ message: '角色创建成功', role: newRole });
} catch (error) {
res.status(400).json({ message: '创建角色失败' });
}
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`不允许 ${req.method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,57 @@
//src\pages\api\team\create.ts
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/ConnectDB'; // 确保数据库连接
import { Team, User } from '@/models'; // 引入 Team 和 User 模型
import { ITeam } from '@/models/types'; // 引入类型定义
// 该 API 使用 connectDB 包裹以确保数据库已连接
export default connectDB(async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== 'POST') {
return res.status(405).json({ error: '方法不被允许' });
}
const { teamName, userId } = req.body; // 从请求体中获取团队名称和用户ID拥有者
// 校验 teamName 和 userId 是否存在
if (!teamName || !userId) {
return res.status(400).json({ error: '团队名称或用户ID缺失' });
}
try {
// 检查是否已存在同名团队
/*const existingTeam = await Team.findOne({ 名称: teamName });
if (existingTeam) {
return res.status(400).json({ error: '该团队名称已被使用' });
}*/
// 查找用户,确保 userId 是有效的
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ error: '用户不存在' });
}
// 创建团队
const newTeam: ITeam = await Team.create({
名称: teamName,
拥有者: user._id, // 团队的拥有者为当前用户
});
// 更新用户,将用户的团队字段设为新创建的团队
user. = newTeam._id;
await user.save(); // 保存用户更新后的信息
// 返回成功响应,包含新创建的团队信息
res.status(201).json({
message: '团队创建成功',
team: newTeam,
});
} catch (error: any) {
if (error.name === 'ValidationError') {
return res.status(400).json({ error: '无效的数据格式' });
}
// 更详细的错误日志信息,方便调试
console.error('团队创建失败:', error.message || error);
res.status(500).json({ error: '服务器错误,无法创建团队' });
}
});

View File

@@ -0,0 +1,19 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Team } from '@/models';
import connectDB from '@/utils/ConnectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
try {
const teams = await Team.find();
res.status(200).json({ teams });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
} else {
res.setHeader('Allow', ['GET']);
res.status(405).end(`不允许 ${req.method} 方法`);
}
};
export default connectDB(handler);

55
src/pages/api/user.ts Normal file
View File

@@ -0,0 +1,55 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { User } from '@/models'; // 用户模型存储在 models 目录中
import connectDB from '@/utils/ConnectDB'; // 数据库连接工具
import { buildPermissionTree } from './buildPermissionTree'; // 存在的权限树构建工具
export default connectDB(async (req: NextApiRequest, res: NextApiResponse) => {
try {
// 从请求查询参数中获取 userId
const { userId } = req.query;
if (!userId) {
return res.status(400).json({ error: '用户ID缺失' });
}
// 查找用户并填充团队和权限信息
const user = await User.findById(userId)
.populate('团队') // 填充团队信息
.populate({
path: '角色',
populate: {
path: '权限',
populate: { path: '子级' } // 填充权限的子权限
}
});
if (!user) {
return res.status(404).json({ error: '用户未找到' });
}
// 构建权限树
const permissionsTree = buildPermissionTree(user..);
// 构建返回的用户信息
const userInfo = {
_id: user._id,
姓名: user.姓名,
邮箱: user.邮箱,
团队: user.团队, // 团队信息
: {
...user..toObject(),
权限: permissionsTree // 权限树
},
微信昵称: user.微信昵称,
头像: user.头像,
unionid: user.unionid,
openid: user.openid,
};
// 返回用户信息
res.status(200).json({ userInfo });
} catch (error) {
console.error('Error fetching user info:', error);
res.status(500).json({ error: '服务器错误' });
}
});

View File

@@ -0,0 +1,57 @@
//src\pages\api\users\[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { User } from '@/models';
import connectDB from '@/utils/ConnectDB';
import { IUser } from '@/models/types'; // 导入 IUser 接口类型
import bcrypt from 'bcryptjs';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;
switch (method) {
case 'GET':
try {
const user = await User.findById(id).populate('团队').populate('角色') as IUser; // 断言为 IUser 类型
if (!user) {
return res.status(404).json({ message: '未找到用户' });
}
res.status(200).json(user);
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
case 'PUT':
try {
const { , , , , , , , unionid, openid, } = req.body as IUser; // 断言为 IUser 类型
const updateData = { , , , , , , , unionid, openid } as IUser; // 断言为 IUser 类型
// 只有当密码字段存在且非空时,才进行更新密码操作
if () {
updateData. = await bcrypt.hash(, 10);
}
const updatedUser = await User.findByIdAndUpdate(id, updateData, { new: true }) as IUser; // 断言为 IUser 类型
if (!updatedUser) {
return res.status(404).json({ message: '未找到用户' });
}
res.status(200).json({ message: '用户更新成功', user: updatedUser });
} catch (error) {
res.status(400).json({ message: '更新用户失败' });
}
break;
case 'DELETE':
try {
const deletedUser = await User.findByIdAndDelete(id);
if (!deletedUser) {
return res.status(404).json({ message: '未找到用户' });
}
res.status(200).json({ message: '用户删除成功' });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`不允许 ${method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,36 @@
//src\pages\api\users\index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { User } from '@/models';
import connectDB from '@/utils/ConnectDB';
import bcrypt from 'bcryptjs';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
try {
const users = await User.find()
//去掉密码字段
.select('-密码')
.populate('团队')
.populate('角色');
res.status(200).json({ users });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
} else if (req.method === 'POST') {
try {
const { , , , , , , , unionid, openid, } = req.body;
//const 密码 = req.body.密码 || '123456'; // 使用默认密码如果没有提供密码
const hashedPassword = await bcrypt.hash( || '123456', 10); // 使用默认密码如果没有提供密码
const newUser = new User({ , , , 密码: hashedPassword, , , , , unionid, openid });
await newUser.save();
res.status(201).json({ message: '用户创建成功', user: newUser });
} catch (error) {
res.status(400).json({ message: '创建用户失败' });
}
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`不允许 ${req.method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,399 @@
/**
* 文件: src/pages/dashboard/index.module.css
* 作者: 阿瑞
* 功能: 用户仪表板页面样式
* 版本: v1.0.0
* @description 现代简约风格的仪表板样式,包含毛玻璃效果、响应式布局和动画效果
*/
/* ===========================================
主容器样式
=========================================== */
.dashboardContainer {
min-height: 100vh;
background: linear-gradient(135deg,
rgba(116, 235, 213, 0.1) 0%,
rgba(159, 172, 230, 0.1) 35%,
rgba(255, 206, 236, 0.1) 100%);
padding: 24px;
animation: fadeInUp 0.6s ease-out;
}
.dashboardHeader {
margin-bottom: 32px;
padding: 24px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.headerContent {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
flex-wrap: wrap;
gap: 16px;
}
.welcomeSection {
flex: 1;
min-width: 300px;
}
.welcomeTitle {
margin: 0 !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 600;
}
.welcomeSubtitle {
font-size: 16px;
opacity: 0.8;
margin-top: 8px;
display: block;
}
.headerActions {
flex-shrink: 0;
}
.dashboardContent {
max-width: 1200px;
margin: 0 auto;
}
/* ===========================================
卡片通用样式
=========================================== */
.userInfoCard,
.teamInfoCard,
.rolePermissionCard {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 16px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.1),
0 1px 0 rgba(255, 255, 255, 0.8) inset;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
height: 100%;
overflow: hidden;
}
.userInfoCard:hover,
.teamInfoCard:hover,
.rolePermissionCard:hover {
transform: translateY(-4px);
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.15),
0 1px 0 rgba(255, 255, 255, 0.9) inset;
}
/* ===========================================
用户信息卡片样式
=========================================== */
.userHeader {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 16px;
}
.userAvatar {
border: 3px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
flex-shrink: 0;
}
.userBasicInfo {
flex: 1;
min-width: 0;
}
.userName {
margin: 0 !important;
font-size: 24px;
font-weight: 600;
color: #1a1a1a;
word-break: break-word;
}
.userRole {
font-size: 16px;
margin-top: 4px;
display: block;
}
.wechatTag {
margin-top: 8px;
border-radius: 20px;
font-size: 12px;
}
.divider {
margin: 16px 0;
border-color: rgba(0, 0, 0, 0.06);
}
.userDescriptions :global(.ant-descriptions-item-label) {
font-weight: 500;
color: #666;
min-width: 80px;
}
.userDescriptions :global(.ant-descriptions-item-content) {
color: #333;
}
/* ===========================================
团队信息卡片样式
=========================================== */
.teamContent {
padding: 4px 0;
}
.teamHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 8px;
}
.teamHeader h4 {
margin: 0 !important;
color: #1a1a1a;
flex: 1;
min-width: 0;
word-break: break-word;
}
/* ===========================================
角色权限卡片样式
=========================================== */
.roleContent {
padding: 4px 0;
}
.roleHeader {
margin-bottom: 20px;
}
.roleHeader h4 {
margin: 0 0 8px 0 !important;
color: #1a1a1a;
}
.permissionsList {
margin-top: 16px;
}
.permissionsList h5 {
margin-bottom: 12px !important;
font-size: 14px;
color: #666;
font-weight: 600;
}
.permissionItem {
padding: 8px 0 !important;
border-bottom: 1px solid rgba(0, 0, 0, 0.04) !important;
}
.permissionItem:last-child {
border-bottom: none !important;
}
.permissionIcon {
display: inline-block;
width: 20px;
text-align: center;
color: #1890ff;
font-size: 16px;
}
.permissionDesc {
font-size: 12px;
margin-top: 2px;
display: block;
}
/* ===========================================
响应式设计
=========================================== */
@media (max-width: 768px) {
.dashboardContainer {
padding: 16px;
}
.headerContent {
flex-direction: column;
align-items: flex-start;
text-align: center;
}
.welcomeSection {
min-width: auto;
width: 100%;
}
.headerActions {
width: 100%;
display: flex;
justify-content: center;
}
.userHeader {
flex-direction: column;
text-align: center;
gap: 16px;
}
.userBasicInfo {
text-align: center;
}
.teamHeader {
flex-direction: column;
align-items: flex-start;
text-align: left;
}
.welcomeTitle {
font-size: 24px !important;
}
}
@media (max-width: 480px) {
.dashboardContainer {
padding: 12px;
}
.userInfoCard,
.teamInfoCard,
.rolePermissionCard {
border-radius: 12px;
}
.userName {
font-size: 20px !important;
}
.welcomeTitle {
font-size: 20px !important;
}
}
/* ===========================================
动画效果
=========================================== */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* 为卡片添加延迟动画 */
.userInfoCard {
animation: slideInLeft 0.6s ease-out 0.1s both;
}
.teamInfoCard {
animation: slideInLeft 0.6s ease-out 0.2s both;
}
.rolePermissionCard {
animation: slideInLeft 0.6s ease-out 0.3s both;
}
/* ===========================================
Ant Design 组件覆盖样式
=========================================== */
/* Card 组件覆盖 */
.userInfoCard :global(.ant-card-body),
.teamInfoCard :global(.ant-card-body),
.rolePermissionCard :global(.ant-card-body) {
padding: 24px;
}
.teamInfoCard :global(.ant-card-head),
.rolePermissionCard :global(.ant-card-head) {
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
background: rgba(255, 255, 255, 0.5);
}
.teamInfoCard :global(.ant-card-head-title),
.rolePermissionCard :global(.ant-card-head-title) {
font-weight: 600;
color: #1a1a1a;
}
/* Badge 组件覆盖 */
.teamInfoCard :global(.ant-badge-status-text),
.teamContent :global(.ant-badge-status-text) {
font-weight: 500;
}
/* Button 组件覆盖 */
.headerActions :global(.ant-btn) {
border-radius: 8px;
font-weight: 500;
height: 40px;
padding: 0 20px;
display: flex;
align-items: center;
gap: 6px;
}
.headerActions :global(.ant-btn-primary) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.headerActions :global(.ant-btn-primary:hover) {
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.5);
}
.headerActions :global(.ant-btn-dangerous) {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
border: none;
color: white;
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
}
.headerActions :global(.ant-btn-dangerous:hover) {
background: linear-gradient(135deg, #ff5252 0%, #e91e63 100%);
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(255, 107, 107, 0.5);
}

View File

@@ -0,0 +1,405 @@
/**
* 文件: src/pages/dashboard/index.tsx
* 作者: 阿瑞
* 功能: 用户信息仪表板页面
* 版本: v1.0.0
* @description 登录成功后的用户信息展示页面,包含用户基本信息、团队信息、角色权限等
*/
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import {
Card,
Avatar,
Typography,
Row,
Col,
Tag,
Descriptions,
Button,
Space,
message,
Badge,
Divider,
List,
Empty
} from 'antd';
import {
UserOutlined,
TeamOutlined,
SafetyOutlined,
SettingOutlined,
LogoutOutlined,
PhoneOutlined,
MailOutlined,
CalendarOutlined,
CrownOutlined
} from '@ant-design/icons';
import { useUserInfo, useUserActions } from '@/store/userStore';
import { IUser } from '@/models/types';
import styles from './index.module.css';
const { Title, Text } = Typography;
// ===========================================
// 类型定义区域
// ===========================================
/**
* 用户信息卡片组件属性
*/
interface UserInfoCardProps {
userInfo: Partial<IUser>;
loading?: boolean;
}
/**
* 团队信息卡片组件属性
*/
interface TeamInfoCardProps {
userInfo: Partial<IUser>;
}
/**
* 角色权限卡片组件属性
*/
interface RolePermissionCardProps {
userInfo: Partial<IUser>;
}
// ===========================================
// 子组件区域
// ===========================================
/**
* 用户基本信息卡片组件
*/
const UserInfoCard: React.FC<UserInfoCardProps> = ({ userInfo, loading = false }) => (
<Card
className={styles.userInfoCard}
loading={loading}
bordered={false}
>
<div className={styles.userHeader}>
<Avatar
size={80}
src={userInfo.}
icon={<UserOutlined />}
className={styles.userAvatar}
/>
<div className={styles.userBasicInfo}>
<Title level={3} className={styles.userName}>
{userInfo. || '未设置姓名'}
</Title>
<Text type="secondary" className={styles.userRole}>
{userInfo.?. || '暂无角色'}
</Text>
{userInfo. && (
<Tag color="green" className={styles.wechatTag}>
: {userInfo.}
</Tag>
)}
</div>
</div>
<Divider className={styles.divider} />
<Descriptions column={1} size="small" className={styles.userDescriptions}>
<Descriptions.Item
label={<><MailOutlined /> </>}
>
{userInfo. || '未设置'}
</Descriptions.Item>
<Descriptions.Item
label={<><PhoneOutlined /> </>}
>
{userInfo. || '未设置'}
</Descriptions.Item>
<Descriptions.Item
label={<><TeamOutlined /> </>}
>
{userInfo.?. || '暂无团队'}
</Descriptions.Item>
</Descriptions>
</Card>
);
/**
* 团队信息卡片组件
*/
const TeamInfoCard: React.FC<TeamInfoCardProps> = ({ userInfo }) => {
const [mounted, setMounted] = useState(false);
const team = userInfo.;
useEffect(() => {
setMounted(true);
}, []);
if (!team) {
return (
<Card title="团队信息" className={styles.teamInfoCard} bordered={false}>
<Empty description="暂无团队信息" />
</Card>
);
}
// 计算会员状态
const getMembershipStatus = () => {
if (team.) {
return { status: 'success', text: '永久会员' };
}
if (team.) {
const endDate = new Date(team.);
const now = new Date();
const isExpired = endDate < now;
return {
status: isExpired ? 'error' : 'processing',
text: isExpired ? '会员已过期' : '会员有效'
};
}
return { status: 'default', text: '普通用户' };
};
const membershipStatus = getMembershipStatus();
return (
<Card
title={
<Space>
<TeamOutlined />
</Space>
}
className={styles.teamInfoCard}
bordered={false}
>
<div className={styles.teamContent}>
<div className={styles.teamHeader}>
<Title level={4}>{team.}</Title>
<Badge
status={membershipStatus.status as any}
text={membershipStatus.text}
/>
</div>
<Descriptions column={1} size="small">
{team. && mounted && (
<Descriptions.Item
label={<><CalendarOutlined /> </>}
>
{new Date(team.).toLocaleDateString('zh-CN')}
</Descriptions.Item>
)}
{team. && !team. && mounted && (
<Descriptions.Item
label={<><CalendarOutlined /> </>}
>
{new Date(team.).toLocaleDateString('zh-CN')}
</Descriptions.Item>
)}
{team. && (
<Descriptions.Item
label={<><CrownOutlined /> </>}
>
<Tag color="gold"></Tag>
</Descriptions.Item>
)}
</Descriptions>
</div>
</Card>
);
};
/**
* 角色权限卡片组件
*/
const RolePermissionCard: React.FC<RolePermissionCardProps> = ({ userInfo }) => {
const role = userInfo.;
if (!role) {
return (
<Card title="角色权限" className={styles.rolePermissionCard} bordered={false}>
<Empty description="暂无角色信息" />
</Card>
);
}
return (
<Card
title={
<Space>
<SafetyOutlined />
</Space>
}
className={styles.rolePermissionCard}
bordered={false}
>
<div className={styles.roleContent}>
<div className={styles.roleHeader}>
<Title level={4}>{role.}</Title>
<Text type="secondary">{role.}</Text>
</div>
{role. && role..length > 0 && (
<div className={styles.permissionsList}>
<Title level={5}></Title>
<List
size="small"
dataSource={role.}
renderItem={(permission: any) => (
<List.Item className={styles.permissionItem}>
<Space>
<span className={styles.permissionIcon}>
{permission.Icon}
</span>
<div>
<Text strong>{permission.}</Text>
{permission. && (
<div>
<Text type="secondary" className={styles.permissionDesc}>
{permission.}
</Text>
</div>
)}
</div>
</Space>
</List.Item>
)}
/>
</div>
)}
</div>
</Card>
);
};
// ===========================================
// 主组件区域
// ===========================================
/**
* 用户仪表板主组件
*/
const Dashboard: React.FC = () => {
const router = useRouter();
const userInfo = useUserInfo();
const { clearUserInfoAndToken, fetchAndSetUserInfo } = useUserActions();
const [loading, setLoading] = useState(false);
const [mounted, setMounted] = useState(false); // 添加mounted状态避免hydration错误
// 页面加载时检查用户登录状态
useEffect(() => {
setMounted(true); // 标记组件已挂载
if (!userInfo || !userInfo._id) {
message.warning('请先登录');
router.push('/start/login');
return;
}
// 刷新用户信息
handleRefreshUserInfo();
}, []);
/**
* 刷新用户信息
*/
const handleRefreshUserInfo = async () => {
setLoading(true);
try {
await fetchAndSetUserInfo();
message.success('用户信息已更新');
} catch (error) {
console.error('刷新用户信息失败:', error);
} finally {
setLoading(false);
}
};
/**
* 处理用户退出登录
*/
const handleLogout = () => {
clearUserInfoAndToken();
message.success('已成功退出登录');
router.push('/start/login');
};
/**
* 跳转到设置页面
*/
const handleGoToSettings = () => {
message.info('设置功能开发中...');
};
// 在组件未挂载或用户未登录时不渲染内容
if (!mounted || !userInfo || !userInfo._id) {
return null;
}
return (
<div className={styles.dashboardContainer}>
{/* 页面头部 */}
<div className={styles.dashboardHeader}>
<div className={styles.headerContent}>
<div className={styles.welcomeSection}>
<Title level={2} className={styles.welcomeTitle}>
{userInfo. || '用户'}
</Title>
<Text type="secondary" className={styles.welcomeSubtitle}>
{mounted ? new Date().toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
}) : ''}
</Text>
</div>
<div className={styles.headerActions}>
<Space>
<Button
icon={<SettingOutlined />}
onClick={handleGoToSettings}
>
</Button>
<Button
icon={<LogoutOutlined />}
onClick={handleLogout}
danger
>
退
</Button>
</Space>
</div>
</div>
</div>
{/* 主要内容区域 */}
<div className={styles.dashboardContent}>
<Row gutter={[24, 24]}>
{/* 用户基本信息 */}
<Col xs={24} lg={8}>
<UserInfoCard userInfo={userInfo} loading={loading} />
</Col>
{/* 团队信息 */}
<Col xs={24} lg={8}>
<TeamInfoCard userInfo={userInfo} />
</Col>
{/* 角色权限 */}
<Col xs={24} lg={8}>
<RolePermissionCard userInfo={userInfo} />
</Col>
</Row>
</div>
</div>
);
};
export default Dashboard;

1156
src/pages/index.module.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,115 +1,597 @@
import Image from "next/image"; /**
import { Geist, Geist_Mono } from "next/font/google"; * 毛玻璃风格私域管理系统首页
* @author 阿瑞
* @description 展示现代简约毛玻璃质感UI效果和系统概览的首页
* @version 3.0.0
* @created 2024-12-19
* @updated 重新设计的现代化UI布局 + CSS模块化架构
*/
const geistSans = Geist({ import React from 'react';
variable: "--font-geist-sans", import {
subsets: ["latin"], Layout,
}); Typography,
Button,
Card,
Space,
Row,
Col,
Avatar,
Statistic,
Badge,
Tag,
Divider,
FloatButton,
App,
} from 'antd';
import {
RocketOutlined,
ApiOutlined,
BulbOutlined,
TeamOutlined,
ThunderboltOutlined,
QuestionCircleOutlined,
SecurityScanOutlined,
ArrowRightOutlined,
PlayCircleOutlined,
CheckCircleOutlined,
GlobalOutlined,
SafetyOutlined,
LineChartOutlined,
} from '@ant-design/icons';
import { useTheme } from '@/hooks/useTheme'; // 关键代码行注释使用独立的useTheme Hook
import { MdDarkMode, MdLightMode } from "react-icons/md";
import styles from './index.module.css'; // 关键代码行注释导入CSS模块样式
const geistMono = Geist_Mono({ const { Header, Content, Footer } = Layout;
variable: "--font-geist-mono", const { Title, Paragraph, Text } = Typography;
subsets: ["latin"],
});
export default function Home() { // 模块级注释:特性数据接口
return ( interface FeatureType {
<div title: string;
className={`${geistSans.className} ${geistMono.className} grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]`} description: string;
> icon: React.ReactNode;
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start"> color: string;
<Image }
className="dark:invert"
src="/next.svg" // 模块级注释:核心特性数据
alt="Next.js logo" const coreFeatures = [
width={180} {
height={38} title: '智能多租户架构',
priority description: '企业级数据隔离,确保每个团队的数据安全独立,支持灵活的权限管理和资源分配',
/> icon: <SecurityScanOutlined />,
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]"> },
<li className="mb-2 tracking-[-.01em]"> {
Get started by editing{" "} title: '实时数据洞察',
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold"> description: '强大的数据分析引擎,提供实时业务指标监控,智能预警和趋势分析',
src/pages/index.tsx icon: <LineChartOutlined />,
</code> },
. {
</li> title: '无缝团队协作',
<li className="tracking-[-.01em]"> description: '集成化协作平台,支持实时消息、任务管理、文档共享和视频会议',
Save and see your changes instantly. icon: <TeamOutlined />,
</li> },
</ol> {
<div className="flex gap-4 items-center flex-col sm:flex-row"> title: '企业级安全',
<a description: 'SOC2认证的安全架构端到端加密支持SSO和多因素认证',
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto" icon: <SafetyOutlined />,
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app" }
target="_blank" ];
rel="noopener noreferrer"
> // 模块级注释:统计数据
<Image const statsData = [
className="dark:invert" {
src="/vercel.svg" title: '全球企业用户',
alt="Vercel logomark" value: 50000,
width={20} suffix: '+',
height={20} prefix: '',
/> icon: <GlobalOutlined />,
Deploy now color: '#667eea',
</a> growth: '+127%',
<a },
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]" {
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app" title: '月活跃团队',
target="_blank" value: 12543,
rel="noopener noreferrer" suffix: '',
> prefix: '',
Read our docs icon: <TeamOutlined />,
</a> color: '#f093fb',
growth: '+23.6%',
},
{
title: 'API调用次数',
value: 98.7,
suffix: 'M',
prefix: '',
icon: <ApiOutlined />,
color: '#4facfe',
growth: '+45.2%',
},
{
title: '系统可用性',
value: 99.9,
suffix: '%',
prefix: '',
icon: <CheckCircleOutlined />,
color: '#06d7b2',
growth: '99.9%',
}
];
// 模块级注释:技术栈特性数据
const techFeatures: FeatureType[] = [
{
title: 'React 19',
description: '使用最新的 React 19 特性构建现代化应用',
icon: <RocketOutlined />,
color: '#2d7ff9',
},
{
title: 'Next.js 15',
description: '基于 Next.js 15 的强大全栈框架',
icon: <ApiOutlined />,
color: '#06d7b2',
},
{
title: 'Ant Design 5',
description: '使用 Ant Design 5.x 构建优雅的用户界面',
icon: <BulbOutlined />,
color: '#ff9640',
},
{
title: 'TypeScript',
description: '完整的 TypeScript 支持,提供类型安全',
icon: <ThunderboltOutlined />,
color: '#8e6bff',
},
];
export default function HomePage(): React.ReactElement {
// 关键代码行注释:状态管理 - 使用_app.tsx的useTheme Hook确保mounted状态
const { isDark, toggleTheme, mounted, isTransitioning } = useTheme(); // 关键代码行注释:解构获取所需的状态
const { message, notification, modal } = App.useApp();
// 关键代码行注释:如果还未挂载,显示加载状态避免闪烁
if (!mounted) {
return <div style={{ height: '100vh', background: '#f6f9fc' }} />;
}
// 模块级注释:事件处理函数
const handleStartClick = (): void => {
message.success('欢迎使用现代化私域管理系统!');
};
const handleLearnMoreClick = (): void => {
notification.info({
message: '了解更多',
description: '感谢您对我们平台的关注。更多功能正在开发中,敬请期待!',
placement: 'topRight',
});
};
const handleHelpClick = (): void => {
modal.info({
title: '帮助中心',
content: (
<div>
<p>使</p>
<p></p>
<p>support@example.com</p>
</div> </div>
</main> ),
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center"> });
<a };
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app" return (
target="_blank" <Layout className={styles.modernLayout}>
rel="noopener noreferrer" {/* 模块级注释:现代化顶部导航栏 */}
> <Header className={styles.modernHeader}>
<Image <div className={styles.headerContainer}>
aria-hidden {/* 左侧品牌区域 */}
src="/file.svg" <div className={styles.brandSection}>
alt="File icon" <div className={styles.brandLogo}>
width={16} <div className={styles.logoIcon}></div>
height={16} <div className={styles.logoText}>
/> <Title level={4} className={styles.brandTitle}>SaaS管理平台</Title>
Learn <Text className={styles.brandSubtitle}>Enterprise Edition</Text>
</a> </div>
<a </div>
className="flex items-center gap-2 hover:underline hover:underline-offset-4" </div>
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank" {/* 中间导航菜单 */}
rel="noopener noreferrer" <nav className={styles.navMenu}>
> <a href="#features" className={styles.navItem}></a>
<Image <a href="#stats" className={styles.navItem}></a>
aria-hidden <a href="#tech" className={styles.navItem}></a>
src="/window.svg" <a href="#about" className={styles.navItem}></a>
alt="Window icon" </nav>
width={16}
height={16} {/* 右侧操作区域 */}
/> <div className={styles.headerActions}>
Examples {/* 关键代码行注释主题切换按钮使用React Icons替代Ant Design Switch添加增强的视觉效果 */}
</a> <Button
<a type="text"
className="flex items-center gap-2 hover:underline hover:underline-offset-4" shape="round"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app" onClick={toggleTheme}
target="_blank" className={`${styles.themeSwitch} theme-switch-ripple`}
rel="noopener noreferrer" disabled={isTransitioning}
> title={isDark ? '切换到明亮模式' : '切换到暗黑模式'}
<Image aria-label={isDark ? '切换到明亮模式' : '切换到暗黑模式'}
aria-hidden >
src="/globe.svg" {isDark ? <MdLightMode /> : <MdDarkMode />}
alt="Globe icon" </Button>
width={16}
height={16} <Button type="text" className={styles.loginBtn}>
/>
Go to nextjs.org </Button>
</a>
</footer> <Button type="primary" className={styles.signupBtn}>
</div>
</Button>
</div>
</div>
</Header>
{/* 主要内容区域 */}
<Content className={styles.modernContent}>
{/* 模块级注释:英雄区域 */}
<section className={styles.heroSectionModern}>
<div className={styles.heroBackground}>
<div className={`${styles.heroDecoration} ${styles.heroDecoration1}`} />
<div className={`${styles.heroDecoration} ${styles.heroDecoration2}`} />
<div className={`${styles.heroDecoration} ${styles.heroDecoration3}`} />
</div>
<div className={styles.heroContainer}>
<div className={styles.heroContentModern}>
{/* 版本徽章 */}
<div className={styles.heroBadge}>
<Badge
count="v3.0.0"
className={styles.versionBadge}
color="blue"
/>
<Text className={styles.versionText}></Text>
</div>
{/* 主标题 */}
<Title level={1} className={styles.heroTitleModern}>
<span className={styles.titleLine}></span>
<span className={`${styles.titleLine} ${styles.titleGradient}`}>SaaS智能私域生态</span>
</Title>
{/* 描述文字 */}
<Paragraph className={styles.heroDescriptionModern}>
</Paragraph>
{/* CTA按钮组 */}
<div className={styles.heroButtonsModern}>
<Button
type="primary"
size="large"
icon={<RocketOutlined />}
onClick={handleStartClick}
className={styles.primaryCtaBtn}
>
</Button>
<Button
size="large"
icon={<PlayCircleOutlined />}
onClick={handleLearnMoreClick}
className={styles.secondaryCtaBtn}
>
</Button>
</div>
{/* 用户统计 */}
<div className={styles.heroStats}>
<div className={styles.statItem}>
<Text className={styles.statNumber}>50K+</Text>
<Text className={styles.statLabel}></Text>
</div>
<div className={styles.statDivider} />
<div className={styles.statItem}>
<Text className={styles.statNumber}>99.9%</Text>
<Text className={styles.statLabel}></Text>
</div>
<div className={styles.statDivider} />
<div className={styles.statItem}>
<Text className={styles.statNumber}>24/7</Text>
<Text className={styles.statLabel}></Text>
</div>
</div>
</div>
{/* 右侧可视化展示 */}
<div className={styles.heroVisual}>
<div className={styles.dashboardMockup}>
<div className={styles.mockupHeader}>
<div className={styles.mockupControls}>
<div className={`${styles.controlDot} ${styles.red}`} />
<div className={`${styles.controlDot} ${styles.yellow}`} />
<div className={`${styles.controlDot} ${styles.green}`} />
</div>
<Text className={styles.mockupTitle}></Text>
</div>
<div className={styles.mockupContent}>
<div className={styles.mockupSidebar}>
<div className={`${styles.sidebarItem} ${styles.active}`} />
<div className={styles.sidebarItem} />
<div className={styles.sidebarItem} />
<div className={styles.sidebarItem} />
</div>
<div className={styles.mockupMain}>
<div className={styles.chartContainer}>
<div className={styles.chartBars}>
<div className={styles.chartBar} style={{ height: '60%' }} />
<div className={styles.chartBar} style={{ height: '80%' }} />
<div className={styles.chartBar} style={{ height: '45%' }} />
<div className={styles.chartBar} style={{ height: '90%' }} />
<div className={styles.chartBar} style={{ height: '65%' }} />
</div>
</div>
<div className={styles.metricsGrid}>
<div className={styles.metricCard} />
<div className={styles.metricCard} />
<div className={styles.metricCard} />
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* 模块级注释:核心特性展示区域 */}
<section className={styles.featuresSectionModern} id="features">
<div className={styles.sectionContainer}>
<div className={styles.sectionHeader}>
<Title level={2} className={styles.sectionTitle}>
</Title>
<Paragraph className={styles.sectionDescription}>
</Paragraph>
</div>
<Row gutter={[32, 32]}>
{coreFeatures.map((feature, index) => (
<Col xs={24} sm={12} lg={6} key={index}>
<Card
className={styles.featureCardModern}
variant="borderless"
>
<div className={styles.featureContent}>
<div className={`${styles.featureIconModern} ${styles[`gradient${index + 1}`]}`}>
{feature.icon}
</div>
<Title level={4} className={styles.featureTitleModern}>
{feature.title}
</Title>
<Paragraph className={styles.featureDescriptionModern}>
{feature.description}
</Paragraph>
<div className={styles.featureAction}>
<Button type="link" icon={<ArrowRightOutlined />}>
</Button>
</div>
</div>
</Card>
</Col>
))}
</Row>
</div>
</section>
{/* 模块级注释:数据统计展示区域 */}
<section className={styles.statsSectionModern} id="stats">
<div className={styles.sectionContainer}>
<div className={styles.statsHeader}>
<Title level={2} className={styles.statsTitle}>
</Title>
<Paragraph className={styles.statsDescription}>
</Paragraph>
</div>
<Row gutter={[32, 32]}>
{statsData.map((stat, index) => (
<Col xs={24} sm={12} lg={6} key={index}>
<Card
className={styles.statCardModern}
variant="borderless"
>
<div className={styles.statContent}>
<div className={styles.statIcon} style={{ color: stat.color }}>
{stat.icon}
</div>
<div className={styles.statInfo}>
<Statistic
value={stat.value}
suffix={stat.suffix}
prefix={stat.prefix}
className={styles.statNumber}
/>
<Text className={styles.statTitle}>{stat.title}</Text>
<Text className={styles.statGrowth}>{stat.growth}</Text>
</div>
</div>
</Card>
</Col>
))}
</Row>
</div>
</section>
{/* 模块级注释:技术栈展示区域 */}
<section className={styles.techSectionModern} id="tech">
<div className={styles.sectionContainer}>
<div className={styles.techHeader}>
<Title level={2} className={styles.techTitle}>
</Title>
<Paragraph className={styles.techDescription}>
</Paragraph>
</div>
<Row gutter={[32, 32]}>
{techFeatures.map((tech, index) => (
<Col xs={24} sm={12} lg={6} key={index}>
<Card
className={styles.techCardModern}
variant="borderless"
>
<div className={styles.techContent}>
<Avatar
size={64}
style={{ backgroundColor: tech.color }}
icon={tech.icon}
className={styles.techAvatar}
/>
<Title level={4} className={styles.techName}>
{tech.title}
</Title>
<Paragraph className={styles.techDesc}>
{tech.description}
</Paragraph>
</div>
</Card>
</Col>
))}
</Row>
{/* 技术标签云 */}
<div className={styles.techTags}>
<Title level={4}></Title>
<Space size={[0, 8]} wrap className={styles.tagsContainer}>
<Tag color="cyan">Node.js</Tag>
<Tag color="blue">React 19</Tag>
<Tag color="green">Next.js 15</Tag>
<Tag color="orange">Ant Design 5</Tag>
<Tag color="purple">TypeScript</Tag>
<Tag color="cyan">Tailwind CSS</Tag>
<Tag color="red">MongoDB</Tag>
<Tag color="geekblue">Mongoose 8.x</Tag>
<Tag color="green">Vercel</Tag>
<Tag color="orange">Docker</Tag>
<Tag color="volcano">pnpm</Tag>
</Space>
</div>
</div>
</section>
{/* 模块级注释:行动号召区域 */}
<section className={styles.ctaSectionModern} id="about">
<div className={styles.sectionContainer}>
<Card className={styles.ctaCard} variant="borderless">
<div className={styles.ctaContent}>
<Title level={2} className={styles.ctaTitle}>
</Title>
<Paragraph className={styles.ctaDescription}>
30
</Paragraph>
<Space size="large" className={styles.ctaButtons}>
<Button
type="primary"
size="large"
icon={<RocketOutlined />}
onClick={handleStartClick}
className={styles.ctaPrimaryBtn}
>
</Button>
<Button
size="large"
icon={<TeamOutlined />}
onClick={handleLearnMoreClick}
className={styles.ctaSecondaryBtn}
>
</Button>
</Space>
</div>
</Card>
</div>
</section>
</Content>
{/* 模块级注释:现代化页脚 */}
<Footer className={styles.modernFooter}>
<div className={styles.footerContainer}>
<div className={styles.footerContent}>
{/* 品牌信息 */}
<div className={styles.footerBrand}>
<div className={styles.footerLogo}>
<div className={styles.logoIcon}></div>
<Title level={4} className={styles.footerBrandTitle}>
SaaS管理平台
</Title>
</div>
<Paragraph className={styles.footerBrandDesc}>
</Paragraph>
</div>
{/* 快速链接 */}
<div className={styles.footerLinks}>
<div className={styles.linkGroup}>
<Title level={5}></Title>
<a href="#features"></a>
<a href="#pricing"></a>
<a href="#integrations"></a>
<a href="#security"></a>
</div>
<div className={styles.linkGroup}>
<Title level={5}></Title>
<a href="#enterprise"></a>
<a href="#startup"></a>
<a href="#education"></a>
<a href="#nonprofit"></a>
</div>
<div className={styles.linkGroup}>
<Title level={5}></Title>
<a href="#docs"></a>
<a href="#api">API参考</a>
<a href="#community"></a>
<a href="#contact"></a>
</div>
</div>
</div>
<Divider />
<div className={styles.footerBottom}>
<Text className={styles.copyright}>
© 2024 SaaS管理平台. .
</Text>
<Space>
<a href="#privacy"></a>
<a href="#terms"></a>
<a href="#cookies">Cookie政策</a>
</Space>
</div>
</div>
</Footer>
{/* 浮动帮助按钮 */}
<FloatButton
icon={<QuestionCircleOutlined />}
type="primary"
tooltip="需要帮助?"
onClick={handleHelpClick}
/>
</Layout>
); );
} }

560
src/pages/index.tsx.bak Normal file
View File

@@ -0,0 +1,560 @@
/**
* 毛玻璃风格私域管理系统首页
* @author 阿瑞
* @description 展示现代简约毛玻璃质感UI效果和系统概览的首页
* @version 3.0.0
* @created 2024-12-19
* @updated 重新设计的现代化UI布局
*/
import React from 'react';
import {
Layout,
Typography,
Button,
Card,
Space,
Row,
Col,
Avatar,
Statistic,
Badge,
Tag,
Divider,
FloatButton,
App,
} from 'antd';
import {
RocketOutlined,
ApiOutlined,
BulbOutlined,
TeamOutlined,
ThunderboltOutlined,
QuestionCircleOutlined,
SecurityScanOutlined,
ArrowRightOutlined,
PlayCircleOutlined,
CheckCircleOutlined,
GlobalOutlined,
SafetyOutlined,
LineChartOutlined,
} from '@ant-design/icons';
import { useTheme } from '../hooks/useTheme'; // 关键代码行注释使用独立的useTheme Hook
import { MdDarkMode, MdLightMode } from "react-icons/md";
const { Header, Content, Footer } = Layout;
const { Title, Paragraph, Text } = Typography;
// 模块级注释:特性数据接口
interface FeatureType {
title: string;
description: string;
icon: React.ReactNode;
color: string;
}
// 模块级注释:核心特性数据
const coreFeatures = [
{
title: '智能多租户架构',
description: '企业级数据隔离,确保每个团队的数据安全独立,支持灵活的权限管理和资源分配',
icon: <SecurityScanOutlined />,
},
{
title: '实时数据洞察',
description: '强大的数据分析引擎,提供实时业务指标监控,智能预警和趋势分析',
icon: <LineChartOutlined />,
},
{
title: '无缝团队协作',
description: '集成化协作平台,支持实时消息、任务管理、文档共享和视频会议',
icon: <TeamOutlined />,
},
{
title: '企业级安全',
description: 'SOC2认证的安全架构端到端加密支持SSO和多因素认证',
icon: <SafetyOutlined />,
}
];
// 模块级注释:统计数据
const statsData = [
{
title: '全球企业用户',
value: 50000,
suffix: '+',
prefix: '',
icon: <GlobalOutlined />,
color: '#667eea',
growth: '+127%',
},
{
title: '月活跃团队',
value: 12543,
suffix: '',
prefix: '',
icon: <TeamOutlined />,
color: '#f093fb',
growth: '+23.6%',
},
{
title: 'API调用次数',
value: 98.7,
suffix: 'M',
prefix: '',
icon: <ApiOutlined />,
color: '#4facfe',
growth: '+45.2%',
},
{
title: '系统可用性',
value: 99.9,
suffix: '%',
prefix: '',
icon: <CheckCircleOutlined />,
color: '#06d7b2',
growth: '99.9%',
}
];
// 模块级注释:技术栈特性数据
const techFeatures: FeatureType[] = [
{
title: 'React 19',
description: '使用最新的 React 19 特性构建现代化应用',
icon: <RocketOutlined />,
color: '#2d7ff9',
},
{
title: 'Next.js 15',
description: '基于 Next.js 15 的强大全栈框架',
icon: <ApiOutlined />,
color: '#06d7b2',
},
{
title: 'Ant Design 5',
description: '使用 Ant Design 5.x 构建优雅的用户界面',
icon: <BulbOutlined />,
color: '#ff9640',
},
{
title: 'TypeScript',
description: '完整的 TypeScript 支持,提供类型安全',
icon: <ThunderboltOutlined />,
color: '#8e6bff',
},
];
export default function HomePage(): React.ReactElement {
// 关键代码行注释:状态管理 - 使用_app.tsx的useTheme Hook确保mounted状态
const { isDark, toggleTheme, mounted } = useTheme(); // 关键代码行注释:解构获取所需的状态
const { message, notification, modal } = App.useApp();
// 关键代码行注释:如果还未挂载,显示加载状态避免闪烁
if (!mounted) {
return <div style={{ height: '100vh', background: '#f6f9fc' }} />;
}
// 模块级注释:事件处理函数
const handleStartClick = (): void => {
message.success('欢迎使用现代化私域管理系统!');
};
const handleLearnMoreClick = (): void => {
notification.info({
message: '了解更多',
description: '感谢您对我们平台的关注。更多功能正在开发中,敬请期待!',
placement: 'topRight',
});
};
const handleHelpClick = (): void => {
modal.info({
title: '帮助中心',
content: (
<div>
<p>使</p>
<p></p>
<p>support@example.com</p>
</div>
),
});
};
return (
<Layout className="modern-layout">
{/* 模块级注释:现代化顶部导航栏 */}
<Header className="modern-header">
<div className="header-container">
{/* 左侧品牌区域 */}
<div className="brand-section">
<div className="brand-logo">
<div className="logo-icon"></div>
<div className="logo-text">
<Title level={4} className="brand-title">SaaS管理平台</Title>
<Text className="brand-subtitle">Enterprise Edition</Text>
</div>
</div>
</div>
{/* 中间导航菜单 */}
<nav className="nav-menu">
<a href="#features" className="nav-item"></a>
<a href="#pricing" className="nav-item"></a>
<a href="#docs" className="nav-item"></a>
<a href="#components" className="nav-item"></a>
<a href="#about" className="nav-item"></a>
</nav>
{/* 右侧操作区域 */}
<div className="header-actions">
{/* 切换主题 使用react-icons*/}
<Button
type="text"
shape="round"
onClick={toggleTheme}
icon={isDark ? <MdLightMode /> : <MdDarkMode />}
className="theme-switch"
/>
<Button type="text" className="login-btn">
</Button>
<Button type="primary" className="signup-btn">
</Button>
</div>
</div>
</Header>
{/* 模块级注释:现代化主要内容区域 */}
<Content className="modern-content">
{/* 模块级注释:重新设计的英雄区域 */}
<section className="hero-section-modern">
<div className="hero-background">
<div className="hero-decoration hero-decoration-1"></div>
<div className="hero-decoration hero-decoration-2"></div>
<div className="hero-decoration hero-decoration-3"></div>
</div>
<div className="hero-container">
<div className="hero-content-modern">
<div className="hero-badge">
<Badge count="NEW" className="version-badge" />
<Text className="version-text">Version 3.0 </Text>
</div>
<Title level={1} className="hero-title-modern">
<span className="title-line"></span>
<span className="title-line title-gradient"></span>
</Title>
<Paragraph className="hero-description-modern">
SaaS管理平台
<br />
</Paragraph>
<div className="hero-buttons-modern">
<Button
type="primary"
size="large"
onClick={handleStartClick}
className="primary-cta-btn"
icon={<RocketOutlined />}
>
</Button>
<Button
size="large"
onClick={handleLearnMoreClick}
className="secondary-cta-btn"
icon={<PlayCircleOutlined />}
>
</Button>
</div>
<div className="hero-stats">
<div className="stat-item">
<Text className="stat-number">50K+</Text>
<Text className="stat-label"></Text>
</div>
<div className="stat-divider"></div>
<div className="stat-item">
<Text className="stat-number">99.9%</Text>
<Text className="stat-label"></Text>
</div>
<div className="stat-divider"></div>
<div className="stat-item">
<Text className="stat-number">24/7</Text>
<Text className="stat-label"></Text>
</div>
</div>
</div>
<div className="hero-visual">
<div className="dashboard-mockup">
<div className="mockup-header">
<div className="mockup-controls">
<span className="control-dot red"></span>
<span className="control-dot yellow"></span>
<span className="control-dot green"></span>
</div>
<Text className="mockup-title">SaaS Dashboard</Text>
</div>
<div className="mockup-content">
<div className="mockup-sidebar">
<div className="sidebar-item active"></div>
<div className="sidebar-item"></div>
<div className="sidebar-item"></div>
<div className="sidebar-item"></div>
</div>
<div className="mockup-main">
<div className="chart-container">
<div className="chart-bars">
<div className="chart-bar" style={{ height: '60%' }}></div>
<div className="chart-bar" style={{ height: '80%' }}></div>
<div className="chart-bar" style={{ height: '45%' }}></div>
<div className="chart-bar" style={{ height: '70%' }}></div>
<div className="chart-bar" style={{ height: '90%' }}></div>
</div>
</div>
<div className="metrics-grid">
<div className="metric-card"></div>
<div className="metric-card"></div>
<div className="metric-card"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* 模块级注释:核心功能展示区域 */}
<section className="features-section-modern">
<div className="section-container">
<div className="section-header">
<Title level={2} className="section-title">
</Title>
<Paragraph className="section-description">
</Paragraph>
</div>
<Row gutter={[32, 32]} className="features-grid">
{coreFeatures.map((feature, index) => (
<Col xs={24} md={12} lg={6} key={index}>
<Card className="feature-card-modern" hoverable>
<div className="feature-content">
<div className={`feature-icon-modern gradient-${index + 1}`}>
{feature.icon}
</div>
<Title level={4} className="feature-title-modern">
{feature.title}
</Title>
<Paragraph className="feature-description-modern">
{feature.description}
</Paragraph>
<div className="feature-action">
<Button type="link" icon={<ArrowRightOutlined />}>
</Button>
</div>
</div>
</Card>
</Col>
))}
</Row>
</div>
</section>
{/* 模块级注释:数据统计展示区域 */}
<section className="stats-section-modern">
<div className="section-container">
<div className="stats-header">
<Title level={2} className="stats-title">
</Title>
<Paragraph className="stats-description">
</Paragraph>
</div>
<Row gutter={[24, 24]} className="stats-grid">
{statsData.map((stat, index) => (
<Col xs={24} sm={12} lg={6} key={index}>
<Card className="stat-card-modern">
<div className="stat-content">
<div className="stat-icon" style={{ color: stat.color }}>
{stat.icon}
</div>
<div className="stat-info">
<Statistic
value={stat.value}
suffix={stat.suffix}
prefix={stat.prefix}
className="stat-number"
/>
<Text className="stat-title">{stat.title}</Text>
<Text className="stat-growth">{stat.growth}</Text>
</div>
</div>
</Card>
</Col>
))}
</Row>
</div>
</section>
{/* 模块级注释:技术栈展示区域 */}
<section className="tech-section-modern">
<div className="section-container">
<div className="tech-header">
<Title level={2} className="tech-title">
</Title>
<Paragraph className="tech-description">
</Paragraph>
</div>
<Row gutter={[24, 24]} className="tech-grid">
{techFeatures.map((tech, index) => (
<Col xs={24} sm={12} md={6} key={index}>
<Card className="tech-card-modern" hoverable>
<div className="tech-content">
<Avatar
size={64}
style={{ backgroundColor: tech.color }}
icon={tech.icon}
className="tech-avatar"
/>
<Title level={4} className="tech-name">
{tech.title}
</Title>
<Paragraph className="tech-desc">
{tech.description}
</Paragraph>
</div>
</Card>
</Col>
))}
</Row>
<div className="tech-tags">
<Title level={5}></Title>
<Space wrap size="middle" className="tags-container">
<Tag color="blue">React 19</Tag>
<Tag color="green">Next.js 15</Tag>
<Tag color="orange">Ant Design 5</Tag>
<Tag color="purple">TypeScript</Tag>
<Tag color="cyan">Tailwind CSS</Tag>
<Tag color="red">MongoDB</Tag>
<Tag color="geekblue">Mongoose 8.x</Tag>
<Tag color="volcano">pnpm</Tag>
</Space>
</div>
</div>
</section>
{/* 模块级注释CTA区域 */}
<section className="cta-section-modern">
<div className="section-container">
<Card className="cta-card">
<div className="cta-content">
<Title level={2} className="cta-title">
</Title>
<Paragraph className="cta-description">
30SaaS管理平台的强大功能
</Paragraph>
<Space size="large" className="cta-buttons">
<Button
type="primary"
size="large"
icon={<RocketOutlined />}
className="cta-primary-btn"
>
</Button>
<Button
size="large"
icon={<QuestionCircleOutlined />}
className="cta-secondary-btn"
>
</Button>
</Space>
</div>
</Card>
</div>
</section>
</Content>
{/* 模块级注释:现代化页脚 */}
<Footer className="modern-footer">
<div className="footer-container">
<div className="footer-content">
<div className="footer-brand">
<div className="footer-logo">
<div className="logo-icon"></div>
<Title level={4} className="footer-brand-title">SaaS管理平台</Title>
</div>
<Paragraph className="footer-brand-desc">
</Paragraph>
</div>
<div className="footer-links">
<div className="link-group">
<Title level={5}></Title>
<a href="#features"></a>
<a href="#pricing"></a>
<a href="#security"></a>
</div>
<div className="link-group">
<Title level={5}></Title>
<a href="#docs">API文档</a>
<a href="#sdk">SDK下载</a>
<a href="#github">GitHub</a>
</div>
<div className="link-group">
<Title level={5}></Title>
<a href="#help"></a>
<a href="#contact"></a>
<a href="#status"></a>
</div>
</div>
</div>
<Divider />
<div className="footer-bottom">
<Text className="copyright">
© 2024 SaaS管理平台. All rights reserved.
</Text>
<Space split={<Divider type="vertical" />}>
<a href="#privacy"></a>
<a href="#terms"></a>
<a href="#cookies">Cookie政策</a>
</Space>
</div>
</div>
</Footer>
{/* 模块级注释:浮动操作按钮 */}
<FloatButton.Group>
<FloatButton
icon={<QuestionCircleOutlined />}
tooltip="帮助中心"
onClick={handleHelpClick}
/>
<FloatButton.BackTop tooltip="回到顶部" />
</FloatButton.Group>
</Layout>
);
}

View File

@@ -0,0 +1,417 @@
//src\pages\management\user\index.tsx
import React, { useState, useEffect } from 'react';
import { Table, Button, Card, Popconfirm, Input, Space, Tag, Avatar, Typography, Tooltip, App } from 'antd';
import { SearchOutlined, UserAddOutlined, UserOutlined, TeamOutlined, IdcardOutlined, PhoneOutlined, MailOutlined } from '@ant-design/icons';
import UserModal from './user-modal'; // 引入用户模态框组件
import { IUser } from '@/models/types'; // 确保有正确的类型定义
import { IconButton, Iconify } from '@/components/icon';
import { ColumnsType } from 'antd/es/table';
const { Title, Text } = Typography;
const UsersPage = () => {
const [users, setUsers] = useState<IUser[]>([]);
const [filteredUsers, setFilteredUsers] = useState<IUser[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [currentUser, setCurrentUser] = useState<IUser | null>(null);
const [searchText, setSearchText] = useState('');
const [loading, setLoading] = useState(false);
const [isMobile, setIsMobile] = useState(false);
// 使用App上下文获取message
const { message } = App.useApp();
// 监听屏幕尺寸变化
useEffect(() => {
const checkIsMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkIsMobile();
window.addEventListener('resize', checkIsMobile);
return () => {
window.removeEventListener('resize', checkIsMobile);
};
}, []);
useEffect(() => {
fetchUsers();
}, []);
useEffect(() => {
if (searchText.trim() === '') {
setFilteredUsers(users);
} else {
const filtered = users.filter(user =>
user.?.toLowerCase().includes(searchText.toLowerCase()) ||
user.?.includes(searchText) ||
user.?.toLowerCase().includes(searchText.toLowerCase())
);
setFilteredUsers(filtered);
}
}, [searchText, users]);
const fetchUsers = async () => {
setLoading(true);
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
setUsers(data.users);
setFilteredUsers(data.users);
} catch (error) {
message.error('加载用户数据失败');
} finally {
setLoading(false);
}
};
const handleModalOk = () => {
setIsModalVisible(false);
fetchUsers();
};
const handleEdit = (user: IUser) => {
setCurrentUser(user);
setIsModalVisible(true);
};
const handleDelete = async (id: string) => {
try {
const response = await fetch(`/api/users/${id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to delete user');
}
fetchUsers();
message.success('用户删除成功');
} catch (error) {
message.error('删除用户失败');
}
};
// 获取角色颜色
const getRoleColor = (roleName: string) => {
const colorMap: Record<string, string> = {
'管理员': '#f50',
'超级管理员': '#722ed1',
'团队管理员': '#108ee9',
'普通用户': '#87d068',
'客服': '#2db7f5',
'运营': '#faad14'
};
return colorMap[roleName] || '#87d068';
};
// 移动端用户卡片组件
const UserCard = ({ user }: { user: IUser }) => (
<Card
//size="small"
//className="mb-3 shadow-sm hover:shadow-md transition-shadow border border-gray-200 rounded-lg"
//bodyStyle={{ padding: '12px 16px' }}
>
<div className="flex items-start justify-between">
{/* 左侧用户信息 */}
<div className="flex items-center space-x-3 flex-1 min-w-0">
<Avatar
size={48}
{...(user.头像 && user.头像.trim() ? { src: user.头像 } : {})}
icon={(!user. || !user..trim()) && <UserOutlined />}
className="flex-shrink-0 border-2 border-blue-100"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center mb-1">
<Text strong className="text-base truncate">{user.}</Text>
</div>
{/* 联系信息 */}
<div className="space-y-1">
<div className="flex items-center text-xs text-gray-600">
<MailOutlined className="mr-1 text-blue-500" />
<Text className="truncate">{user.}</Text>
</div>
<div className="flex items-center text-xs text-gray-600">
<PhoneOutlined className="mr-1 text-green-500" />
<Text>{user.}</Text>
</div>
</div>
{/* 标签信息 */}
<div className="flex flex-wrap gap-1 mt-2">
{user. ? (
<Tag icon={<TeamOutlined />} color="cyan" className="text-xs">
{user..}
</Tag>
) : (
<Tag color="default" className="text-xs"></Tag>
)}
{user. ? (
<Tag
icon={<IdcardOutlined />}
color={getRoleColor(user..)}
className="text-xs"
>
{user..}
</Tag>
) : (
<Tag color="default" className="text-xs"></Tag>
)}
</div>
</div>
</div>
{/* 右侧操作按钮 */}
<div className="flex flex-col space-y-1 ml-2">
<Tooltip title="编辑用户" placement="left">
<IconButton
onClick={() => handleEdit(user)}
className="hover:bg-blue-50 transition-colors w-8 h-8"
>
<Iconify icon="solar:pen-bold-duotone" size={16} className="text-blue-500" />
</IconButton>
</Tooltip>
<Tooltip title="删除用户" placement="left">
<Popconfirm
title="确定要删除这个用户吗?"
description="删除后将无法恢复,请谨慎操作。"
onConfirm={() => handleDelete(user._id)}
okText="确定"
cancelText="取消"
placement="left"
okButtonProps={{ danger: true }}
>
<IconButton
className="hover:bg-red-50 transition-colors w-8 h-8"
>
<Iconify icon="mingcute:delete-2-fill" size={16} className="text-red-500" />
</IconButton>
</Popconfirm>
</Tooltip>
</div>
</div>
</Card>
);
const columns: ColumnsType<IUser> = [
{
title: '用户信息',
dataIndex: '姓名',
key: 'userInfo',
render: (_, record) => (
<div className="flex items-center space-x-3">
<Avatar
size={40}
{...(record.头像 && record.头像.trim() ? { src: record.头像 } : {})}
icon={(!record. || !record..trim()) && <UserOutlined />}
className="flex-shrink-0 border-2 border-blue-100"
/>
<div className="flex flex-col">
<Text strong>{record.}</Text>
<Text type="secondary" className="text-xs">
{record.}
</Text>
</div>
</div>
),
},
{
title: '联系电话',
dataIndex: '电话',
key: 'phone',
render: (phone) => (
<Tag icon={<Iconify icon="solar:phone-bold" size={14} />} color="blue">
{phone}
</Tag>
),
},
{
title: '团队',
dataIndex: '团队',
key: 'team',
render: (team: any) => (
team ? (
<Tag icon={<TeamOutlined />} color="cyan">
{team.}
</Tag>
) : (
<Tag color="default"></Tag>
)
)
},
{
title: '角色',
dataIndex: '角色',
key: 'role',
render: (role: any) => (
role ? (
<Tag
icon={<IdcardOutlined />}
color={getRoleColor(role.)}
className="py-1"
>
{role.}
</Tag>
) : (
<Tag color="default"></Tag>
)
)
},
{
title: '操作',
width: 120,
align: 'center',
key: 'action',
render: (_: any, record: IUser) => (
<Space size="small" className="flex justify-center">
<Tooltip
title="编辑用户"
getPopupContainer={(triggerNode) => triggerNode.parentElement || document.body}
>
<IconButton onClick={() => handleEdit(record)} className="hover:bg-blue-50 transition-colors">
<Iconify icon="solar:pen-bold-duotone" size={18} className="text-blue-500" />
</IconButton>
</Tooltip>
<Tooltip
title="删除用户"
getPopupContainer={(triggerNode) => triggerNode.parentElement || document.body}
>
<Popconfirm
title="确定要删除这个用户吗?"
description="删除后将无法恢复,请谨慎操作。"
onConfirm={() => handleDelete(record._id)}
okText="确定"
cancelText="取消"
placement="left"
okButtonProps={{ danger: true }}
getPopupContainer={(triggerNode) => triggerNode.parentElement || document.body}
>
<IconButton className="hover:bg-red-50 transition-colors">
<Iconify icon="mingcute:delete-2-fill" size={18} className="text-red-500" />
</IconButton>
</Popconfirm>
</Tooltip>
</Space>
),
},
];
return (
<div className="h-full flex flex-col px-2 sm:px-0">
{/* 用户列表卡片 */}
<Card
title={
<div className="flex items-center">
<Iconify icon="solar:users-group-rounded-bold-duotone" size={22} className="text-primary mr-2" />
<Title level={4} style={{ margin: 0 }} className="text-sm sm:text-base">
</Title>
</div>
}
className="shadow-md hover:shadow-lg transition-shadow flex-1 flex flex-col"
extra={
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3 w-full sm:w-auto">
<Input
placeholder="搜索用户"
value={searchText}
onChange={e => setSearchText(e.target.value)}
prefix={<SearchOutlined />}
allowClear
className="rounded-md w-full sm:w-48"
size={isMobile ? 'middle' : 'middle'}
/>
<Button
type="primary"
icon={<UserAddOutlined />}
onClick={() => { setIsModalVisible(true); setCurrentUser(null); }}
className="rounded-md flex items-center justify-center whitespace-nowrap"
size={isMobile ? 'middle' : 'middle'}
block={isMobile}
>
{isMobile ? '添加用户' : '添加用户'}
</Button>
</div>
}
styles={{
body: {
padding: isMobile ? '8px' : '16px',
display: 'flex',
flexDirection: 'column',
flex: 1,
overflow: 'hidden',
}
}}
>
{/* 桌面端显示表格 */}
{!isMobile ? (
<div className="flex-1 flex flex-col overflow-hidden">
<Table
sticky
loading={loading}
scroll={{ y: 'calc(100vh - 300px)', x: 'max-content' }}
pagination={{
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
defaultPageSize: 10,
pageSizeOptions: ['10', '20', '50'],
}}
size='middle'
columns={columns}
dataSource={filteredUsers}
rowKey="_id"
rowClassName="hover:bg-blue-50"
className="rounded-lg overflow-hidden flex-1"
/>
</div>
) : (
/* 移动端显示卡片列表 */
<div className="flex-1 flex flex-col overflow-hidden">
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex justify-center items-center py-8">
<div className="text-gray-500">...</div>
</div>
) : filteredUsers.length === 0 ? (
<div className="flex justify-center items-center py-8">
<div className="text-gray-500"></div>
</div>
) : (
<>
{filteredUsers.map((user) => (
<UserCard key={user._id} user={user} />
))}
</>
)}
</div>
{/* 移动端分页信息 */}
{!loading && filteredUsers.length > 0 && (
<div className="flex justify-center py-2 border-t border-gray-100 mt-2">
<div className="text-xs text-gray-500">
{filteredUsers.length}
</div>
</div>
)}
</div>
)}
</Card>
{isModalVisible && (
<UserModal
visible={isModalVisible}
onOk={handleModalOk}
onCancel={() => setIsModalVisible(false)}
user={currentUser}
/>
)}
</div>
);
};
export default UsersPage;

View File

@@ -0,0 +1,175 @@
//src\pages\management\user\user-modal.tsx
import React, { useEffect, useState } from 'react';
import { Modal, Form, Input, message, Select } from 'antd';
import { IRole, ITeam, IUser } from '@/models/types';
interface UserModalProps {
visible: boolean;
onCancel: () => void;
onOk: () => void; // 更新 onOk 类型,不需要传递参数
user: IUser | null;
}
const UserModal: React.FC<UserModalProps> = ({ visible, onOk, onCancel, user }) => {
const [form] = Form.useForm();
const [teams, setTeams] = useState<ITeam[]>([]);
const [roles, setRoles] = useState<IRole[]>([]);
useEffect(() => {
fetchTeams();
fetchRoles();
}, []);
useEffect(() => {
if (user) {
form.setFieldsValue({
姓名: user.姓名,
电话: user.电话,
邮箱: user.邮箱,
团队: user.团队?._id, // 确保团队和角色是对象,并包含 _id
角色: user.角色?._id
});
} else {
form.resetFields(); // 当没有用户数据时重置表单
}
}, [user, form]); // 依赖项包括 user 和 form
const fetchTeams = async () => {
try {
const response = await fetch('/api/team');
if (!response.ok) {
throw new Error('Failed to fetch teams');
}
const data = await response.json();
setTeams(data.teams);
} catch (error) {
message.error('加载团队数据失败');
}
};
const fetchRoles = async () => {
try {
const response = await fetch('/api/roles');
if (!response.ok) {
throw new Error('Failed to fetch roles');
}
const data = await response.json();
setRoles(data.roles);
} catch (error) {
message.error('加载角色数据失败');
}
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
const method = user ? 'PUT' : 'POST';
const url = user ? `/api/users/${user._id}` : '/api/users';
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
});
if (!response.ok) {
throw new Error('Failed to save user');
}
message.success('用户操作成功');
onOk(); // 直接调用 onOk 通知外部重新加载
} catch (info) {
console.error('Validate Failed:', info);
message.error('用户操作失败');
}
};
return (
<Modal
title={user ? '编辑用户' : '添加新用户'}
open={visible}
onOk={handleSubmit}
onCancel={() => {
form.resetFields();
onCancel();
}}
>
<Form
form={form}
layout="vertical"
initialValues={user ? { ...user } : {}}
>
<Form.Item
name="头像"
label="头像"
rules={[{ required: false, message: '请选择头像!' }]}
>
<Input />
</Form.Item>
<Form.Item
name="姓名"
label="姓名"
rules={[{ required: true, message: '请输入姓名!' }]}
>
<Input />
</Form.Item>
<Form.Item
name="电话"
label="电话"
rules={[{ required: true, message: '请输入电话!' }]}
>
<Input />
</Form.Item>
<Form.Item
name="邮箱"
label="邮箱"
rules={[{ required: true, message: '请输入邮箱!' }]}
>
<Input />
</Form.Item>
<Form.Item
name="密码"
label="密码"
rules={[{ required: false, message: '请输入密码!' }]}
>
<Input.Password
placeholder="填写以更改密码,留空则保持不变"
autoComplete="off" // 禁用自动完成
/>
</Form.Item>
<Form.Item
name="团队"
label="团队"
rules={[{ required: true, message: '请选择团队!' }]}
>
<Select placeholder="请选择团队">
{teams.map(team => (
<Select.Option key={team._id} value={team._id}>
{team.}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="角色"
label="角色"
rules={[{ required: true, message: '请选择角色!' }]}
>
<Select placeholder="请选择角色">
{roles.map(role => (
<Select.Option key={role._id} value={role._id}>
{role.}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>
);
};
export default UserModal;

View File

@@ -0,0 +1,118 @@
/**
* 文件: src/components/StartLayout.tsx
* 作者: 阿瑞
* 功能: 登录/注册页面通用布局 - 毛玻璃风格
* 版本: v2.0.0
* @updated 优化毛玻璃效果支持
*/
import { Row, Col, Card } from 'antd';
import StartLeftContent from './StartLeftContent';
import useDevice from '@/store/useDevice';
interface StartLayoutProps {
children: React.ReactNode;
cardStyle?: React.CSSProperties;
cardClassName?: string;
}
const StartLayout = ({ children, cardStyle, cardClassName }: StartLayoutProps) => {
// 关键代码行注释:判断设备类型,响应式布局
const device = useDevice();
// 关键代码行注释:移动端布局 - 全屏卡片模式
if (device === 'mobile') {
return (
<div
style={{
width: '100%',
height: '100vh',
background: 'var(--bg-gradient)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '16px',
overflow: 'hidden'
}}
>
<Card
className={`glass-card ${cardClassName || ''}`}
style={{
width: '100%',
maxWidth: '400px',
height: 'calc(100vh - 32px)',
maxHeight: '90vh',
background: 'var(--glass-bg)',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
border: '1px solid var(--glass-border)',
borderRadius: 'var(--border-radius-xl)',
boxShadow: '0 20px 60px var(--glass-shadow)',
padding: 0,
overflow: 'hidden',
...cardStyle,
}}
bodyStyle={{
padding: 0,
height: '100%',
overflow: 'auto'
}}
>
{children}
</Card>
</div>
);
}
// 关键代码行注释:桌面端布局 - 左右分栏模式
return (
<Row className="min-h-screen" style={{ height: '100vh', overflow: 'hidden' }}>
{/* 左侧内容 */}
<StartLeftContent />
{/* 右侧表单 */}
<Col
xs={24}
md={14}
style={{
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--bg-gradient)',
padding: '20px',
overflow: 'hidden'
}}
>
<Card
className={`glass-card ${cardClassName || ''}`}
style={{
width: '100%',
maxWidth: '480px',
height: 'calc(100vh - 40px)',
maxHeight: '95vh',
background: 'var(--glass-bg)',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
border: '1px solid var(--glass-border)',
borderRadius: 'var(--border-radius-xl)',
boxShadow: '0 20px 60px var(--glass-shadow)',
padding: 0,
transition: 'var(--transition-all)',
overflow: 'hidden',
...cardStyle,
}}
bodyStyle={{
padding: 0,
borderRadius: 'var(--border-radius-xl)',
height: '100%',
overflow: 'auto'
}}
>
{children}
</Card>
</Col>
</Row>
);
}
export default StartLayout;

View File

@@ -0,0 +1,227 @@
/**
* 启动页面左侧内容样式模块
* @author 阿瑞
* @description 毛玻璃风格左侧宣传区域样式
* @version 1.0.0
* @created 2024-12-19
*/
/* ==================== 左侧内容容器 ==================== */
.leftContent {
background: var(--glass-bg) !important;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-right: 1px solid var(--glass-border);
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
padding: 32px !important;
position: relative;
overflow: hidden;
}
/* 关键代码行注释:为左侧添加装饰性渐变背景 */
.leftContent::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
135deg,
rgba(45, 127, 249, 0.05) 0%,
rgba(167, 133, 255, 0.03) 50%,
rgba(45, 212, 191, 0.02) 100%
);
pointer-events: none;
z-index: 0;
}
.leftContent::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(
circle at 30% 40%,
rgba(45, 127, 249, 0.08) 0%,
transparent 50%
);
pointer-events: none;
z-index: 0;
animation: floatingGradient 8s ease-in-out infinite;
}
@keyframes floatingGradient {
0%, 100% {
transform: translate(-50%, -50%) rotate(0deg);
}
50% {
transform: translate(-45%, -45%) rotate(180deg);
}
}
/* ==================== 内容包装器 ==================== */
.contentWrapper {
max-width: 420px;
text-align: center;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
/* ==================== 品牌区域 ==================== */
.brandSection {
margin-bottom: 8px;
}
.brandTitle {
color: var(--text-primary) !important;
font-size: 32px !important;
font-weight: 700 !important;
margin-bottom: 8px !important;
background: linear-gradient(135deg, var(--color-primary-blue) 0%, var(--color-primary-purple) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1.2 !important;
text-shadow: 0 2px 4px rgba(45, 127, 249, 0.1);
}
.brandSubtitle {
color: var(--text-secondary) !important;
font-size: 18px !important;
font-weight: 500 !important;
margin: 0 0 16px 0 !important;
letter-spacing: 0.5px;
}
/* ==================== 图片区域 ==================== */
.imageSection {
margin: 16px 0;
position: relative;
}
.welcomeImage {
max-width: 100% !important;
height: auto !important;
max-height: 280px;
filter: drop-shadow(0 8px 32px rgba(45, 127, 249, 0.15));
transition: var(--transition-all);
animation: imageFloat 6s ease-in-out infinite;
}
.welcomeImage:hover {
transform: scale(1.02);
filter: drop-shadow(0 12px 40px rgba(45, 127, 249, 0.2));
}
@keyframes imageFloat {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
/* ==================== 描述区域 ==================== */
.descriptionSection {
margin-top: 8px;
}
.description {
color: var(--text-secondary) !important;
font-size: 16px !important;
line-height: 1.6 !important;
margin-bottom: 0 !important;
opacity: 0.9;
font-weight: 400;
letter-spacing: 0.3px;
}
/* ==================== 响应式设计 ==================== */
@media (max-width: 768px) {
.leftContent {
padding: 24px 20px !important;
border-right: none;
border-bottom: 1px solid var(--glass-border);
}
.contentWrapper {
max-width: 100%;
gap: 16px;
}
.brandTitle {
font-size: 26px !important;
}
.brandSubtitle {
font-size: 16px !important;
}
.welcomeImage {
max-height: 200px;
}
.description {
font-size: 14px !important;
}
}
@media (max-width: 480px) {
.leftContent {
padding: 16px !important;
min-height: 300px;
}
.contentWrapper {
gap: 12px;
}
.brandSection {
margin-bottom: 4px;
}
.brandTitle {
font-size: 22px !important;
}
.brandSubtitle {
font-size: 14px !important;
margin-bottom: 8px !important;
}
.imageSection {
margin: 8px 0;
}
.welcomeImage {
max-height: 160px;
}
.description {
font-size: 13px !important;
line-height: 1.5 !important;
}
}
/* ==================== 动画优化 ==================== */
@media (prefers-reduced-motion: reduce) {
.leftContent::after,
.welcomeImage {
animation: none !important;
}
.welcomeImage:hover {
transform: none !important;
}
}

View File

@@ -0,0 +1,40 @@
/**
* 文件: src/pages/start/components/StartLeftContent.tsx
* 作者: 阿瑞
* 功能: 启动页面左侧内容组件 - 毛玻璃风格
* 版本: v2.0.0
* @updated 应用毛玻璃风格设计优化UI/UX体验
*/
import { Col, Typography } from 'antd';
import styles from './StartLeftContent.module.css';
const { Title, Text } = Typography;
const StartLeftContent = () => (
<Col xs={24} md={10} className={styles.leftContent}>
<div className={styles.contentWrapper}>
<div className={styles.brandSection}>
<Title level={2} className={styles.brandTitle}>
Aoun Admin Plus
</Title>
<h2 className={styles.brandSubtitle}></h2>
</div>
<div className={styles.imageSection}>
<img
src="/welcome.svg"
alt="Welcome"
className={styles.welcomeImage}
/>
</div>
<div className={styles.descriptionSection}>
<Text className={styles.description}>
Aoun Admin ReactAnt DesignTypeScript
</Text>
</div>
</div>
</Col>
);
export default StartLeftContent;

View File

@@ -0,0 +1,456 @@
/**
* 忘记密码页面样式模块
* @author 阿瑞
* @description 毛玻璃风格忘记密码页面样式
* @version 1.0.0
* @created 2024-12-19
*/
/* ==================== 忘记密码页面容器 ==================== */
.forgotPasswordContainer {
padding: 24px 32px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
overflow: hidden;
box-sizing: border-box;
}
/* ==================== 品牌头部区域 ==================== */
.brandSection {
text-align: center;
margin-bottom: 20px;
flex-shrink: 0;
}
.brandAvatar {
width: 80px !important;
height: 80px !important;
margin: 0 auto 20px auto !important;
border: 3px solid var(--glass-border) !important;
box-shadow: 0 8px 32px rgba(45, 127, 249, 0.2) !important;
background: linear-gradient(135deg, var(--color-primary-blue) 0%, var(--color-primary-purple) 100%) !important;
transition: var(--transition-all);
animation: brandPulse 4s infinite ease-in-out;
}
.brandAvatar:hover {
transform: scale(1.05);
box-shadow: 0 12px 40px rgba(45, 127, 249, 0.3) !important;
}
@keyframes brandPulse {
0%, 100% {
box-shadow: 0 8px 32px rgba(45, 127, 249, 0.2);
}
50% {
box-shadow: 0 12px 40px rgba(45, 127, 249, 0.35);
}
}
.welcomeTitle {
font-size: 26px !important;
font-weight: 600 !important;
margin-bottom: 8px !important;
background: linear-gradient(135deg, var(--color-primary-blue) 0%, var(--color-primary-purple) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1.2 !important;
}
.welcomeSubtitle {
color: var(--text-secondary) !important;
font-size: 16px !important;
margin-bottom: 0 !important;
}
/* ==================== 步骤指示器 ==================== */
.stepsSection {
margin: 16px 0 20px 0;
flex-shrink: 0;
}
.steps :global(.ant-steps-item-title) {
color: var(--text-secondary) !important;
font-size: 14px !important;
font-weight: 500;
}
.steps :global(.ant-steps-item-process .ant-steps-item-title) {
color: var(--color-primary-blue) !important;
font-weight: 600;
}
.steps :global(.ant-steps-item-finish .ant-steps-item-title) {
color: var(--color-primary-blue) !important;
}
.steps :global(.ant-steps-item-icon) {
border-color: var(--glass-border) !important;
background: var(--glass-bg) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.steps :global(.ant-steps-item-process .ant-steps-item-icon) {
background: var(--color-primary-blue) !important;
border-color: var(--color-primary-blue) !important;
box-shadow: 0 4px 16px rgba(45, 127, 249, 0.3);
}
.steps :global(.ant-steps-item-finish .ant-steps-item-icon) {
background: var(--color-primary-blue) !important;
border-color: var(--color-primary-blue) !important;
}
/* ==================== 内容区域 ==================== */
.contentSection {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.forgotPasswordForm {
width: 100%;
}
/* ==================== 表单样式 ==================== */
.formItem {
margin-bottom: 14px !important;
}
.formItem:last-of-type {
margin-bottom: 8px !important;
}
.formLabel {
color: var(--text-primary) !important;
font-weight: 500 !important;
font-size: 14px !important;
margin-bottom: 8px !important;
}
.formInput {
height: 48px !important;
border-radius: var(--border-radius-md) !important;
border: 1px solid var(--glass-border) !important;
background: var(--glass-bg) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: var(--transition-medium);
font-size: 15px !important;
padding: 0 16px !important;
}
.formInput:focus,
.formInput:hover {
border-color: var(--color-primary-blue) !important;
box-shadow: 0 0 0 2px rgba(45, 127, 249, 0.1) !important;
background: var(--glass-highlight) !important;
}
.formInput::placeholder {
color: var(--text-tertiary) !important;
}
/* ==================== 邮箱提示 ==================== */
.emailHint {
margin-bottom: 16px;
padding: 12px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius-md);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
text-align: center;
}
.hintText {
color: var(--text-secondary) !important;
font-size: 14px !important;
}
.hintText strong {
color: var(--color-primary-blue) !important;
font-weight: 600;
}
/* ==================== 提交按钮 ==================== */
.submitButton {
height: 48px !important;
border-radius: var(--border-radius-lg) !important;
font-size: 16px !important;
font-weight: 600 !important;
margin-top: 8px !important;
background: linear-gradient(135deg, var(--color-primary-blue) 0%, var(--color-primary-purple) 100%) !important;
border: none !important;
box-shadow: 0 8px 32px rgba(45, 127, 249, 0.3) !important;
transition: var(--transition-all);
position: relative;
overflow: hidden;
}
.submitButton::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.submitButton:hover {
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(45, 127, 249, 0.4) !important;
}
.submitButton:hover::before {
left: 100%;
}
.submitButton:active {
transform: translateY(0);
}
/* ==================== 辅助链接 ==================== */
.helperLinks {
margin: 12px 0;
text-align: center;
}
.helperLink {
color: var(--color-primary-blue) !important;
font-size: 14px !important;
text-decoration: none;
font-weight: 500;
margin-left: 4px;
transition: var(--transition-fast);
position: relative;
cursor: pointer;
}
.helperLink::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: linear-gradient(135deg, var(--color-primary-blue), var(--color-primary-purple));
transition: width 0.3s ease;
}
.helperLink:hover {
color: var(--color-primary-purple) !important;
}
.helperLink:hover::after {
width: 100%;
}
/* ==================== 成功页面 ==================== */
.successSection {
text-align: center;
padding: 20px 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.successIcon {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, var(--color-success) 0%, var(--color-primary-cyan) 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
box-shadow: 0 8px 32px rgba(82, 196, 26, 0.3);
animation: successPulse 2s ease-in-out infinite;
}
.successIcon :global(.anticon) {
font-size: 36px !important;
color: white !important;
}
@keyframes successPulse {
0%, 100% {
box-shadow: 0 8px 32px rgba(82, 196, 26, 0.3);
transform: scale(1);
}
50% {
box-shadow: 0 12px 40px rgba(82, 196, 26, 0.4);
transform: scale(1.02);
}
}
.successTitle {
color: var(--text-primary) !important;
font-size: 22px !important;
font-weight: 600 !important;
margin-bottom: 8px !important;
background: linear-gradient(135deg, var(--color-success) 0%, var(--color-primary-cyan) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.successText {
color: var(--text-secondary) !important;
font-size: 15px !important;
line-height: 1.5;
margin-bottom: 16px !important;
}
/* ==================== 响应式设计 ==================== */
@media (max-width: 768px) {
.forgotPasswordContainer {
padding: 16px 20px;
}
.brandSection {
margin-bottom: 16px;
}
.brandAvatar {
width: 60px !important;
height: 60px !important;
margin-bottom: 12px !important;
}
.welcomeTitle {
font-size: 22px !important;
}
.welcomeSubtitle {
font-size: 14px !important;
}
.stepsSection {
margin: 12px 0 16px 0;
}
.steps :global(.ant-steps-item-title) {
font-size: 12px !important;
}
.formItem {
margin-bottom: 12px !important;
}
.formInput {
height: 42px !important;
font-size: 14px !important;
}
.submitButton {
height: 44px !important;
font-size: 15px !important;
}
.successIcon {
width: 64px;
height: 64px;
}
.successIcon :global(.anticon) {
font-size: 28px !important;
}
.successTitle {
font-size: 20px !important;
}
.successText {
font-size: 14px !important;
}
}
@media (max-width: 480px) {
.forgotPasswordContainer {
padding: 12px 16px;
}
.brandSection {
margin-bottom: 12px;
}
.brandAvatar {
width: 56px !important;
height: 56px !important;
margin-bottom: 8px !important;
}
.welcomeTitle {
font-size: 20px !important;
}
.stepsSection {
margin: 10px 0 12px 0;
}
.contentSection {
margin-top: 8px;
}
.formItem {
margin-bottom: 10px !important;
}
.formInput {
height: 40px !important;
}
.submitButton {
height: 42px !important;
}
.emailHint {
padding: 8px;
margin-bottom: 12px;
}
.hintText {
font-size: 13px !important;
}
.successIcon {
width: 56px;
height: 56px;
}
.successIcon :global(.anticon) {
font-size: 24px !important;
}
.successTitle {
font-size: 18px !important;
}
.successText {
font-size: 13px !important;
}
}
/* ==================== 动画优化 ==================== */
@media (prefers-reduced-motion: reduce) {
.brandAvatar,
.submitButton,
.successIcon {
animation: none !important;
transition: none !important;
}
.brandAvatar:hover,
.submitButton:hover {
transform: none !important;
}
}

View File

@@ -0,0 +1,324 @@
/**
* 文件: src/pages/start/forgot-password.tsx
* 作者: 阿瑞
* 功能: 忘记密码页面 - 毛玻璃风格
* 版本: v1.0.0
* @created 应用毛玻璃风格设计,提供密码重置功能
*/
import { useState } from 'react';
import { useRouter } from 'next/router';
import { Form, Input, Button, Typography, message, Avatar, Steps } from 'antd';
import { MailOutlined, UserOutlined, LockOutlined, CheckCircleOutlined } from '@ant-design/icons';
import StartLayout from './components/StartLayout';
import styles from './forgot-password.module.css';
const { Title, Text } = Typography;
const { Step } = Steps;
interface ForgotPasswordForm {
邮箱: string;
}
interface ResetPasswordForm {
验证码: string;
新密码: string;
确认密码: string;
}
const ForgotPassword = () => {
const [form] = Form.useForm();
const [resetForm] = Form.useForm();
const [loading, setLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [email, setEmail] = useState('');
const router = useRouter();
// 发送重置邮件
const handleSendEmail = async (values: ForgotPasswordForm) => {
setLoading(true);
try {
const response = await fetch('/api/forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '发送重置邮件失败');
}
setEmail(values.);
setCurrentStep(1);
message.success('重置邮件已发送,请查收邮箱 📧');
} catch (error: any) {
message.error(error.message || '发送失败,请稍后重试');
} finally {
setLoading(false);
}
};
// 重置密码
const handleResetPassword = async (values: ResetPasswordForm) => {
if (values. !== values.) {
message.error('两次输入的密码不一致');
return;
}
setLoading(true);
try {
const response = await fetch('/api/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
邮箱: email,
验证码: values.验证码,
新密码: values.新密码,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '重置密码失败');
}
setCurrentStep(2);
message.success('密码重置成功!正在跳转... 🎉');
// 延迟跳转到登录页面
setTimeout(() => {
router.push('/start/login');
}, 2000);
} catch (error: any) {
message.error(error.message || '重置失败,请稍后重试');
} finally {
setLoading(false);
}
};
// 重新发送邮件
const handleResendEmail = async () => {
if (!email) return;
setLoading(true);
try {
const response = await fetch('/api/forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ 邮箱: email }),
});
if (response.ok) {
message.success('重置邮件已重新发送');
} else {
throw new Error('重新发送失败');
}
} catch (error: any) {
message.error(error.message || '重新发送失败');
} finally {
setLoading(false);
}
};
return (
<StartLayout cardStyle={{ background: 'var(--glass-bg)', backdropFilter: 'blur(20px)', border: '1px solid var(--glass-border)' }}>
<div className={styles.forgotPasswordContainer}>
{/* 品牌头部区域 */}
<div className={styles.brandSection}>
<Avatar
size={80}
src="/aoun.png"
icon={<UserOutlined />}
className={styles.brandAvatar}
/>
<Title level={2} className={styles.welcomeTitle}>
</Title>
<Text className={styles.welcomeSubtitle}>
访 🔒
</Text>
</div>
{/* 步骤指示器 */}
<div className={styles.stepsSection}>
<Steps current={currentStep} className={styles.steps}>
<Step title="输入邮箱" icon={<MailOutlined />} />
<Step title="验证重置" icon={<LockOutlined />} />
<Step title="重置成功" icon={<CheckCircleOutlined />} />
</Steps>
</div>
{/* 步骤内容 */}
<div className={styles.contentSection}>
{/* 步骤1输入邮箱 */}
{currentStep === 0 && (
<Form
form={form}
layout="vertical"
onFinish={handleSendEmail}
className={styles.forgotPasswordForm}
requiredMark={false}
>
<Form.Item
name="邮箱"
label={<span className={styles.formLabel}></span>}
rules={[
{ required: true, message: '请输入邮箱地址' },
{ type: 'email', message: '请输入有效的邮箱地址' }
]}
className={styles.formItem}
>
<Input
prefix={<MailOutlined style={{ color: 'var(--text-tertiary)' }} />}
placeholder="请输入注册时的邮箱地址"
className={styles.formInput}
/>
</Form.Item>
<Form.Item className={styles.formItem}>
<Button
type="primary"
htmlType="submit"
block
loading={loading}
className={styles.submitButton}
>
{loading ? '发送中...' : '发送重置邮件'}
</Button>
</Form.Item>
<div className={styles.helperLinks}>
<Text>
<a href="/start/login" className={styles.helperLink}>
</a>
</Text>
</div>
</Form>
)}
{/* 步骤2验证重置 */}
{currentStep === 1 && (
<Form
form={resetForm}
layout="vertical"
onFinish={handleResetPassword}
className={styles.forgotPasswordForm}
requiredMark={false}
>
<div className={styles.emailHint}>
<Text className={styles.hintText}>
<strong>{email}</strong>
</Text>
</div>
<Form.Item
name="验证码"
label={<span className={styles.formLabel}></span>}
rules={[
{ required: true, message: '请输入验证码' },
{ len: 6, message: '验证码为6位数字' }
]}
className={styles.formItem}
>
<Input
placeholder="请输入6位验证码"
className={styles.formInput}
maxLength={6}
/>
</Form.Item>
<Form.Item
name="新密码"
label={<span className={styles.formLabel}></span>}
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码至少6位字符' }
]}
className={styles.formItem}
>
<Input.Password
prefix={<LockOutlined style={{ color: 'var(--text-tertiary)' }} />}
placeholder="请输入新密码"
className={styles.formInput}
/>
</Form.Item>
<Form.Item
name="确认密码"
label={<span className={styles.formLabel}></span>}
rules={[
{ required: true, message: '请确认新密码' },
{ min: 6, message: '密码至少6位字符' }
]}
className={styles.formItem}
>
<Input.Password
prefix={<LockOutlined style={{ color: 'var(--text-tertiary)' }} />}
placeholder="请再次输入新密码"
className={styles.formInput}
/>
</Form.Item>
<Form.Item className={styles.formItem}>
<Button
type="primary"
htmlType="submit"
block
loading={loading}
className={styles.submitButton}
>
{loading ? '重置中...' : '重置密码'}
</Button>
</Form.Item>
<div className={styles.helperLinks}>
<Text>
<a onClick={handleResendEmail} className={styles.helperLink}>
</a>
</Text>
</div>
</Form>
)}
{/* 步骤3重置成功 */}
{currentStep === 2 && (
<div className={styles.successSection}>
<div className={styles.successIcon}>
<CheckCircleOutlined />
</div>
<Title level={3} className={styles.successTitle}>
</Title>
<Text className={styles.successText}>
...
</Text>
<Button
type="primary"
className={styles.submitButton}
onClick={() => router.push('/start/login')}
>
</Button>
</div>
)}
</div>
</div>
</StartLayout>
);
};
export default ForgotPassword;

View File

@@ -0,0 +1,470 @@
/**
* 登录页面样式模块
* @author 阿瑞
* @description 毛玻璃风格登录页面样式
* @version 1.0.0
* @created 2024-12-19
*/
/* ==================== 登录页面容器 ==================== */
.loginContainer {
padding: 24px 32px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
box-sizing: border-box;
}
/* ==================== 品牌头部区域 ==================== */
.brandSection {
text-align: center;
margin-bottom: 24px;
flex-shrink: 0;
}
.brandAvatar {
width: 80px !important;
height: 80px !important;
margin: 0 auto 24px auto !important;
border: 3px solid var(--glass-border) !important;
box-shadow: 0 8px 32px rgba(45, 127, 249, 0.2) !important;
background: linear-gradient(135deg, var(--color-primary-blue) 0%, var(--color-primary-purple) 100%) !important;
transition: var(--transition-all);
animation: brandPulse 4s infinite ease-in-out;
}
.brandAvatar:hover {
transform: scale(1.05);
box-shadow: 0 12px 40px rgba(45, 127, 249, 0.3) !important;
}
@keyframes brandPulse {
0%, 100% {
box-shadow: 0 8px 32px rgba(45, 127, 249, 0.2);
}
50% {
box-shadow: 0 12px 40px rgba(45, 127, 249, 0.35);
}
}
.welcomeTitle {
font-size: 28px !important;
font-weight: 600 !important;
margin-bottom: 8px !important;
background: linear-gradient(135deg, var(--color-primary-blue) 0%, var(--color-primary-purple) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1.2 !important;
}
.welcomeSubtitle {
color: var(--text-secondary) !important;
font-size: 16px !important;
margin-bottom: 0 !important;
}
/* ==================== 表单样式 ==================== */
.loginForm {
margin-top: 8px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
/* 表单主要内容区域 */
.formMainContent {
flex: 1;
}
.formItem {
margin-bottom: 14px !important;
}
.formItem:last-of-type {
margin-bottom: 8px !important;
}
.formLabel {
color: var(--text-primary) !important;
font-weight: 500 !important;
font-size: 14px !important;
margin-bottom: 8px !important;
}
.formInput {
height: 48px !important;
border-radius: var(--border-radius-md) !important;
border: 1px solid var(--glass-border) !important;
background: var(--glass-bg) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: var(--transition-medium);
font-size: 15px !important;
padding: 0 16px !important;
}
.formInput:focus,
.formInput:hover {
border-color: var(--color-primary-blue) !important;
box-shadow: 0 0 0 2px rgba(45, 127, 249, 0.1) !important;
background: var(--glass-highlight) !important;
}
.formInput::placeholder {
color: var(--text-tertiary) !important;
}
/* ==================== 协议复选框 ==================== */
.agreementSection {
margin: 12px 0;
padding: 10px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius-md);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.agreementCheckbox {
font-size: 14px !important;
color: var(--text-secondary) !important;
}
.agreementCheckbox :global(.ant-checkbox-wrapper) {
align-items: flex-start;
line-height: 1.4;
}
.agreementCheckbox :global(.ant-checkbox) {
margin-top: 2px;
}
.agreementCheckbox :global(.ant-checkbox-checked .ant-checkbox-inner) {
background-color: var(--color-primary-blue) !important;
border-color: var(--color-primary-blue) !important;
}
.protocolLink {
color: var(--color-primary-blue) !important;
text-decoration: none;
font-weight: 500;
transition: var(--transition-fast);
}
.protocolLink:hover {
color: var(--color-primary-purple) !important;
text-decoration: underline;
}
/* ==================== 登录按钮 ==================== */
.loginButton {
height: 48px !important;
border-radius: var(--border-radius-lg) !important;
font-size: 16px !important;
font-weight: 600 !important;
margin-top: 4px !important;
background: linear-gradient(135deg, var(--color-primary-blue) 0%, var(--color-primary-purple) 100%) !important;
border: none !important;
box-shadow: 0 8px 32px rgba(45, 127, 249, 0.3) !important;
transition: var(--transition-all);
position: relative;
overflow: hidden;
}
.loginButton::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.loginButton:hover {
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(45, 127, 249, 0.4) !important;
}
.loginButton:hover::before {
left: 100%;
}
.loginButton:active {
transform: translateY(0);
}
/* ==================== 辅助链接 ==================== */
.helperLinks {
margin: 12px 0;
}
.helperLink {
color: var(--text-secondary) !important;
font-size: 14px !important;
text-decoration: none;
transition: var(--transition-fast);
position: relative;
}
.helperLink::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: linear-gradient(135deg, var(--color-primary-blue), var(--color-primary-purple));
transition: width 0.3s ease;
}
.helperLink:hover {
color: var(--color-primary-blue) !important;
}
.helperLink:hover::after {
width: 100%;
}
/* ==================== 分割线 ==================== */
.dividerSection {
margin: 16px 0 12px 0;
}
.divider {
color: var(--text-tertiary) !important;
font-size: 14px !important;
border-color: var(--glass-border) !important;
}
/* ==================== 第三方登录 ==================== */
.socialLoginSection {
display: flex;
justify-content: space-around;
align-items: center;
gap: 16px;
flex-shrink: 0;
padding-bottom: 4px;
}
.socialLoginItem {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
flex: 1;
transition: var(--transition-all);
}
.socialLoginButton {
width: 56px !important;
height: 56px !important;
border-radius: 50% !important;
background: var(--glass-bg) !important;
border: 1px solid var(--glass-border) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: var(--transition-all);
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.socialLoginButton:hover {
background: var(--glass-highlight) !important;
border-color: var(--color-primary-blue) !important;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(45, 127, 249, 0.2) !important;
}
.socialLoginIcon {
font-size: 22px !important;
color: var(--text-secondary) !important;
transition: var(--transition-fast);
}
.socialLoginButton:hover .socialLoginIcon {
color: var(--color-primary-blue) !important;
}
.socialLoginText {
font-size: 12px !important;
color: var(--text-tertiary) !important;
font-weight: 500;
transition: var(--transition-fast);
}
.socialLoginItem:hover .socialLoginText {
color: var(--text-secondary) !important;
}
/* 特定社交平台的图标颜色 */
.wechatIcon {
color: #07c160 !important;
}
.googleIcon {
color: #db4437 !important;
}
.phoneIcon {
color: var(--color-primary-cyan) !important;
}
/* ==================== 响应式设计 ==================== */
@media (max-width: 768px) {
.loginContainer {
padding: 16px 20px;
}
.brandSection {
margin-bottom: 16px;
}
.brandAvatar {
width: 60px !important;
height: 60px !important;
margin-bottom: 12px !important;
}
.welcomeTitle {
font-size: 22px !important;
}
.welcomeSubtitle {
font-size: 14px !important;
}
.loginForm {
margin-top: 4px;
}
.formItem {
margin-bottom: 12px !important;
}
.formInput {
height: 42px !important;
font-size: 14px !important;
}
.loginButton {
height: 44px !important;
font-size: 15px !important;
}
.agreementSection {
margin: 10px 0;
padding: 8px;
}
.helperLinks {
margin: 10px 0;
}
.dividerSection {
margin: 12px 0 8px 0;
}
.socialLoginSection {
gap: 14px;
padding-bottom: 2px;
}
.socialLoginButton {
width: 44px !important;
height: 44px !important;
}
.socialLoginIcon {
font-size: 16px !important;
}
.socialLoginText {
font-size: 11px !important;
}
}
@media (max-width: 480px) {
.loginContainer {
padding: 12px 16px;
}
.brandSection {
margin-bottom: 12px;
}
.brandAvatar {
width: 56px !important;
height: 56px !important;
margin-bottom: 8px !important;
}
.welcomeTitle {
font-size: 20px !important;
}
.formItem {
margin-bottom: 10px !important;
}
.formInput {
height: 40px !important;
}
.loginButton {
height: 42px !important;
}
.agreementSection {
margin: 8px 0;
padding: 6px;
}
.helperLinks {
margin: 8px 0;
}
.dividerSection {
margin: 10px 0 6px 0;
}
.socialLoginSection {
gap: 12px;
}
.socialLoginButton {
width: 40px !important;
height: 40px !important;
}
.socialLoginIcon {
font-size: 14px !important;
}
.socialLoginText {
font-size: 10px !important;
}
}
/* ==================== 动画优化 ==================== */
@media (prefers-reduced-motion: reduce) {
.brandAvatar,
.loginButton,
.socialLoginButton,
.socialLoginItem {
animation: none !important;
transition: none !important;
}
.brandAvatar:hover,
.loginButton:hover,
.socialLoginButton:hover {
transform: none !important;
}
}

240
src/pages/start/login.tsx Normal file
View File

@@ -0,0 +1,240 @@
/**
* 文件: src/pages/start/login.tsx
* 作者: 阿瑞
* 功能: 用户登录页面 - 毛玻璃风格
* 版本: v2.0.0
* @updated 应用毛玻璃风格设计优化UI/UX体验
*/
import { useState } from 'react';
import { useRouter } from 'next/router';
import { useUserActions } from '@/store/userStore';
import { Form, Input, Button, Typography, message, Divider, Avatar, Col, Row, Checkbox } from 'antd';
import { GoogleOutlined, MobileOutlined, WechatOutlined, UserOutlined, LockOutlined } from '@ant-design/icons';
import StartLayout from './components/StartLayout';
import styles from './login.module.css';
//import ProtocolModal from '@/components/ProtocolModal';
const { Title, Text } = Typography;
interface LoginForm {
identifier: string; // 可以是邮箱或者手机号
密码: string; // 保持原有的中文字段名与API一致
}
const Login = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const router = useRouter();
const { setUserToken, setUserInfo } = useUserActions();
const [checked, setChecked] = useState(false); // 用户是否同意协议
// 登录提交处理 - 使用原生fetch替代axios
const handleSubmit = async (values: LoginForm) => {
if (!checked) {
message.error('请阅读并同意用户协议和隐私政策');
return;
}
setLoading(true);
try {
// 使用原生fetch进行POST请求
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
});
// 检查响应状态
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '登录失败,请检查账号和密码');
}
const data = await response.json();
const { token, refreshToken, userInfo } = data;
// 设置用户信息和令牌
setUserToken(token, refreshToken);
setUserInfo(userInfo);
message.success('登录成功!欢迎回来 🎉');
// 根据用户角色跳转到对应主页
const updatedHomePath = userInfo.?. || '/dashboard';
router.push(updatedHomePath);
} catch (error: any) {
message.error(error.message || '登录失败,请稍后重试');
} finally {
setLoading(false);
}
};
// 处理 Checkbox 的勾选状态
const handleCheckboxChange = (e: any) => {
setChecked(e.target.checked);
};
// 处理第三方登录
const handleSocialLogin = (type: 'wechat' | 'phone' | 'google') => {
message.info(`${type === 'wechat' ? '微信' : type === 'phone' ? '手机号' : 'Google'}登录功能开发中...`);
};
return (
<StartLayout cardStyle={{ background: 'var(--glass-bg)', backdropFilter: 'blur(20px)', border: '1px solid var(--glass-border)' }}>
<div className={styles.loginContainer}>
{/* 品牌头部区域 */}
<div className={styles.brandSection}>
<Avatar
size={80}
src="/aoun.png"
icon={<UserOutlined />}
className={styles.brandAvatar}
/>
<Title level={2} className={styles.welcomeTitle}>
</Title>
<Text className={styles.welcomeSubtitle}>
</Text>
</div>
{/* 登录表单 */}
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
className={styles.loginForm}
requiredMark={false}
>
<div className={styles.formMainContent}>
<Form.Item
name="identifier"
label={<span className={styles.formLabel}></span>}
rules={[
{ required: true, message: '请输入邮箱或手机号' },
{
pattern: /^(\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*|1[3-9]\d{9})$/,
message: '请输入有效的邮箱或手机号'
}
]}
className={styles.formItem}
>
<Input
prefix={<UserOutlined style={{ color: 'var(--text-tertiary)' }} />}
placeholder="请输入邮箱或手机号"
className={styles.formInput}
/>
</Form.Item>
<Form.Item
name="密码"
label={<span className={styles.formLabel}></span>}
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码至少6位字符' }
]}
className={styles.formItem}
>
<Input.Password
prefix={<LockOutlined style={{ color: 'var(--text-tertiary)' }} />}
placeholder="请输入密码"
className={styles.formInput}
/>
</Form.Item>
{/* 协议复选框 */}
<div className={styles.agreementSection}>
<Checkbox
onChange={handleCheckboxChange}
className={styles.agreementCheckbox}
>
<a href="#" className={styles.protocolLink}></a>
<a href="#" className={styles.protocolLink}></a>
</Checkbox>
</div>
{/* 登录按钮 */}
<Form.Item className={styles.formItem}>
<Button
type="primary"
htmlType="submit"
block
loading={loading}
className={styles.loginButton}
disabled={!checked}
>
{loading ? '登录中...' : '登录'}
</Button>
</Form.Item>
{/* 辅助链接 */}
<Row justify="space-between" className={styles.helperLinks}>
<Col>
<a href="/start/forgot-password" className={styles.helperLink}>
</a>
</Col>
<Col>
<a href="/start/register" className={styles.helperLink}>
</a>
</Col>
</Row>
</div>
{/* 分割线和第三方登录放在表单底部 */}
<div>
{/* 分割线 */}
<div className={styles.dividerSection}>
<Divider className={styles.divider} plain>
</Divider>
</div>
{/* 第三方登录 */}
<div className={styles.socialLoginSection}>
<div className={styles.socialLoginItem}>
<Button
shape="circle"
className={styles.socialLoginButton}
onClick={() => handleSocialLogin('wechat')}
>
<WechatOutlined className={`${styles.socialLoginIcon} ${styles.wechatIcon}`} />
</Button>
<Text className={styles.socialLoginText}></Text>
</div>
<div className={styles.socialLoginItem}>
<Button
shape="circle"
className={styles.socialLoginButton}
onClick={() => handleSocialLogin('phone')}
>
<MobileOutlined className={`${styles.socialLoginIcon} ${styles.phoneIcon}`} />
</Button>
<Text className={styles.socialLoginText}></Text>
</div>
<div className={styles.socialLoginItem}>
<Button
shape="circle"
className={styles.socialLoginButton}
onClick={() => handleSocialLogin('google')}
>
<GoogleOutlined className={`${styles.socialLoginIcon} ${styles.googleIcon}`} />
</Button>
<Text className={styles.socialLoginText}>Google</Text>
</div>
</div>
</div>
</Form>
</div>
</StartLayout>
);
};
export default Login;

View File

@@ -0,0 +1,346 @@
/**
* 注册页面样式模块
* @author 阿瑞
* @description 毛玻璃风格注册页面样式
* @version 1.0.0
* @created 2024-12-19
*/
/* ==================== 注册页面容器 ==================== */
.registerContainer {
padding: 24px 32px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
/* 关键代码行注释:确保内容合理分布,不超出容器高度 */
overflow: hidden;
box-sizing: border-box;
}
/* ==================== 品牌头部区域 ==================== */
.brandSection {
text-align: center;
margin-bottom: 24px;
flex-shrink: 0;
}
.brandAvatar {
width: 80px !important;
height: 80px !important;
margin: 0 auto 20px auto !important;
border: 3px solid var(--glass-border) !important;
box-shadow: 0 8px 32px rgba(45, 127, 249, 0.2) !important;
background: linear-gradient(135deg, var(--color-primary-blue) 0%, var(--color-primary-purple) 100%) !important;
transition: var(--transition-all);
animation: brandPulse 4s infinite ease-in-out;
}
.brandAvatar:hover {
transform: scale(1.05);
box-shadow: 0 12px 40px rgba(45, 127, 249, 0.3) !important;
}
@keyframes brandPulse {
0%, 100% {
box-shadow: 0 8px 32px rgba(45, 127, 249, 0.2);
}
50% {
box-shadow: 0 12px 40px rgba(45, 127, 249, 0.35);
}
}
.welcomeTitle {
font-size: 26px !important;
font-weight: 600 !important;
margin-bottom: 8px !important;
background: linear-gradient(135deg, var(--color-primary-blue) 0%, var(--color-primary-purple) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1.2 !important;
}
.welcomeSubtitle {
color: var(--text-secondary) !important;
font-size: 16px !important;
margin-bottom: 0 !important;
}
/* ==================== 表单样式 ==================== */
.registerForm {
margin-top: 8px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
/* 表单主要内容区域 */
.formMainContent {
flex: 1;
}
.formItem {
margin-bottom: 14px !important;
}
.formItem:last-of-type {
margin-bottom: 8px !important;
}
.formLabel {
color: var(--text-primary) !important;
font-weight: 500 !important;
font-size: 14px !important;
margin-bottom: 8px !important;
}
.formInput {
height: 48px !important;
border-radius: var(--border-radius-md) !important;
border: 1px solid var(--glass-border) !important;
background: var(--glass-bg) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: var(--transition-medium);
font-size: 15px !important;
padding: 0 16px !important;
}
.formInput:focus,
.formInput:hover {
border-color: var(--color-primary-blue) !important;
box-shadow: 0 0 0 2px rgba(45, 127, 249, 0.1) !important;
background: var(--glass-highlight) !important;
}
.formInput::placeholder {
color: var(--text-tertiary) !important;
}
/* ==================== 协议复选框 ==================== */
.agreementSection {
margin: 12px 0;
padding: 10px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius-md);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.agreementCheckbox {
font-size: 14px !important;
color: var(--text-secondary) !important;
}
.agreementCheckbox :global(.ant-checkbox-wrapper) {
align-items: flex-start;
line-height: 1.4;
}
.agreementCheckbox :global(.ant-checkbox) {
margin-top: 2px;
}
.agreementCheckbox :global(.ant-checkbox-checked .ant-checkbox-inner) {
background-color: var(--color-primary-blue) !important;
border-color: var(--color-primary-blue) !important;
}
.protocolLink {
color: var(--color-primary-blue) !important;
text-decoration: none;
font-weight: 500;
transition: var(--transition-fast);
}
.protocolLink:hover {
color: var(--color-primary-purple) !important;
text-decoration: underline;
}
/* ==================== 注册按钮 ==================== */
.registerButton {
height: 48px !important;
border-radius: var(--border-radius-lg) !important;
font-size: 16px !important;
font-weight: 600 !important;
margin-top: 4px !important;
background: linear-gradient(135deg, var(--color-primary-blue) 0%, var(--color-primary-purple) 100%) !important;
border: none !important;
box-shadow: 0 8px 32px rgba(45, 127, 249, 0.3) !important;
transition: var(--transition-all);
position: relative;
overflow: hidden;
}
.registerButton::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.registerButton:hover {
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(45, 127, 249, 0.4) !important;
}
.registerButton:hover::before {
left: 100%;
}
.registerButton:active {
transform: translateY(0);
}
/* ==================== 底部区域 ==================== */
.bottomSection {
flex-shrink: 0;
padding-top: 8px;
}
.helperLinks {
margin: 12px 0;
text-align: center;
}
.helperLink {
color: var(--text-secondary) !important;
font-size: 14px !important;
text-decoration: none;
transition: var(--transition-fast);
position: relative;
}
.helperLink::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: linear-gradient(135deg, var(--color-primary-blue), var(--color-primary-purple));
transition: width 0.3s ease;
}
.helperLink:hover {
color: var(--color-primary-blue) !important;
}
.helperLink:hover::after {
width: 100%;
}
/* ==================== 响应式设计 ==================== */
@media (max-width: 768px) {
.registerContainer {
padding: 16px 20px;
}
.brandSection {
margin-bottom: 16px;
}
.brandAvatar {
width: 60px !important;
height: 60px !important;
margin-bottom: 12px !important;
}
.welcomeTitle {
font-size: 22px !important;
}
.welcomeSubtitle {
font-size: 14px !important;
}
.registerForm {
margin-top: 4px;
}
.formItem {
margin-bottom: 12px !important;
}
.formInput {
height: 42px !important;
font-size: 14px !important;
}
.registerButton {
height: 44px !important;
font-size: 15px !important;
}
.agreementSection {
margin: 10px 0;
padding: 8px;
}
.helperLinks {
margin: 10px 0;
}
}
@media (max-width: 480px) {
.registerContainer {
padding: 12px 16px;
}
.brandSection {
margin-bottom: 12px;
}
.brandAvatar {
width: 56px !important;
height: 56px !important;
margin-bottom: 8px !important;
}
.welcomeTitle {
font-size: 20px !important;
}
.formItem {
margin-bottom: 10px !important;
}
.formInput {
height: 40px !important;
}
.registerButton {
height: 42px !important;
}
.agreementSection {
margin: 8px 0;
padding: 6px;
}
.helperLinks {
margin: 8px 0;
}
}
/* ==================== 动画优化 ==================== */
@media (prefers-reduced-motion: reduce) {
.brandAvatar,
.registerButton {
animation: none !important;
transition: none !important;
}
.brandAvatar:hover,
.registerButton:hover {
transform: none !important;
}
}

View File

@@ -0,0 +1,223 @@
/**
* 文件: src/pages/start/register.tsx
* 作者: 阿瑞
* 功能: 用户注册页面 - 毛玻璃风格
* 版本: v2.0.0
* @updated 应用毛玻璃风格设计优化UI/UX体验
*/
import { useState } from 'react';
import { useRouter } from 'next/router';
import { Form, Input, Button, Typography, message, Avatar, Checkbox } from 'antd';
import { UserOutlined, LockOutlined, MailOutlined, PhoneOutlined } from '@ant-design/icons';
import StartLayout from './components/StartLayout';
import styles from './register.module.css';
const { Title, Text } = Typography;
interface RegisterForm {
姓名: string;
电话: string;
邮箱: string;
密码: string;
}
const Register = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [checked, setChecked] = useState(false); // 用户是否同意协议
const router = useRouter();
// 注册提交处理 - 使用原生fetch替代axios
const handleSubmit = async (values: RegisterForm) => {
if (!checked) {
message.error('请阅读并同意用户协议和隐私政策');
return;
}
setLoading(true);
try {
// 使用原生fetch进行POST请求
const response = await fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
});
// 检查响应状态
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '注册失败,请稍后重试');
}
message.success('注册成功!正在跳转... 🎉');
// 注册成功后跳转到登录页面或团队创建页面
setTimeout(() => {
router.push('/start/login');
}, 1500);
} catch (error: any) {
if (error.message === '邮箱已被注册') {
form.setFields([
{
name: '邮箱',
errors: ['该邮箱已被注册,请使用其他邮箱'],
},
]);
} else if (error.message === '电话已被注册') {
form.setFields([
{
name: '电话',
errors: ['该电话已被注册,请使用其他电话'],
},
]);
} else {
message.error(error.message || '注册失败,请稍后重试');
}
} finally {
setLoading(false);
}
};
// 处理 Checkbox 的勾选状态
const handleCheckboxChange = (e: any) => {
setChecked(e.target.checked);
};
return (
<StartLayout cardStyle={{ background: 'var(--glass-bg)', backdropFilter: 'blur(20px)', border: '1px solid var(--glass-border)' }}>
<div className={styles.registerContainer}>
{/* 品牌头部区域 */}
<div className={styles.brandSection}>
<Avatar
size={80}
src="/aoun.png"
icon={<UserOutlined />}
className={styles.brandAvatar}
/>
<Title level={2} className={styles.welcomeTitle}>
Aoun Admin
</Title>
<Text className={styles.welcomeSubtitle}>
</Text>
</div>
{/* 注册表单 */}
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
className={styles.registerForm}
requiredMark={false}
>
<div className={styles.formMainContent}>
<Form.Item
name="姓名"
label={<span className={styles.formLabel}></span>}
rules={[{ required: true, message: '请输入您的姓名' }]}
className={styles.formItem}
>
<Input
prefix={<UserOutlined style={{ color: 'var(--text-tertiary)' }} />}
placeholder="请输入您的姓名"
className={styles.formInput}
/>
</Form.Item>
<Form.Item
name="电话"
label={<span className={styles.formLabel}></span>}
rules={[
{ required: true, message: '请输入您的手机号' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号' }
]}
className={styles.formItem}
>
<Input
prefix={<PhoneOutlined style={{ color: 'var(--text-tertiary)' }} />}
placeholder="请输入手机号"
className={styles.formInput}
/>
</Form.Item>
<Form.Item
name="邮箱"
label={<span className={styles.formLabel}></span>}
rules={[
{ required: true, message: '请输入邮箱地址' },
{ type: 'email', message: '请输入有效的邮箱地址' }
]}
className={styles.formItem}
>
<Input
prefix={<MailOutlined style={{ color: 'var(--text-tertiary)' }} />}
placeholder="请输入邮箱地址"
className={styles.formInput}
/>
</Form.Item>
<Form.Item
name="密码"
label={<span className={styles.formLabel}></span>}
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码至少6位字符' }
]}
className={styles.formItem}
>
<Input.Password
prefix={<LockOutlined style={{ color: 'var(--text-tertiary)' }} />}
placeholder="请输入密码"
className={styles.formInput}
/>
</Form.Item>
{/* 协议复选框 */}
<div className={styles.agreementSection}>
<Checkbox
onChange={handleCheckboxChange}
className={styles.agreementCheckbox}
>
<a href="#" className={styles.protocolLink}></a>
<a href="#" className={styles.protocolLink}></a>
</Checkbox>
</div>
{/* 注册按钮 */}
<Form.Item className={styles.formItem}>
<Button
type="primary"
htmlType="submit"
block
loading={loading}
className={styles.registerButton}
disabled={!checked}
>
{loading ? '注册中...' : '注册'}
</Button>
</Form.Item>
</div>
{/* 底部区域 */}
<div className={styles.bottomSection}>
<div className={styles.helperLinks}>
<Text>
<a href="/start/login" className={styles.helperLink}>
</a>
</Text>
</div>
</div>
</Form>
</div>
</StartLayout>
);
};
export default Register;

26
src/pages/test/1.tsx Normal file
View File

@@ -0,0 +1,26 @@
/**
* 测试页面
* @author 阿瑞
* @description 简单的测试页面,用于验证组件和样式
* @version 1.0.0
* @created 2024-12-19
*/
import React from 'react';
// 模块级注释:测试页面主组件
export default function TestPage(): React.ReactElement {
return (
<div className="min-h-screen flex items-center justify-center p-8">
<div className="text-center">
<h1 className="text-5xl md:text-6xl font-bold mb-6 leading-tight">
<span className="inline-block bg-gradient-to-r from-[var(--color-primary-cyan)] via-[var(--color-primary-blue)] to-[var(--color-primary-purple)] animate-gradient bg-clip-text text-transparent">
</span>
<br />
<span className={'text-[var(--text-primary)]'}>SaaS管理平台</span>
</h1>
</div>
</div>
);
}

View File

@@ -0,0 +1,129 @@
/**
* 字体测试页面
* @author 阿瑞
* @description 用于测试Geist字体是否正常加载和显示的页面
* @version 1.1.0
* @created 2024-12-19
* @updated 修复SSR错误安全获取字体信息
*/
import React, { useState, useEffect } from 'react';
import { Typography, Card, Button, Input, Select } from 'antd';
const { Title, Paragraph, Text } = Typography;
// 模块级注释:字体测试组件
const FontTestPage: React.FC = () => {
// 关键代码行注释:状态管理,用于安全获取字体信息
const [fontFamily, setFontFamily] = useState<string>('加载中...');
const [isMounted, setIsMounted] = useState<boolean>(false);
// 关键代码行注释:客户端挂载后获取字体信息
useEffect(() => {
setIsMounted(true);
if (typeof window !== 'undefined') {
const bodyFontFamily = getComputedStyle(document.body).fontFamily;
setFontFamily(bodyFontFamily);
}
}, []);
return (
<div style={{ padding: '24px', maxWidth: '800px', margin: '0 auto' }}>
<Card className="glass-card">
<Title level={1}>Geist SaasS</Title>
<Paragraph>
Geist Geist
</Paragraph>
{/* 关键代码行注释:不同字重的字体测试 */}
<Title level={2}></Title>
<div style={{ marginBottom: '16px' }}>
<Text style={{ fontWeight: 300, fontSize: '16px', display: 'block', marginBottom: '8px' }}>
Geist Light (300):
</Text>
<Text style={{ fontWeight: 400, fontSize: '16px', display: 'block', marginBottom: '8px' }}>
Geist Regular (400):
</Text>
<Text style={{ fontWeight: 500, fontSize: '16px', display: 'block', marginBottom: '8px' }}>
Geist Medium (500):
</Text>
<Text style={{ fontWeight: 600, fontSize: '16px', display: 'block', marginBottom: '8px' }}>
Geist SemiBold (600):
</Text>
<Text style={{ fontWeight: 700, fontSize: '16px', display: 'block', marginBottom: '8px' }}>
Geist Bold (700):
</Text>
<Text style={{ fontWeight: 800, fontSize: '16px', display: 'block', marginBottom: '8px' }}>
Geist ExtraBold (800):
</Text>
</div>
{/* 关键代码行注释:组件字体测试 */}
<Title level={2}></Title>
<div style={{ marginBottom: '16px' }}>
<Button type="primary" style={{ marginRight: '8px', marginBottom: '8px' }}>
Geist
</Button>
<Input
placeholder="Geist 输入框"
style={{ marginBottom: '8px', maxWidth: '200px' }}
/>
<Select
placeholder="Geist 选择框"
style={{ width: '200px', marginBottom: '8px' }}
options={[
{ value: '1', label: 'Geist 选项 1' },
{ value: '2', label: 'Geist 选项 2' },
]}
/>
</div>
{/* 关键代码行注释:英文字体测试 */}
<Title level={2}>English Font Test</Title>
<Paragraph>
This is a paragraph to test the Geist font with English text.
Geist is a modern, clean typeface designed for digital interfaces.
It should look crisp and readable across different sizes and weights.
</Paragraph>
{/* 关键代码行注释:字体信息显示 */}
<Title level={2}></Title>
<Paragraph>
<Text code>font-family: {fontFamily}</Text>
</Paragraph>
<div style={{
padding: '16px',
background: 'rgba(0,0,0,0.05)',
borderRadius: '8px',
fontFamily: 'monospace'
}}>
<strong></strong><br />
1. (F12)<br />
2. <br />
3. computed styles font-family<br />
4. "Geist"
</div>
{/* 关键代码行注释:实时字体检测结果 */}
{isMounted && (
<div style={{
marginTop: '16px',
padding: '16px',
background: fontFamily.includes('Geist') ? 'rgba(0, 255, 0, 0.1)' : 'rgba(255, 165, 0, 0.1)',
borderRadius: '8px',
border: `2px solid ${fontFamily.includes('Geist') ? '#06d7b2' : '#ff9640'}`
}}>
<strong style={{ color: fontFamily.includes('Geist') ? '#06d7b2' : '#ff9640' }}>
{fontFamily.includes('Geist') ? '✅ Geist 字体已成功加载!' : '⚠️ Geist 字体未加载正在使用fallback字体'}
</strong>
</div>
)}
</Card>
</div>
);
};
export default FontTestPage;

1345
src/pages/test/ui/index.tsx Normal file

File diff suppressed because it is too large Load Diff

208
src/store/settingStore.ts Normal file
View File

@@ -0,0 +1,208 @@
/**
* 设置状态管理器
* @author 阿瑞
* @description 应用全局设置的状态管理,包括主题、布局等配置的存储和管理
* @version 1.1.0
* @date 2024
* @updated 修复SSR初始化问题
*/
import { create } from 'zustand';
import { StorageEnum, ThemeColorPresets, ThemeLayout, ThemeMode } from '@/types/enum';
// ==================== 本地存储工具函数 ====================
/**
* 从 localStorage 中获取指定键的值
* @template T - 返回值的类型
* @param key - 存储键名,使用 StorageEnum 枚举值
* @returns 解析后的值或 null
*/
export const getItem = <T>(key: StorageEnum): T | null => {
let value = null;
try {
// 关键代码行注释检查是否在客户端环境SSR兼容性处理
if (typeof window !== 'undefined') {
// 尝试从 localStorage 获取数据
const result = window.localStorage.getItem(key);
if (result) {
// 解析 JSON 字符串为对象
value = JSON.parse(result);
}
}
} catch (error) {
// 捕获解析错误,避免程序崩溃
console.error('localStorage 读取失败:', error);
}
return value;
};
// ==================== 类型定义 ====================
/**
* 应用设置类型定义
* @interface SettingsType
*/
type SettingsType = {
/** 主题色彩预设 */
themeColorPresets: ThemeColorPresets;
/** 主题模式(明亮/暗黑) */
themeMode: ThemeMode;
/** 主题布局类型 */
themeLayout: ThemeLayout;
/** 是否拉伸布局 */
themeStretch: boolean;
/** 是否显示面包屑导航 */
breadCrumb: boolean;
/** 是否启用多标签页 */
multiTab: boolean;
};
/**
* 设置状态管理器类型定义
* @interface SettingStore
*/
type SettingStore = {
/** 当前设置状态 */
settings: SettingsType;
/** 操作方法集合,使用 actions 命名空间避免状态污染 */
actions: {
/** 更新设置并同步到本地存储 */
setSettings: (settings: SettingsType) => void;
/** 清除本地存储的设置 */
clearSettings: () => void;
/** 切换主题模式 */
toggleThemeMode: () => void;
};
};
// ==================== 本地存储操作函数 ====================
/**
* 将数据存储到 localStorage
* @template T - 存储值的类型
* @param key - 存储键名
* @param value - 要存储的值
*/
export const setItem = <T>(key: StorageEnum, value: T): void => {
// 关键代码行注释检查客户端环境再进行localStorage操作
if (typeof window !== 'undefined') {
// 将对象序列化为 JSON 字符串存储
localStorage.setItem(key, JSON.stringify(value));
}
};
/**
* 从 localStorage 中移除指定键的数据
* @param key - 要移除的键名
*/
export const removeItem = (key: StorageEnum): void => {
// 关键代码行注释SSR兼容性检查
if (typeof window !== 'undefined') {
localStorage.removeItem(key);
}
};
/**
* 清空所有 localStorage 数据
*/
export const clearItems = () => {
// 关键代码行注释SSR兼容性检查
if (typeof window !== 'undefined') {
localStorage.clear();
}
};
// ==================== 默认设置配置 ====================
/**
* 默认设置配置
* 确保服务端和客户端初始化时使用相同的默认值
*/
const DEFAULT_SETTINGS: SettingsType = {
themeColorPresets: ThemeColorPresets.Default,
themeMode: ThemeMode.Light, // 关键代码行注释SSR时默认使用明亮模式
themeLayout: ThemeLayout.Vertical,
themeStretch: true,
breadCrumb: true,
multiTab: true,
};
// ==================== Zustand 状态管理器 ====================
/**
* 设置状态管理器
* 使用 zustand 创建全局设置状态,包含默认配置和操作方法
*/
const useSettingStore = create<SettingStore>((set) => ({
// 关键代码行注释初始化设置服务端渲染时使用默认值客户端使用localStorage或默认值
settings: typeof window !== 'undefined'
? getItem<SettingsType>(StorageEnum.Settings) || DEFAULT_SETTINGS
: DEFAULT_SETTINGS,
// 操作方法集合
actions: {
/**
* 设置新的配置并同步到本地存储
* @param settings - 新的设置配置
*/
setSettings: (settings) => {
// 更新状态
set({ settings });
// 同步保存到本地存储
setItem(StorageEnum.Settings, settings);
},
/**
* 清除本地存储的设置数据
* 注意:这里只清除本地存储,不重置当前状态
*/
clearSettings() {
removeItem(StorageEnum.Settings);
},
/**
* 切换主题模式
*/
toggleThemeMode() {
set((state) => {
// 切换主题模式:明亮 ↔ 暗黑
const newThemeMode = state.settings.themeMode === ThemeMode.Light
? ThemeMode.Dark
: ThemeMode.Light;
// 创建新的设置对象
const newSettings = {
...state.settings,
themeMode: newThemeMode,
};
// 同步保存到本地存储
setItem(StorageEnum.Settings, newSettings);
// 返回新状态
return { settings: newSettings };
});
},
},
}));
// ==================== 导出的 Hook 函数 ====================
/**
* 获取当前设置状态的 Hook
* @returns 当前的设置配置对象
*/
export const useSettings = () => useSettingStore((state) => state.settings);
/**
* 获取设置操作方法的 Hook
* @returns 设置操作方法对象setSettings, clearSettings, toggleThemeMode
*/
export const useSettingActions = () => useSettingStore((state) => state.actions);
/**
* 获取当前主题模式的 Hook
* @returns 当前的主题模式ThemeMode.Light 或 ThemeMode.Dark
*/
export const useThemeMode = () => useSettingStore((state) => state.settings.themeMode);

29
src/store/useDevice.tsx Normal file
View File

@@ -0,0 +1,29 @@
// src/hooks/useDevice.tsx
import { useState, useEffect } from 'react';
type DeviceType = 'mobile' | 'tablet' | 'desktop';
const useDevice = (): DeviceType => {
const [device, setDevice] = useState<DeviceType>('desktop');
const updateDevice = () => {
const width = window.innerWidth;
if (width < 768) {
setDevice('mobile'); // 手机
} else if (width >= 768 && width < 992) {
setDevice('tablet'); // 平板
} else {
setDevice('desktop'); // 桌面
}
};
useEffect(() => {
updateDevice();
window.addEventListener('resize', updateDevice);
return () => window.removeEventListener('resize', updateDevice);
}, []);
return device;
};
export default useDevice;

121
src/store/userStore.ts Normal file
View File

@@ -0,0 +1,121 @@
/**
* 文件: src/store/userStore.ts
* 作者: 阿瑞
* 功能: 用户状态管理存储
* 版本: v1.0.0
*/
import { create } from 'zustand';
import { message } from 'antd'; // 用于显示错误消息
import { IUser } from '@/models/types';
// 定义用户存储的类型
interface UserStore {
userInfo: Partial<IUser>;
accessToken: string;
refreshToken: string;
actions: {
setUserInfo: (userInfo: IUser) => void;
setUserToken: (accessToken: string, refreshToken: string) => void;
clearUserInfoAndToken: () => void;
fetchAndSetUserInfo: () => Promise<void>; // 新增 fetchAndSetUserInfo 函数
};
}
// 安全解析 JSON 的辅助函数
function safeJSONParse<T>(item: string | null, defaultVal: T): T {
try {
return item ? JSON.parse(item) : defaultVal;
} catch (error) {
console.error('JSON parsing error:', error);
return defaultVal;
}
}
// 判断是否在浏览器环境中运行
const isBrowser = typeof window !== 'undefined';
// 使用 Zustand 创建全局用户存储
const useUserStore = create<UserStore>((set, get) => ({
// 如果在浏览器环境,则尝试从 localStorage 读取用户信息,否则使用默认值
userInfo: isBrowser ? safeJSONParse<Partial<IUser>>(localStorage.getItem('userInfo'), {}) : {},
accessToken: isBrowser ? localStorage.getItem('userAccessToken') || '' : '',
refreshToken: isBrowser ? localStorage.getItem('userRefreshToken') || '' : '',
actions: {
// 设置用户信息
setUserInfo: (userInfo: IUser) => {
set({ userInfo });
if (isBrowser) {
localStorage.setItem('userInfo', JSON.stringify(userInfo));
//console.log('setUserInfo用户信息:', userInfo);
}
},
// 设置访问令牌和刷新令牌
setUserToken: (accessToken: string, refreshToken: string) => {
set({ accessToken, refreshToken });
if (isBrowser) {
localStorage.setItem('userAccessToken', accessToken);
localStorage.setItem('userRefreshToken', refreshToken);
console.log('在localStorage更新令牌:', { accessToken, refreshToken });
}
},
// 清除用户信息和令牌
clearUserInfoAndToken: () => {
set({ userInfo: {}, accessToken: '', refreshToken: '' });
if (isBrowser) {
localStorage.removeItem('userInfo');
localStorage.removeItem('userAccessToken');
localStorage.removeItem('userRefreshToken');
console.log('从localStorage清除用户信息和令牌');
}
},
// 获取并设置用户信息 - 使用原生fetch替代axios
fetchAndSetUserInfo: async () => {
const { userInfo } = get(); // 获取当前的 userInfo
const { setUserInfo } = get().actions; // 获取 actions 中的 setUserInfo 方法
try {
const userId = userInfo._id;
if (!userId) {
throw new Error('用户未登录或用户ID缺失');
}
// 使用原生fetch替代axios
const response = await fetch(`/api/user?userId=${userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
// 检查响应状态
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const updatedUserInfo = data.userInfo;
setUserInfo(updatedUserInfo); // 更新用户信息
//打印更新成功提示
console.log('用户信息更新成功!');
} catch (error) {
console.error('获取用户信息失败:', error);
message.error('无法更新用户信息');
}
},
},
}));
// 为用户的 homePath 提供一个钩子
export const useUserHomePath = () => {
const userInfo = useUserStore((state: UserStore) => state.userInfo);
const homePath = userInfo && userInfo. ? userInfo.. : '/index';
//console.log('homePath:', homePath);
return homePath;
};
// 导出钩子函数以便使用
export const useUserInfo = () => useUserStore((state: UserStore) => state.userInfo);
export const useAccessToken = () => useUserStore((state: UserStore) => state.accessToken);
export const useRefreshToken = () => useUserStore((state: UserStore) => state.refreshToken);
export const useUserActions = () => useUserStore((state: UserStore) => state.actions);
export const useUserToken = () => useUserStore((state: UserStore) => ({ accessToken: state.accessToken, refreshToken: state.refreshToken }));
export default useUserStore;

10
src/styles/antd.css Normal file
View File

@@ -0,0 +1,10 @@
/**
* Ant Design组件特定样式覆盖
* @author 阿瑞
* @description 最小化的Ant Design组件样式调整配合globals.css使用
* @version 3.0.0 - 简化版本,移除重复定义
* @created 2024-12-19
* @updated 减少冲突,专注于必要的组件行为修正
*/
/* ==================== 仅必要的样式调整 ==================== */

View File

@@ -1,26 +1,488 @@
/**
* 毛玻璃UI全局样式配置 - 基础设施
* @author 阿瑞
* @description 优化后的基础样式配置,解决重复定义和层级关系问题
* @version 2.3.1
* @date 2024
* @updated 重构变量层级,移除重复定义,优化性能和代码组织
*
* 架构说明:
* 1. CSS变量系统基础常量 → 主题变量 → 应用样式
* 2. 性能优化:统一过渡、避免重复、合理层叠
* 3. 主题系统:支持明亮/暗黑模式无缝切换
* 4. 毛玻璃效果backdrop-filter + rgba 实现现代UI
*/
/* ==================== Tailwind CSS导入 ==================== */
@import "tailwindcss"; @import "tailwindcss";
/* ==================== 防闪烁优化 + 分层过渡策略 ==================== */
/* 关键代码行注释防止SSR水合过程中的主题闪烁采用分层过渡策略 */
html {
background-color: #f6f9fc;
transition: background-color 0.4s ease;
}
html.dark {
background-color: #0a1128;
}
/* ==================== 全局变量系统 ==================== */
/* 关键代码行注释:统一在一个:root中定义所有全局变量提高可维护性 */
:root { :root {
--background: #ffffff; /* 过渡效果系统 - 关键代码行注释:统一过渡配置,避免重复定义 */
--foreground: #171717; --transition-fast: color 0.3s ease;
--transition-medium: background-color 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease;
--transition-slow: background-color 0.4s ease, background-image 0.6s ease, color 0.3s ease;
--transition-transform: transform 0.2s ease;
--transition-all: all 0.3s ease;
/* 字体系统 - 关键代码行注释:统一字体变量,移除冗余定义 */
--font-primary: 'Geist', 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: 'Geist Mono', 'SF Mono', ui-monospace, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
/* 基础色彩常量 - 关键代码行注释:定义核心颜色,便于维护和一致性 */
--color-light-bg: #f6f9fc;
--color-light-secondary: #eef4ff;
--color-dark-bg: #0a1128;
--color-dark-secondary: #1a2332;
--color-white: #ffffff;
--color-black: #0a1128;
/* 透明度常量 - 关键代码行注释:标准化透明度值,保持视觉一致性 */
--alpha-high: 0.95;
--alpha-medium: 0.85;
--alpha-low: 0.65;
--alpha-minimal: 0.45;
/* 布局常量 - 关键代码行注释:定义常用的布局数值 */
--border-radius-sm: 8px;
--border-radius-md: 12px;
--border-radius-lg: 16px;
--border-radius-xl: 20px;
--border-radius-2xl: 24px;
/* 阴影系统 - 关键代码行注释:分级阴影系统,提供层次感 */
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.15);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.2);
--shadow-xl: 0 12px 48px rgba(0, 0, 0, 0.25);
} }
@theme inline { /* ==================== 明亮主题变量系统 ==================== */
--color-background: var(--background); .light {
--color-foreground: var(--foreground); /* 背景色系统 */
--font-sans: var(--font-geist-sans); --bg-primary: var(--color-light-bg);
--font-mono: var(--font-geist-mono); --bg-secondary: var(--color-light-secondary);
--bg-dark: var(--color-dark-bg);
/* 文本色系统 */
--text-primary: var(--color-black);
--text-secondary: rgba(10, 17, 40, var(--alpha-medium));
--text-tertiary: rgba(10, 17, 40, var(--alpha-low));
--text-quaternary: rgba(10, 17, 40, var(--alpha-minimal));
--text-light: var(--color-white);
/* 现代强调色系统 */
--color-primary-blue: #2d7ff9;
--color-primary-purple: #8e6bff;
--color-primary-cyan: #06d7b2;
--color-primary-pink: #ff66c2;
--color-primary-orange: #ff9640;
/* 毛玻璃效果参数 */
--glass-bg: rgba(255, 255, 255, 0.25);
--glass-border: rgba(255, 255, 255, 0.25);
--glass-highlight: rgba(255, 255, 255, 0.3);
--glass-shadow: rgba(31, 38, 135, 0.15);
/* 背景图片定义 - 关键代码行注释:将背景图片作为变量定义,而不是直接应用 */
--bg-gradient:
radial-gradient(circle at 80% 10%, rgba(142, 107, 255, 0.12) 0%, transparent 50%),
radial-gradient(circle at 20% 30%, rgba(6, 215, 178, 0.12) 0%, transparent 50%),
radial-gradient(circle at 90% 80%, rgba(45, 127, 249, 0.12) 0%, transparent 50%),
radial-gradient(circle at 10% 90%, rgba(255, 102, 194, 0.12) 0%, transparent 50%);
} }
@media (prefers-color-scheme: dark) { /* ==================== 暗黑主题变量系统 ==================== */
:root { .dark {
--background: #0a0a0a; /* 背景色系统 */
--foreground: #ededed; --bg-primary: var(--color-dark-bg);
--bg-secondary: var(--color-dark-secondary);
--bg-dark: var(--color-dark-bg);
/* 文本色系统 */
--text-primary: rgba(255, 255, 255, var(--alpha-high));
--text-secondary: rgba(255, 255, 255, 0.8);
--text-tertiary: rgba(255, 255, 255, 0.6);
--text-quaternary: rgba(255, 255, 255, 0.4);
--text-light: var(--color-white);
/* 暗黑主题强调色 */
--color-primary-blue: #4d8fff;
--color-primary-purple: #a785ff;
--color-primary-cyan: #26e1c2;
--color-primary-pink: #ff85d1;
--color-primary-orange: #ffb366;
/* 暗黑主题毛玻璃效果参数 */
--glass-bg: rgba(16, 22, 58, 0.25);
--glass-border: rgba(255, 255, 255, 0.08);
--glass-highlight: rgba(255, 255, 255, 0.15);
--glass-shadow: rgba(0, 0, 10, 0.2);
/* 背景图片定义 - 关键代码行注释:暗黑主题的背景图片变量 */
--bg-gradient:
radial-gradient(circle at 80% 10%, rgba(142, 107, 255, 0.18) 0%, transparent 45%),
radial-gradient(circle at 20% 30%, rgba(6, 215, 178, 0.15) 0%, transparent 45%),
radial-gradient(circle at 90% 80%, rgba(45, 127, 249, 0.18) 0%, transparent 45%),
radial-gradient(circle at 10% 90%, rgba(255, 102, 194, 0.15) 0%, transparent 45%);
}
/* ==================== 基础样式重置 ==================== */
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
/*overflow-x: hidden;*/
font-family: var(--font-primary);
transition: var(--transition-slow);
}
/* 关键代码行注释正确的背景应用方式将背景设置在body元素上 */
body {
min-height: 100vh;
line-height: 1.6;
position: relative;
font-weight: 400;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* 关键代码行注释:使用变量方式应用背景,确保背景图片能正确显示 */
color: var(--text-primary);
background-color: var(--bg-primary);
background-image: var(--bg-gradient);
background-attachment: fixed;
background-size: 100% 100%;
background-repeat: no-repeat;
}
a {
color: inherit;
text-decoration: none;
}
/* 关键代码行注释:优化字体设置选择器,分类提高效率 */
/* 基础文本元素 */
h1, h2, h3, h4, h5, h6, p, span, div, button, input, textarea, select, label {
font-family: var(--font-primary);
}
/* ==================== 动画效果定义 ==================== */
/* 关键代码行注释优化动画性能使用transform和opacity以触发硬件加速 */
@keyframes float {
0% { transform: translate3d(0, 0, 0) rotate(0); }
50% { transform: translate3d(8px, -8px, 0) rotate(2deg); }
100% { transform: translate3d(0, 0, 0) rotate(0); }
}
@keyframes blob {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.2; }
33% { transform: translate3d(15px, -15px, 0) scale(1.1); opacity: 0.25; }
66% { transform: translate3d(-10px, 10px, 0) scale(0.95); opacity: 0.18; }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translate3d(0, 30px, 0); }
to { opacity: 1; transform: translate3d(0, 0, 0); }
}
@keyframes pulse {
0%, 100% { opacity: 0.95; transform: scale3d(1, 1, 1); }
50% { opacity: 1; transform: scale3d(1.02, 1.02, 1); }
}
@keyframes shimmer {
0% { background-position: -200px 0; }
100% { background-position: calc(200px + 100%) 0; }
}
@keyframes bounce-in {
0% { transform: scale3d(0.8, 0.8, 1); opacity: 0; }
70% { transform: scale3d(1.05, 1.05, 1); opacity: 1; }
100% { transform: scale3d(1, 1, 1); }
}
@keyframes themeRipple {
0% { transform: scale3d(0, 0, 1); opacity: 0.4; }
100% { transform: scale3d(2.5, 2.5, 1); opacity: 0; }
}
/* ==================== 动画应用类 ==================== */
/* 关键代码行注释为动画元素添加will-change属性以优化性能 */
.animate-float {
animation: float 8s infinite ease-in-out;
will-change: transform;
}
.animate-blob {
animation: blob 18s infinite ease-in-out alternate;
will-change: transform, opacity;
}
.animate-pulse-slow {
animation: pulse 5s infinite ease-in-out;
will-change: transform, opacity;
}
.animate-bounce-in {
animation: bounce-in 0.3s cubic-bezier(0.38, 1.6, 0.55, 0.9) forwards;
will-change: transform, opacity;
}
.fade-in-up {
animation: fadeInUp 0.6s ease-out;
will-change: transform, opacity;
}
.shimmer-effect {
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
background-size: 200px 100%;
animation: shimmer 2s infinite;
will-change: background-position;
}
/* 动画延迟工具类 - 关键代码行注释:提供标准化的动画延迟选项 */
.animation-delay-2000 { animation-delay: 2s; }
.animation-delay-4000 { animation-delay: 4s; }
.animation-delay-6000 { animation-delay: 6s; }
/* ==================== 通用毛玻璃工具类 ==================== */
/* 关键代码行注释:使用统一的变量系统,提高一致性和可维护性 */
.glass-card {
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
box-shadow: 0 8px 32px 0 var(--glass-shadow);
border-radius: var(--border-radius-lg);
transition: var(--transition-all);
/* 关键代码行注释:添加性能优化属性 */
will-change: transform, box-shadow;
}
.glass-card:hover {
transform: translate3d(0, -2px, 0);
box-shadow: 0 12px 48px 0 var(--glass-shadow);
}
.glass-nav {
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--glass-border);
box-shadow: 0 4px 20px 0 var(--glass-shadow);
transition: var(--transition-medium);
}
.glass-button {
background: var(--glass-bg);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius-md);
transition: var(--transition-all);
will-change: transform, background-color;
}
.glass-button:hover {
background: var(--glass-highlight);
box-shadow: 0 4px 16px var(--glass-shadow);
transform: translate3d(0, -1px, 0);
}
/* ==================== 应用容器 ==================== */
.app-container {
min-height: 100vh;
/* 关键代码行注释设置为透明背景让body的背景图片能够正确显示 */
background: transparent;
font-family: var(--font-primary);
transition: var(--transition-slow);
}
/* ==================== 主题切换优化 ==================== */
/* 关键代码行注释:简化的主题切换动画,减少重复定义 */
.theme-switch-ripple {
position: relative;
overflow: hidden;
}
.theme-switch-ripple::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: var(--color-primary-blue);
transform: translate(-50%, -50%);
transition: opacity 0.2s ease-out;
z-index: -1;
opacity: 0;
}
.theme-switch-ripple:active::before {
width: 120px;
height: 120px;
opacity: 0.1;
animation: themeRipple 0.3s ease-out;
}
/* ==================== 自定义滚动条 ==================== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--glass-border);
border-radius: 4px;
transition: var(--transition-all);
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-primary-blue);
}
.dark ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
}
/* ==================== 统一过渡效果应用 ==================== */
/* 关键代码行注释:统一过渡效果,避免重复定义 */
.ant-btn, .ant-card, .ant-input, .ant-select, .ant-table, .ant-menu, .ant-modal, .ant-drawer, .ant-typography,
h1, h2, h3, h4, h5, h6, p, span {
transition: var(--transition-medium), var(--transition-transform);
}
/* ==================== 响应式断点 ==================== */
/* 关键代码行注释:遵循移动优先原则,使用统一的断点系统 */
@media (max-width: 768px) {
body {
font-size: 14px;
}
.glass-card {
border-radius: var(--border-radius-md);
}
/* 关键代码行注释:移动设备上减少动画复杂度,提升性能 */
.animate-float,
.animate-blob {
animation-duration: 12s; /* 减慢动画速度 */
} }
} }
body { /* 关键代码行注释:超小屏幕优化 */
background: var(--background); @media (max-width: 480px) {
color: var(--foreground); body {
font-family: Arial, Helvetica, sans-serif; font-size: 13px;
}
.glass-card {
border-radius: var(--border-radius-sm);
}
}
/* ==================== 动画减少设置 ==================== */
/* 关键代码行注释:尊重用户的无障碍偏好设置 */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
/* 关键代码行注释:完全禁用装饰性动画 */
.animate-float,
.animate-blob,
.animate-pulse-slow,
.shimmer-effect,
.theme-switch-ripple::before {
animation: none !important;
}
/* 关键代码行注释:保留关键的无障碍动画 */
.fade-in-up,
.animate-bounce-in {
animation-duration: 0.2s !important;
}
}
/* ==================== 性能优化和最佳实践 ==================== */
/* 关键代码行注释:为可能触发复合层的元素添加优化 */
.glass-card,
.glass-nav,
.glass-button,
.animate-float,
.animate-blob {
/* 强制创建新的复合层,避免重绘 */
transform: translateZ(0);
/* 优化文本渲染 */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 关键代码行注释:高对比度模式适配 */
@media (prefers-contrast: high) {
.glass-card,
.glass-nav,
.glass-button {
border-width: 2px;
backdrop-filter: none;
-webkit-backdrop-filter: none;
background: var(--bg-primary);
}
}
/* 关键代码行注释:暗色模式偏好检测(作为 .dark 类的后备) */
@media (prefers-color-scheme: dark) {
:root:not(.light) {
color-scheme: dark;
}
}
/* 关键代码行注释:打印样式优化 */
@media print {
.glass-card,
.glass-nav,
.glass-button {
background: white !important;
border: 1px solid #ccc !important;
box-shadow: none !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
.animate-float,
.animate-blob,
.animate-pulse-slow,
.shimmer-effect {
animation: none !important;
}
} }

2290
src/styles/globals.css.bak Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,303 @@
/**
* 毛玻璃UI主题配置文件
* @author 阿瑞
* @description Ant Design 主题配置文件,支持毛玻璃风格的明暗主题切换
* @version 1.1.0
* @created 2024-12-19
* @updated 优化字体配置确保Geist字体生效
*/
// theme/themeConfig.ts
import type { ThemeConfig } from 'antd';
import { theme } from 'antd';
// 模块级注释毛玻璃风格通用token配置
const commonToken = {
// 关键代码行注释:设置圆角大小,符合毛玻璃风格
borderRadius: 12,
borderRadiusLG: 16,
borderRadiusSM: 8,
// 关键代码行注释设置字体族确保Geist字体优先加载与globals.css保持完全一致
fontFamily: 'var(--font-primary), "Geist", "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
// 关键代码行注释:设置字体大小
fontSize: 14,
fontSizeLG: 16,
fontSizeSM: 12,
// 关键代码行注释:设置行高
lineHeight: 1.6,
lineHeightLG: 1.8,
// 关键代码行注释:控制组件高度
controlHeight: 40,
controlHeightLG: 48,
controlHeightSM: 32,
};
// 模块级注释:毛玻璃风格通用组件配置
const commonComponents = {
// 关键代码行注释:按钮组件毛玻璃风格配置
Button: {
controlHeight: 40,
controlHeightLG: 48,
controlHeightSM: 32,
borderRadius: 12,
borderRadiusLG: 16,
borderRadiusSM: 8,
fontWeight: 500,
},
// 关键代码行注释:卡片组件毛玻璃风格配置
Card: {
borderRadiusLG: 16,
borderRadius: 12,
paddingLG: 24,
padding: 16,
},
// 关键代码行注释:输入框组件配置
Input: {
borderRadius: 10,
controlHeight: 40,
paddingInline: 16,
},
// 关键代码行注释:选择器组件配置
Select: {
borderRadius: 10,
controlHeight: 40,
},
// 关键代码行注释:模态框组件配置
Modal: {
borderRadiusLG: 20,
padding: 24,
paddingLG: 32,
},
// 关键代码行注释:抽屉组件配置
Drawer: {
borderRadiusLG: 20,
padding: 24,
},
// 关键代码行注释统一字体应用确保所有Typography组件使用Geist字体
Typography: {
fontFamily: 'var(--font-primary)',
},
};
// 模块级注释:明亮主题毛玻璃风格配置
export const lightTheme: ThemeConfig = {
token: {
...commonToken,
// 关键代码行注释:设置主色调为毛玻璃风格的蓝色
colorPrimary: '#2d7ff9',
colorPrimaryHover: '#4d8fff',
colorPrimaryActive: '#1d6ff9',
// 关键代码行注释:明亮主题背景色系统
colorBgContainer: 'rgba(255, 255, 255, 0.8)',
colorBgLayout: 'rgba(255, 255, 255, 0.4)',
colorBgBase: 'rgba(255, 255, 255, 0.9)',
colorBgElevated: 'rgba(255, 255, 255, 0.9)',
colorBgMask: 'rgba(0, 0, 0, 0.45)',
// 关键代码行注释:明亮主题文本颜色系统
colorText: '#0a1128',
colorTextSecondary: 'rgba(10, 17, 40, 0.85)',
colorTextTertiary: 'rgba(10, 17, 40, 0.65)',
colorTextQuaternary: 'rgba(10, 17, 40, 0.45)',
// 关键代码行注释:明亮主题边框色系统
colorBorder: 'rgba(255, 255, 255, 0.3)',
colorBorderSecondary: 'rgba(255, 255, 255, 0.2)',
colorSplit: 'rgba(255, 255, 255, 0.2)',
// 关键代码行注释:明亮主题填充色系统
colorFill: 'rgba(255, 255, 255, 0.4)',
colorFillSecondary: 'rgba(255, 255, 255, 0.3)',
colorFillTertiary: 'rgba(255, 255, 255, 0.2)',
colorFillQuaternary: 'rgba(255, 255, 255, 0.1)',
// 关键代码行注释:功能色彩
colorSuccess: '#06d7b2',
colorSuccessHover: '#26e1c2',
colorWarning: '#ff9640',
colorWarningHover: '#ffb366',
colorError: '#ff4d4f',
colorErrorHover: '#ff7875',
colorInfo: '#8e6bff',
colorInfoHover: '#a785ff',
// 关键代码行注释:毛玻璃风格阴影效果
boxShadow: '0 4px 16px 0 rgba(31, 38, 135, 0.2)',
boxShadowSecondary: '0 8px 32px 0 rgba(31, 38, 135, 0.37)',
},
components: {
...commonComponents,
// 关键代码行注释:表格组件明亮主题配置
Table: {
headerBg: 'rgba(255, 255, 255, 0.8)',
borderColor: 'rgba(255, 255, 255, 0.3)',
rowHoverBg: 'rgba(45, 127, 249, 0.1)',
colorText: '#0a1128',
colorTextSecondary: 'rgba(10, 17, 40, 0.85)',
},
// 关键代码行注释:菜单组件明亮主题配置
Menu: {
itemBg: 'transparent',
subMenuItemBg: 'transparent',
itemHoverBg: 'rgba(45, 127, 249, 0.1)',
itemSelectedBg: 'rgba(45, 127, 249, 0.15)',
itemActiveBg: 'rgba(45, 127, 249, 0.2)',
colorText: 'rgba(10, 17, 40, 0.85)',
colorTextSecondary: 'rgba(10, 17, 40, 0.65)',
},
// 关键代码行注释:开关组件配置
Switch: {
colorPrimary: '#2d7ff9',
colorPrimaryHover: '#4d8fff',
},
// 关键代码行注释:标签页组件配置
Tabs: {
itemSelectedColor: '#2d7ff9',
itemHoverColor: '#4d8fff',
inkBarColor: '#2d7ff9',
colorText: 'rgba(10, 17, 40, 0.85)',
colorTextSecondary: 'rgba(10, 17, 40, 0.65)',
},
// 关键代码行注释:统计组件配置
Statistic: {
colorText: '#0a1128',
colorTextSecondary: 'rgba(10, 17, 40, 0.85)',
},
// 关键代码行注释:标签组件配置
Tag: {
borderRadiusSM: 8,
colorText: 'rgba(10, 17, 40, 0.85)',
colorTextSecondary: 'rgba(10, 17, 40, 0.65)',
},
},
};
// 模块级注释:暗黑主题毛玻璃风格配置
export const darkTheme: ThemeConfig = {
// 关键代码行注释使用Ant Design官方暗黑算法
algorithm: theme.darkAlgorithm,
token: {
...commonToken,
// 关键代码行注释:设置主色调为更亮的蓝色适配暗黑主题
colorPrimary: '#4d8fff',
colorPrimaryHover: '#6da3ff',
colorPrimaryActive: '#2d7ff9',
// 关键代码行注释:暗黑主题背景色系统
colorBgContainer: 'rgba(16, 22, 58, 0.8)',
colorBgLayout: 'rgba(16, 22, 58, 0.4)',
colorBgBase: 'rgba(10, 17, 40, 0.9)',
colorBgElevated: 'rgba(20, 30, 60, 0.9)',
colorBgMask: 'rgba(0, 0, 0, 0.65)',
// 关键代码行注释:暗黑主题文本颜色系统
colorText: 'rgba(255, 255, 255, 0.95)',
colorTextSecondary: 'rgba(255, 255, 255, 0.8)',
colorTextTertiary: 'rgba(255, 255, 255, 0.6)',
colorTextQuaternary: 'rgba(255, 255, 255, 0.4)',
// 关键代码行注释:暗黑主题边框色系统
colorBorder: 'rgba(255, 255, 255, 0.15)',
colorBorderSecondary: 'rgba(255, 255, 255, 0.1)',
colorSplit: 'rgba(255, 255, 255, 0.1)',
// 关键代码行注释:暗黑主题填充色系统
colorFill: 'rgba(255, 255, 255, 0.15)',
colorFillSecondary: 'rgba(255, 255, 255, 0.1)',
colorFillTertiary: 'rgba(255, 255, 255, 0.08)',
colorFillQuaternary: 'rgba(255, 255, 255, 0.05)',
// 关键代码行注释:暗黑主题功能色彩
colorSuccess: '#06d7b2',
colorSuccessHover: '#26e1c2',
colorWarning: '#ff9640',
colorWarningHover: '#ffb366',
colorError: '#ff6b6b',
colorErrorHover: '#ff8e8e',
colorInfo: '#8e6bff',
colorInfoHover: '#a785ff',
// 关键代码行注释:暗黑主题阴影效果
boxShadow: '0 4px 16px 0 rgba(0, 0, 0, 0.3)',
boxShadowSecondary: '0 8px 32px 0 rgba(0, 0, 0, 0.5)',
},
components: {
...commonComponents,
// 关键代码行注释:表格组件暗黑主题配置
Table: {
headerBg: 'rgba(255, 255, 255, 0.05)',
borderColor: 'rgba(255, 255, 255, 0.1)',
rowHoverBg: 'rgba(77, 143, 255, 0.1)',
colorText: 'rgba(255, 255, 255, 0.95)',
colorTextSecondary: 'rgba(255, 255, 255, 0.8)',
},
// 关键代码行注释:菜单组件暗黑主题配置
Menu: {
itemBg: 'transparent',
subMenuItemBg: 'transparent',
itemHoverBg: 'rgba(77, 143, 255, 0.1)',
itemSelectedBg: 'rgba(77, 143, 255, 0.15)',
itemActiveBg: 'rgba(77, 143, 255, 0.2)',
colorText: 'rgba(255, 255, 255, 0.8)',
colorTextSecondary: 'rgba(255, 255, 255, 0.6)',
},
// 关键代码行注释:开关组件暗黑主题配置
Switch: {
colorPrimary: '#4d8fff',
colorPrimaryHover: '#6da3ff',
},
// 关键代码行注释:卡片组件暗黑主题配置
Card: {
colorBgContainer: 'rgba(16, 22, 58, 0.8)',
borderRadiusLG: 16,
colorText: 'rgba(255, 255, 255, 0.95)',
colorTextSecondary: 'rgba(255, 255, 255, 0.8)',
},
// 关键代码行注释:标签页组件暗黑主题配置
Tabs: {
itemSelectedColor: '#4d8fff',
itemHoverColor: '#6da3ff',
inkBarColor: '#4d8fff',
colorText: 'rgba(255, 255, 255, 0.8)',
colorTextSecondary: 'rgba(255, 255, 255, 0.6)',
},
// 关键代码行注释:模态框组件暗黑主题配置
Modal: {
contentBg: 'rgba(16, 22, 58, 0.9)',
headerBg: 'rgba(16, 22, 58, 0.9)',
colorText: 'rgba(255, 255, 255, 0.95)',
},
// 关键代码行注释:抽屉组件暗黑主题配置
Drawer: {
colorBgElevated: 'rgba(16, 22, 58, 0.9)',
colorText: 'rgba(255, 255, 255, 0.95)',
},
// 关键代码行注释:统计组件暗黑主题配置
Statistic: {
colorText: 'rgba(255, 255, 255, 0.95)',
colorTextSecondary: 'rgba(255, 255, 255, 0.8)',
},
// 关键代码行注释:输入框暗黑主题配置
Input: {
colorText: 'rgba(255, 255, 255, 0.95)',
colorTextPlaceholder: 'rgba(255, 255, 255, 0.6)',
},
// 关键代码行注释:按钮暗黑主题配置
Button: {
colorText: 'rgba(255, 255, 255, 0.95)',
colorTextSecondary: 'rgba(255, 255, 255, 0.8)',
},
// 关键代码行注释:标签组件配置
Tag: {
borderRadiusSM: 8,
colorText: 'rgba(255, 255, 255, 0.85)',
colorTextSecondary: 'rgba(255, 255, 255, 0.65)',
},
},
};
// 模块级注释:默认导出明亮主题
const defaultTheme: ThemeConfig = lightTheme;
export default defaultTheme;

60
src/types/enum.ts Normal file
View File

@@ -0,0 +1,60 @@
export enum BasicStatus {
DISABLE,
ENABLE,
}
export enum ResultEnum {
SUCCESS = 0,
ERROR = -1,
TIMEOUT = 401,
}
export enum StorageEnum {
User = 'user',
Token = 'token',
Settings = 'settings',
I18N = 'i18nextLng',
}
export enum ThemeMode {
Light = 'light',
Dark = 'dark',
}
export enum ThemeLayout {
Vertical = 'vertical',
Horizontal = 'horizontal',
Mini = 'mini',
}
export enum ThemeColorPresets {
Default = 'default',
Cyan = 'cyan',
Purple = 'purple',
Blue = 'blue',
Orange = 'orange',
Red = 'red',
//Green = 'green',
}
export enum LocalEnum {
en_US = 'en_US',
zh_CN = 'zh_CN',
}
export enum MultiTabOperation {
FULLSCREEN = 'fullscreen',
REFRESH = 'refresh',
CLOSE = 'close',
CLOSEOTHERS = 'closeOthers',
CLOSEALL = 'closeAll',
CLOSELEFT = 'closeLeft',
CLOSERIGHT = 'closeRight',
}
export enum PermissionType {
CATALOGUE,
MENU,
BUTTON,
}

240
src/utils/ConnectDB.ts Normal file
View File

@@ -0,0 +1,240 @@
/**
* 文件: src/utils/ConnectDB.ts
* 作者: 阿瑞
* 功能: MongoDB数据库连接管理工具 - 高阶函数装饰器
* 版本: v2.0.0
* @description 为Next.js API路由提供数据库连接管理支持连接复用、错误重试、连接超时等功能
*/
import mongoose from 'mongoose';
import { NextApiRequest, NextApiResponse } from 'next';
// ===========================================
// 类型定义区域
// ===========================================
/**
* 数据库连接配置接口
*/
interface DatabaseConfig {
maxRetries?: number; // 最大重试次数
retryDelay?: number; // 重试延迟(毫秒)
connectTimeout?: number; // 连接超时(毫秒)
enableLogging?: boolean; // 是否启用日志
}
/**
* API处理函数类型定义
*/
type ApiHandler = (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
// ===========================================
// 配置常量区域
// ===========================================
/**
* 默认数据库连接配置
*/
const DEFAULT_CONFIG: Required<DatabaseConfig> = {
maxRetries: 3, // 默认重试3次
retryDelay: 1000, // 默认延迟1秒
connectTimeout: 10000, // 默认10秒超时
enableLogging: process.env.NODE_ENV === 'development', // 开发环境启用日志
};
/**
* Mongoose连接状态枚举
* 0 = 断开连接, 1 = 已连接, 2 = 正在连接, 3 = 正在断开连接
*/
const CONNECTION_STATES = {
DISCONNECTED: 0,
CONNECTED: 1,
CONNECTING: 2,
DISCONNECTING: 3,
} as const;
// ===========================================
// 工具函数区域
// ===========================================
/**
* 安全的日志记录函数
* @param message 日志消息
* @param data 可选的数据对象
* @param isError 是否为错误日志
*/
const safeLog = (message: string, data?: any, isError = false): void => {
if (DEFAULT_CONFIG.enableLogging) {
if (isError) {
console.error(`[DB Error] ${message}`, data);
} else {
console.log(`[DB Info] ${message}`, data);
}
}
};
/**
* 延迟函数
* @param ms 延迟毫秒数
*/
const delay = (ms: number): Promise<void> =>
new Promise(resolve => setTimeout(resolve, ms));
/**
* 检查数据库连接状态
* @returns 是否已连接
*/
const isDbConnected = (): boolean => {
const connection = mongoose.connections[0];
return connection && connection.readyState === CONNECTION_STATES.CONNECTED;
};
/**
* 获取数据库连接URI
* @returns 数据库URI或null
*/
const getDatabaseUri = (): string | null => {
const dbUri = process.env.MONGODB_URI;
if (!dbUri) {
safeLog('数据库URI未在环境变量中配置', null, true);
return null;
}
return dbUri;
};
// ===========================================
// 核心连接功能区域
// ===========================================
/**
* 执行数据库连接(带重试机制)
* @param dbUri 数据库连接URI
* @param config 连接配置
* @returns Promise<boolean> 连接是否成功
*/
const connectWithRetry = async (
dbUri: string,
config: Required<DatabaseConfig>
): Promise<boolean> => {
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
try {
safeLog(`尝试连接数据库 (第${attempt}/${config.maxRetries}次)`);
// 设置mongoose连接选项
const connectOptions = {
serverSelectionTimeoutMS: config.connectTimeout,
socketTimeoutMS: config.connectTimeout,
family: 4, // 使用IPv4
maxPoolSize: 10, // 连接池大小
retryWrites: true,
w: 'majority' as const, // 写关注级别 - 修复类型错误
};
await mongoose.connect(dbUri, connectOptions);
safeLog('数据库连接成功');
return true;
} catch (error) {
const isLastAttempt = attempt === config.maxRetries;
safeLog(
`数据库连接失败 (第${attempt}/${config.maxRetries}次)`,
{ error: error instanceof Error ? error.message : 'Unknown error' },
true
);
if (!isLastAttempt) {
safeLog(`等待${config.retryDelay}ms后重试...`);
await delay(config.retryDelay);
}
}
}
return false;
};
// ===========================================
// 主要导出功能区域
// ===========================================
/**
* 数据库连接高阶函数装饰器
* @description 为API路由提供数据库连接管理包含连接复用、错误重试、超时处理等功能
* @param handler API处理函数
* @param config 可选的数据库连接配置
* @returns 包装后的API处理函数
*
* @example
* ```typescript
* export default connectDB(async (req, res) => {
* // 你的API逻辑
* });
* ```
*/
const connectDB = (
handler: ApiHandler,
config: DatabaseConfig = {}
) => async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
// 合并配置
const finalConfig: Required<DatabaseConfig> = { ...DEFAULT_CONFIG, ...config };
try {
// 检查现有连接状态
if (isDbConnected()) {
safeLog('使用现有数据库连接');
return await handler(req, res);
}
// 获取数据库URI
const dbUri = getDatabaseUri();
if (!dbUri) {
return res.status(500).json({
error: 'Database configuration error',
message: process.env.NODE_ENV === 'development'
? 'MONGODB_URI not found in environment variables'
: 'Database configuration is missing'
});
}
// 尝试连接数据库
const connected = await connectWithRetry(dbUri, finalConfig);
if (!connected) {
safeLog('所有数据库连接尝试均失败', null, true);
return res.status(500).json({
error: 'Database connection failed',
message: 'Unable to establish database connection after multiple attempts'
});
}
// 成功连接后执行API处理函数
return await handler(req, res);
} catch (error) {
// 全局错误捕获
safeLog('数据库连接装饰器发生未预期错误', error, true);
return res.status(500).json({
error: 'Internal server error',
message: process.env.NODE_ENV === 'development'
? (error instanceof Error ? error.message : 'Unknown error occurred')
: 'An unexpected error occurred'
});
}
};
// ===========================================
// 导出区域
// ===========================================
export default connectDB;
/**
* 导出配置常量供外部使用
*/
export { DEFAULT_CONFIG, CONNECTION_STATES };
/**
* 导出类型定义供外部使用
*/
export type { DatabaseConfig, ApiHandler };

237
src/utils/theme.ts Normal file
View File

@@ -0,0 +1,237 @@
/**
* 主题工具函数
* @author 阿瑞
* @description 主题管理工具与项目的主题系统集成支持Ant Design主题配置和CSS变量
* @version 3.0.0
* @created 2024-12-19
* @updated 重构以适配当前项目的主题架构
*/
import { useCallback, useEffect, useState, useMemo } from 'react';
import { useSettingActions, useThemeMode } from '@/store/settingStore';
import { ThemeMode as ProjectThemeMode } from '@/types/enum';
import { lightTheme, darkTheme } from '@/styles/theme/themeConfig';
import type { ThemeConfig } from 'antd';
// 模块级注释主题Token接口与Ant Design集成
export interface ThemeToken {
colorPrimary: string;
colorText: string;
colorBgContainer: string;
colorLink: string;
colorLinkHover: string;
colorBorder: string;
padding: number;
fontSize: number;
lineHeight: number;
borderRadius: number;
[key: string]: any;
}
// 模块级注释默认主题Token配置
const defaultLightToken: ThemeToken = {
colorPrimary: '#2d7ff9',
colorText: '#0a1128',
colorBgContainer: 'rgba(255, 255, 255, 0.8)',
colorLink: '#2d7ff9',
colorLinkHover: '#4d8fff',
colorBorder: 'rgba(255, 255, 255, 0.3)',
padding: 16,
fontSize: 14,
lineHeight: 1.6,
borderRadius: 12,
};
const defaultDarkToken: ThemeToken = {
colorPrimary: '#4d8fff',
colorText: 'rgba(255, 255, 255, 0.95)',
colorBgContainer: 'rgba(16, 22, 58, 0.8)',
colorLink: '#4d8fff',
colorLinkHover: '#6da3ff',
colorBorder: 'rgba(255, 255, 255, 0.15)',
padding: 16,
fontSize: 14,
lineHeight: 1.6,
borderRadius: 12,
};
// 模块级注释主题Hook接口
export interface UseThemeReturn {
navTheme: 'light' | 'realDark';
themeToken: ThemeToken;
toggleTheme: () => void;
changePrimaryColor: (color: string) => void;
antdTheme: ThemeConfig;
isDark: boolean;
mounted: boolean;
}
// 模块级注释应用CSS主题类的函数
const applyThemeClasses = (isDark: boolean): void => {
if (typeof document !== 'undefined') {
const root = document.documentElement;
const body = document.body;
// 关键代码行注释:清除旧的主题类
body.classList.remove('light', 'dark');
root.classList.remove('light', 'dark');
// 关键代码行注释:应用新的主题类
const themeClass = isDark ? 'dark' : 'light';
body.classList.add(themeClass);
root.classList.add(themeClass);
// 关键代码行注释设置data-theme属性
body.setAttribute('data-theme', themeClass);
root.setAttribute('data-theme', themeClass);
}
};
// 模块级注释从Ant Design主题配置提取Token
const extractTokenFromTheme = (theme: ThemeConfig, isDark: boolean): ThemeToken => {
const token = theme.token || {};
const baseToken = isDark ? defaultDarkToken : defaultLightToken;
return {
...baseToken,
colorPrimary: token.colorPrimary || baseToken.colorPrimary,
colorText: token.colorText || baseToken.colorText,
colorBgContainer: token.colorBgContainer || baseToken.colorBgContainer,
colorLink: token.colorPrimary || baseToken.colorLink,
colorLinkHover: token.colorPrimaryHover || baseToken.colorLinkHover,
colorBorder: token.colorBorder || baseToken.colorBorder,
padding: token.padding || baseToken.padding,
fontSize: token.fontSize || baseToken.fontSize,
lineHeight: token.lineHeight || baseToken.lineHeight,
borderRadius: token.borderRadius || baseToken.borderRadius,
};
};
// 模块级注释主题Hook实现
export const useTheme = (
_updateLayoutSettings?: (newTheme: 'light' | 'realDark', newColorPrimary?: string) => void
): UseThemeReturn => {
// 关键代码行注释使用zustand store管理主题状态
const themeMode = useThemeMode();
const { toggleThemeMode } = useSettingActions();
// 关键代码行注释:状态管理
const [mounted, setMounted] = useState(false);
const [customPrimaryColor, setCustomPrimaryColor] = useState<string>('');
// 关键代码行注释:计算当前主题状态
const isDark = themeMode === ProjectThemeMode.Dark;
const navTheme = isDark ? 'realDark' : 'light';
// 关键代码行注释获取当前Ant Design主题配置
const currentAntdTheme = useMemo(() => {
const baseTheme = isDark ? darkTheme : lightTheme;
// 如果有自定义主色,应用到主题配置
if (customPrimaryColor) {
return {
...baseTheme,
token: {
...baseTheme.token,
colorPrimary: customPrimaryColor,
colorPrimaryHover: customPrimaryColor,
colorLink: customPrimaryColor,
},
};
}
return baseTheme;
}, [isDark, customPrimaryColor]);
// 关键代码行注释提取主题Token
const themeToken = useMemo(() => {
const token = extractTokenFromTheme(currentAntdTheme, isDark);
// 如果有自定义主色,覆盖相关颜色
if (customPrimaryColor) {
token.colorPrimary = customPrimaryColor;
token.colorLink = customPrimaryColor;
}
return token;
}, [currentAntdTheme, isDark, customPrimaryColor]);
// 关键代码行注释:主题切换函数
const toggleTheme = useCallback(() => {
toggleThemeMode();
// 关键代码行注释:调用旧的回调函数以保持兼容性
if (_updateLayoutSettings) {
const newTheme = isDark ? 'light' : 'realDark';
_updateLayoutSettings(newTheme, themeToken.colorPrimary);
}
}, [toggleThemeMode, isDark, themeToken.colorPrimary, _updateLayoutSettings]);
// 关键代码行注释:主色更改函数
const changePrimaryColor = useCallback((color: string) => {
setCustomPrimaryColor(color);
// 关键代码行注释保存到localStorage
if (typeof localStorage !== 'undefined') {
localStorage.setItem('customPrimaryColor', color);
}
// 关键代码行注释:调用旧的回调函数以保持兼容性
if (_updateLayoutSettings) {
_updateLayoutSettings(navTheme, color);
}
}, [navTheme, _updateLayoutSettings]);
// 关键代码行注释:组件挂载时的初始化
useEffect(() => {
setMounted(true);
// 关键代码行注释从localStorage恢复自定义主色
if (typeof localStorage !== 'undefined') {
const savedColor = localStorage.getItem('customPrimaryColor');
if (savedColor) {
setCustomPrimaryColor(savedColor);
}
}
}, []);
// 关键代码行注释应用CSS主题类
useEffect(() => {
if (mounted) {
applyThemeClasses(isDark);
}
}, [mounted, isDark]);
// 关键代码行注释:监听存储变化(多标签页同步)
useEffect(() => {
if (!mounted) return;
const handleStorageChange = (event: StorageEvent) => {
if (event.key === 'customPrimaryColor' && event.newValue) {
setCustomPrimaryColor(event.newValue);
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [mounted]);
return {
navTheme,
themeToken,
toggleTheme,
changePrimaryColor,
antdTheme: currentAntdTheme,
isDark,
mounted,
};
};
// 模块级注释:导出类型别名以保持兼容性
export type ThemeMode = 'light' | 'realDark';
// 模块级注释:导出主题配置
export { lightTheme, darkTheme };
// 模块级注释:导出工具函数
export { applyThemeClasses };

View File

@@ -5,6 +5,8 @@
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"noUnusedLocals": true, // 未使用的局部变量
"noUnusedParameters": true, // 未使用的参数
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "esnext", "module": "esnext",