Browse Source

Merge 28e98b2334 into 607e63f4fd

pull/11631/merge
afc163 2 weeks ago
committed by GitHub
parent
commit
675a356745
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      .gitignore
  2. 125
      README.ar-DZ.md
  3. 125
      README.es-ES.md
  4. 125
      README.fr-FR.md
  5. 125
      README.ja-JP.md
  6. 50
      README.md
  7. 125
      README.pt-BR.md
  8. 125
      README.ru-RU.md
  9. 125
      README.tr-TR.md
  10. 50
      README.zh-CN.md
  11. 3
      biome.json
  12. 3
      config/config.ts
  13. 55
      config/routes.simple.ts
  14. 242
      config/routes.ts
  15. 2
      jest.config.ts
  16. 210
      mock/analysis.mock.ts
  17. 38
      mock/listTableList.ts
  18. 6
      mock/notices.ts
  19. 63
      mock/requestRecord.mock.js
  20. 6
      mock/user.ts
  21. 418
      mock/workplace.mock.ts
  22. 18
      package.json
  23. 2
      public/scripts/loading.js
  24. 165
      scripts/simple.js
  25. 3
      src/locales/bn-BD.ts
  26. 3
      src/locales/en-US.ts
  27. 3
      src/locales/fa-IR.ts
  28. 3
      src/locales/id-ID.ts
  29. 2
      src/locales/ja-JP.ts
  30. 2
      src/locales/zh-CN.ts
  31. 69
      src/pages/account/center/Center.style.ts
  32. 249
      src/pages/account/center/_mock.ts
  33. 43
      src/pages/account/center/components/Applications/index.style.ts
  34. 136
      src/pages/account/center/components/Applications/index.tsx
  35. 31
      src/pages/account/center/components/ArticleListContent/index.style.ts
  36. 29
      src/pages/account/center/components/ArticleListContent/index.tsx
  37. 14
      src/pages/account/center/components/Articles/index.style.ts
  38. 71
      src/pages/account/center/components/Articles/index.tsx
  39. 41
      src/pages/account/center/components/AvatarList/index.style.ts
  40. 89
      src/pages/account/center/components/AvatarList/index.tsx
  41. 49
      src/pages/account/center/components/Projects/index.style.ts
  42. 65
      src/pages/account/center/components/Projects/index.tsx
  43. 75
      src/pages/account/center/data.d.ts
  44. 278
      src/pages/account/center/index.tsx
  45. 14
      src/pages/account/center/service.ts
  46. 80
      src/pages/account/settings/_mock.ts
  47. 239
      src/pages/account/settings/components/base.tsx
  48. 48
      src/pages/account/settings/components/binding.tsx
  49. 60
      src/pages/account/settings/components/index.style.ts
  50. 44
      src/pages/account/settings/components/notification.tsx
  51. 60
      src/pages/account/settings/components/security.tsx
  52. 43
      src/pages/account/settings/data.d.ts
  53. 1784
      src/pages/account/settings/geographic/city.json
  54. 138
      src/pages/account/settings/geographic/province.json
  55. 108
      src/pages/account/settings/index.tsx
  56. 20
      src/pages/account/settings/service.ts
  57. 74
      src/pages/account/settings/style.style.ts
  58. 210
      src/pages/dashboard/analysis/_mock.ts
  59. 75
      src/pages/dashboard/analysis/components/Charts/ChartCard/index.less
  60. 77
      src/pages/dashboard/analysis/components/Charts/ChartCard/index.style.ts
  61. 110
      src/pages/dashboard/analysis/components/Charts/ChartCard/index.tsx
  62. 17
      src/pages/dashboard/analysis/components/Charts/Field/index.less
  63. 22
      src/pages/dashboard/analysis/components/Charts/Field/index.style.ts
  64. 17
      src/pages/dashboard/analysis/components/Charts/Field/index.tsx
  65. 19
      src/pages/dashboard/analysis/components/Charts/index.less
  66. 23
      src/pages/dashboard/analysis/components/Charts/index.style.ts
  67. 13
      src/pages/dashboard/analysis/components/Charts/index.tsx
  68. 168
      src/pages/dashboard/analysis/components/IntroduceRow.tsx
  69. 68
      src/pages/dashboard/analysis/components/NumberInfo/index.less
  70. 56
      src/pages/dashboard/analysis/components/NumberInfo/index.style.ts
  71. 79
      src/pages/dashboard/analysis/components/NumberInfo/index.tsx
  72. 110
      src/pages/dashboard/analysis/components/OfflineData.tsx
  73. 9
      src/pages/dashboard/analysis/components/PageLoading/index.tsx
  74. 67
      src/pages/dashboard/analysis/components/ProportionSales.tsx
  75. 225
      src/pages/dashboard/analysis/components/SalesCard.tsx
  76. 181
      src/pages/dashboard/analysis/components/TopSearch.tsx
  77. 37
      src/pages/dashboard/analysis/components/Trend/index.less
  78. 32
      src/pages/dashboard/analysis/components/Trend/index.style.ts
  79. 47
      src/pages/dashboard/analysis/components/Trend/index.tsx
  80. 45
      src/pages/dashboard/analysis/data.d.ts
  81. 157
      src/pages/dashboard/analysis/index.tsx
  82. 6
      src/pages/dashboard/analysis/service.ts
  83. 189
      src/pages/dashboard/analysis/style.less
  84. 160
      src/pages/dashboard/analysis/style.style.ts
  85. 17
      src/pages/dashboard/analysis/utils/Yuan.tsx
  86. 57
      src/pages/dashboard/analysis/utils/utils.ts
  87. 14
      src/pages/dashboard/monitor/_mock.ts
  88. 51
      src/pages/dashboard/monitor/components/ActiveChart/index.less
  89. 48
      src/pages/dashboard/monitor/components/ActiveChart/index.style.ts
  90. 94
      src/pages/dashboard/monitor/components/ActiveChart/index.tsx
  91. 150
      src/pages/dashboard/monitor/components/Map/index.tsx
  92. 5
      src/pages/dashboard/monitor/data.d.ts
  93. 203
      src/pages/dashboard/monitor/index.tsx
  94. 6
      src/pages/dashboard/monitor/service.ts
  95. 21
      src/pages/dashboard/monitor/style.less
  96. 16
      src/pages/dashboard/monitor/style.style.ts
  97. 410
      src/pages/dashboard/workplace/_mock.ts
  98. 16
      src/pages/dashboard/workplace/components/EditableLinkGroup/index.less
  99. 21
      src/pages/dashboard/workplace/components/EditableLinkGroup/index.style.ts
  100. 38
      src/pages/dashboard/workplace/components/EditableLinkGroup/index.tsx

3
.gitignore

@ -28,6 +28,9 @@ package-lock.json
functions/*
.temp/**
# claude code local settings
.claude/settings.local.json
# umi
.umi
.umi-production

125
README.ar-DZ.md

@ -1,125 +0,0 @@
# Ant Design Pro
اللغة: 🇺🇸 | [🇨🇳](./README.zh-CN.md) | [🇷🇺](./README.ru-RU.md) | [🇹🇷](./README.tr-TR.md) | [🇯🇵](./README.ja-JP.md) | [🇫🇷](./README.fr-FR.md) | [🇧🇷](./README.pt-BR.md) | [🇩🇿](./README.ar-DZ.md) | [🇪🇸](./README.es-ES.md)
<h1 align="center">Ant Design Pro</h1>
<div align="center">
حل واجهة مستخدم جاهز لتطبيقات المؤسسات مبني على React.
[![CI](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml)
[![Preview Deploy](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml)
[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/)
[![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev)
[![](https://badgen.net/badge/icon/Ant%20Design?icon=https://gw.alipayobjects.com/zos/antfincdn/Pp4WPgVDB3/KDpgvguMpGfqaHPjicRK.svg&label)](https://ant.design/)
<img width="1718" height="1191" alt="معاينة السمة الفاتحة" src="https://github.com/user-attachments/assets/74ad0b4a-e086-4955-8edd-9f2cff31aee8" />
<img width="1718" height="1191" alt="معاينة السمة الداكنة" src="https://github.com/user-attachments/assets/d4bcb7c1-42c7-4c0f-b130-1193a931f9f7" />
</div>
- المعاينة: http://preview.pro.ant.design
- الصفحة الرئيسية: http://pro.ant.design
- التوثيق: http://pro.ant.design/docs/getting-started
- سجل التغييرات: http://pro.ant.design/docs/changelog
- الأسئلة الشائعة: http://pro.ant.design/docs/faq
## الميزات
- :bulb: **TypeScript**: لغة لتطبيقات JavaScript على نطاق واسع
- :scroll: **الكتل**: بناء الصفحات باستخدام قوالب الكتل
- :gem: **تصميم أنيق**: يتبع [مواصفات Ant Design](http://ant.design/)
- :triangular_ruler: **قوالب شائعة**: قوالب نموذجية لتطبيقات المؤسسات
- :rocket: **تطوير حديث**: أحدث تقنيات React/umi/dva/antd
- :iphone: **متجاوب**: مصمم لأحجام شاشات مختلفة
- :art: **تخصيص السمة**: سمة قابلة للتخصيص بإعداد بسيط
- :globe_with_meridians: **دعم اللغات**: حل i18n مدمج
- :gear: **أفضل الممارسات**: سير عمل قوي للحفاظ على صحة الكود
- :1234: **تطوير وهمي**: حل تطوير وهمي سهل الاستخدام
- :white_check_mark: **اختبار الواجهة**: أمان مع اختبارات الوحدة وe2e
## القوالب
```
- لوحة القيادة
- تحليلات
- مراقبة
- مساحة العمل
- النماذج
- نموذج أساسي
- نموذج متعدد الخطوات
- نموذج متقدم
- القوائم
- جدول قياسي
- قائمة قياسية
- قائمة البطاقات
- قائمة البحث (مشروع/تطبيقات/مقال)
- الملف الشخصي
- ملف شخصي بسيط
- ملف شخصي متقدم
- الحساب
- مركز الحساب
- إعدادات الحساب
- النتائج
- نجاح
- فشل
- الاستثناءات
- 403
- 404
- 500
- المستخدم
- تسجيل الدخول
- التسجيل
- نتيجة التسجيل
```
## الاستخدام
### استخدام bash
نوفر pro-cli لبدء المشروع بسرعة.
```bash
# استخدم npm
npm i @ant-design/pro-cli -g
pro create myapp
```
اختر قالب pro. Simple هو القالب الأساسي الذي يوفر فقط المحتوى الأساسي لتشغيل الإطار. Complete يحتوي على جميع الكتل، وهو غير مناسب كقالب أساسي للتطوير الثانوي.
```shell
? 🚀 مشروع كامل أم هيكل بسيط؟ (استخدم الأسهم)
➥ simple
complete
```
تهيئة مستودع Git:
```shell
$ git init myapp
```
تثبيت التبعيات:
```shell
$ cd myapp && tyarn
// أو
$ cd myapp && npm install
```
## المتصفحات المدعومة
المتصفحات الحديثة.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --- | --- | --- | --- | --- |
| Edge | آخر إصدارين | آخر إصدارين | آخر إصدارين | آخر إصدارين |
## المساهمة
أي مساهمة مرحب بها. إليك بعض الطرق للمساهمة في هذا المشروع:
- استخدم Ant Design Pro في عملك اليومي.
- أرسل [issues](http://github.com/ant-design/ant-design-pro/issues) للإبلاغ عن الأخطاء أو طرح الأسئلة.
- اقترح [pull requests](http://github.com/ant-design/ant-design-pro/pulls) لتحسين الكود الخاص بنا.

125
README.es-ES.md

@ -1,125 +0,0 @@
# Ant Design Pro
Idioma: 🇺🇸 | [🇨🇳](./README.zh-CN.md) | [🇷🇺](./README.ru-RU.md) | [🇹🇷](./README.tr-TR.md) | [🇯🇵](./README.ja-JP.md) | [🇫🇷](./README.fr-FR.md) | [🇧🇷](./README.pt-BR.md) | [🇩🇿](./README.ar-DZ.md) | [🇪🇸](./README.es-ES.md)
<h1 align="center">Ant Design Pro</h1>
<div align="center">
Una solución de interfaz de usuario lista para usar para aplicaciones empresariales basada en React.
[![CI](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml)
[![Preview Deploy](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml)
[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/)
[![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev)
[![](https://badgen.net/badge/icon/Ant%20Design?icon=https://gw.alipayobjects.com/zos/antfincdn/Pp4WPgVDB3/KDpgvguMpGfqaHPjicRK.svg&label)](https://ant.design/)
<img width="1718" height="1191" alt="vista previa del tema claro" src="https://github.com/user-attachments/assets/74ad0b4a-e086-4955-8edd-9f2cff31aee8" />
<img width="1718" height="1191" alt="vista previa del tema oscuro" src="https://github.com/user-attachments/assets/d4bcb7c1-42c7-4c0f-b130-1193a931f9f7" />
</div>
- Vista previa: http://preview.pro.ant.design
- Página principal: http://pro.ant.design
- Documentación: http://pro.ant.design/docs/getting-started
- Registro de cambios: http://pro.ant.design/docs/changelog
- Preguntas frecuentes: http://pro.ant.design/docs/faq
## Características
- :bulb: **TypeScript**: Un lenguaje para aplicaciones JavaScript a gran escala
- :scroll: **Bloques**: Construye páginas con plantillas de bloques
- :gem: **Diseño elegante**: Sigue la [especificación de Ant Design](http://ant.design/)
- :triangular_ruler: **Plantillas comunes**: Plantillas típicas para aplicaciones empresariales
- :rocket: **Desarrollo de vanguardia**: Stack de desarrollo más reciente con React/umi/dva/antd
- :iphone: **Responsivo**: Diseñado para diferentes tamaños de pantalla
- :art: **Tematización**: Tema personalizable con configuración sencilla
- :globe_with_meridians: **Internacionalización**: Solución i18n integrada
- :gear: **Buenas prácticas**: Flujo de trabajo sólido para mantener tu código saludable
- :1234: **Desarrollo mock**: Solución de desarrollo mock fácil de usar
- :white_check_mark: **Pruebas de UI**: Seguridad con pruebas unitarias y e2e
## Plantillas
```
- Panel de control
- Analítica
- Monitorización
- Espacio de trabajo
- Formulario
- Formulario básico
- Formulario por pasos
- Formulario avanzado
- Lista
- Tabla estándar
- Lista estándar
- Lista de tarjetas
- Lista de búsqueda (Proyecto/Aplicaciones/Artículo)
- Perfil
- Perfil simple
- Perfil avanzado
- Cuenta
- Centro de cuenta
- Configuración de cuenta
- Resultado
- Éxito
- Fallo
- Excepción
- 403
- 404
- 500
- Usuario
- Iniciar sesión
- Registrarse
- Resultado del registro
```
## Uso
### Usar bash
Proporcionamos pro-cli para inicializar rápidamente el proyecto.
```bash
# usar npm
npm i @ant-design/pro-cli -g
pro create myapp
```
Elige la plantilla pro. Simple es la plantilla básica, que solo proporciona el contenido esencial para el funcionamiento del framework. Complete contiene todos los bloques, por lo que no es adecuada como plantilla base para desarrollo secundario.
```shell
? 🚀 ¿Proyecto completo o un esqueleto simple? (Usa las flechas)
➥ simple
complete
```
Inicializa el repositorio Git:
```shell
$ git init myapp
```
Instala las dependencias:
```shell
$ cd myapp && tyarn
// o
$ cd myapp && npm install
```
## Navegadores soportados
Navegadores modernos.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --- | --- | --- | --- | --- |
| Edge | últimas 2 versiones | últimas 2 versiones | últimas 2 versiones | últimas 2 versiones |
## Contribución
Cualquier tipo de contribución es bienvenida. Aquí tienes algunos ejemplos de cómo puedes contribuir a este proyecto:
- Usa Ant Design Pro en tu trabajo diario.
- Envía [issues](http://github.com/ant-design/ant-design-pro/issues) para informar de errores o hacer preguntas.
- Propón [pull requests](http://github.com/ant-design/ant-design-pro/pulls) para mejorar nuestro código.

125
README.fr-FR.md

@ -1,125 +0,0 @@
# Ant Design Pro
Langue : 🇺🇸 | [🇨🇳](./README.zh-CN.md) | [🇷🇺](./README.ru-RU.md) | [🇹🇷](./README.tr-TR.md) | [🇯🇵](./README.ja-JP.md) | [🇫🇷](./README.fr-FR.md) | [🇧🇷](./README.pt-BR.md) | [🇩🇿](./README.ar-DZ.md) | [🇪🇸](./README.es-ES.md)
<h1 align="center">Ant Design Pro</h1>
<div align="center">
Une solution d'interface utilisateur prête à l'emploi pour les applications d'entreprise, basée sur React.
[![CI](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml)
[![Preview Deploy](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml)
[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/)
[![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev)
[![](https://badgen.net/badge/icon/Ant%20Design?icon=https://gw.alipayobjects.com/zos/antfincdn/Pp4WPgVDB3/KDpgvguMpGfqaHPjicRK.svg&label)](https://ant.design/)
<img width="1718" height="1191" alt="aperçu du thème clair" src="https://github.com/user-attachments/assets/74ad0b4a-e086-4955-8edd-9f2cff31aee8" />
<img width="1718" height="1191" alt="aperçu du thème sombre" src="https://github.com/user-attachments/assets/d4bcb7c1-42c7-4c0f-b130-1193a931f9f7" />
</div>
- Aperçu : http://preview.pro.ant.design
- Page d'accueil : http://pro.ant.design
- Documentation : http://pro.ant.design/docs/getting-started
- Journal des modifications : http://pro.ant.design/docs/changelog
- FAQ : http://pro.ant.design/docs/faq
## Fonctionnalités
- :bulb: **TypeScript** : Un langage pour des applications JavaScript à grande échelle
- :scroll: **Blocs** : Construisez des pages avec des modèles de blocs
- :gem: **Design soigné** : Conforme à la [spécification Ant Design](http://ant.design/)
- :triangular_ruler: **Modèles courants** : Modèles typiques pour les applications d'entreprise
- :rocket: **Développement de pointe** : Stack de développement la plus récente avec React/umi/dva/antd
- :iphone: **Responsive** : Conçu pour différentes tailles d'écran
- :art: **Thématisation** : Thème personnalisable avec une configuration simple
- :globe_with_meridians: **Internationalisation** : Solution i18n intégrée
- :gear: **Bonnes pratiques** : Workflow solide pour garder votre code sain
- :1234: **Développement mock** : Solution de développement mock facile à utiliser
- :white_check_mark: **Tests UI** : Sécurité avec des tests unitaires et e2e
## Modèles
```
- Tableau de bord
- Analytique
- Surveillance
- Espace de travail
- Formulaire
- Formulaire de base
- Formulaire par étapes
- Formulaire avancé
- Liste
- Tableau standard
- Liste standard
- Liste de cartes
- Liste de recherche (Projet/Applications/Article)
- Profil
- Profil simple
- Profil avancé
- Compte
- Centre de compte
- Paramètres du compte
- Résultat
- Succès
- Échec
- Exception
- 403
- 404
- 500
- Utilisateur
- Connexion
- Inscription
- Résultat d'inscription
```
## Utilisation
### Utiliser bash
Nous fournissons pro-cli pour initialiser rapidement le projet.
```bash
# utiliser npm
npm i @ant-design/pro-cli -g
pro create myapp
```
Choisissez le modèle pro. Simple est le modèle de base, qui ne fournit que le contenu de base pour le fonctionnement du framework. Complete contient tous les blocs, ce qui n'est pas adapté comme modèle de base pour un développement secondaire.
```shell
? 🚀 Un projet complet ou un simple squelette ? (Utilisez les flèches)
➥ simple
complete
```
Initialisez le dépôt Git :
```shell
$ git init myapp
```
Installez les dépendances :
```shell
$ cd myapp && tyarn
// ou
$ cd myapp && npm install
```
## Navigateurs supportés
Navigateurs modernes.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --- | --- | --- | --- | --- |
| Edge | 2 dernières versions | 2 dernières versions | 2 dernières versions | 2 dernières versions |
## Contribution
Toute contribution est la bienvenue, voici quelques exemples de comment vous pouvez contribuer à ce projet :
- Utilisez Ant Design Pro dans votre travail quotidien.
- Soumettez des [issues](http://github.com/ant-design/ant-design-pro/issues) pour signaler des bugs ou poser des questions.
- Proposez des [pull requests](http://github.com/ant-design/ant-design-pro/pulls) pour améliorer notre code.

125
README.ja-JP.md

@ -1,125 +0,0 @@
# Ant Design Pro
言語: 🇺🇸 | [🇨🇳](./README.zh-CN.md) | [🇷🇺](./README.ru-RU.md) | [🇹🇷](./README.tr-TR.md) | [🇯🇵](./README.ja-JP.md) | [🇫🇷](./README.fr-FR.md) | [🇧🇷](./README.pt-BR.md) | [🇩🇿](./README.ar-DZ.md) | [🇪🇸](./README.es-ES.md)
<h1 align="center">Ant Design Pro</h1>
<div align="center">
エンタープライズアプリケーション向けの、Reactベースのすぐに使えるUIソリューション。
[![CI](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml)
[![Preview Deploy](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml)
[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/)
[![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev)
[![](https://badgen.net/badge/icon/Ant%20Design?icon=https://gw.alipayobjects.com/zos/antfincdn/Pp4WPgVDB3/KDpgvguMpGfqaHPjicRK.svg&label)](https://ant.design/)
<img width="1718" height="1191" alt="ライトテーマのプレビュー" src="https://github.com/user-attachments/assets/74ad0b4a-e086-4955-8edd-9f2cff31aee8" />
<img width="1718" height="1191" alt="ダークテーマのプレビュー" src="https://github.com/user-attachments/assets/d4bcb7c1-42c7-4c0f-b130-1193a931f9f7" />
</div>
- プレビュー: http://preview.pro.ant.design
- ホームページ: http://pro.ant.design
- ドキュメント: http://pro.ant.design/docs/getting-started
- 変更履歴: http://pro.ant.design/docs/changelog
- FAQ: http://pro.ant.design/docs/faq
## 特徴
- :bulb: **TypeScript**: 大規模JavaScriptアプリケーション向けの言語
- :scroll: **ブロック**: ブロックテンプレートでページを構築
- :gem: **洗練されたデザイン**: [Ant Design仕様](http://ant.design/)に準拠
- :triangular_ruler: **一般的なテンプレート**: 企業向けアプリケーションの典型的なテンプレート
- :rocket: **最新の開発環境**: React/umi/dva/antdの最新スタック
- :iphone: **レスポンシブ**: 様々な画面サイズに対応
- :art: **テーマ**: シンプルな設定でカスタマイズ可能なテーマ
- :globe_with_meridians: **国際化**: 組み込みのi18nソリューション
- :gear: **ベストプラクティス**: 健全なコードを保つためのワークフロー
- :1234: **モック開発**: 使いやすいモック開発ソリューション
- :white_check_mark: **UIテスト**: ユニットテストとE2Eテストで安全に
## テンプレート
```
- ダッシュボード
- 分析
- モニター
- ワークスペース
- フォーム
- 基本フォーム
- ステップフォーム
- 高度なフォーム
- リスト
- 標準テーブル
- 標準リスト
- カードリスト
- 検索リスト(プロジェクト/アプリケーション/記事)
- プロフィール
- シンプルプロフィール
- 高度なプロフィール
- アカウント
- アカウントセンター
- アカウント設定
- 結果
- 成功
- 失敗
- 例外
- 403
- 404
- 500
- ユーザー
- ログイン
- 登録
- 登録結果
```
## 使い方
### bashを使う
pro-cliを使って素早くプロジェクトを初期化できます。
```bash
# npmを使用
npm i @ant-design/pro-cli -g
pro create myapp
```
proテンプレートを選択します。simpleは基本テンプレートで、フレームワークの基本的な内容のみを提供します。completeはすべてのブロックを含み、二次開発のベースとしては適していません。
```shell
? 🚀 フル機能かシンプルなスキャフォールドか?(矢印キーで選択)
➥ simple
complete
```
Gitリポジトリを初期化:
```shell
$ git init myapp
```
依存関係をインストール:
```shell
$ cd myapp && tyarn
// または
$ cd myapp && npm install
```
## 対応ブラウザ
モダンブラウザ対応。
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --- | --- | --- | --- | --- |
| Edge | 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン |
## コントリビュート
どんな形の貢献も歓迎します。以下はこのプロジェクトに貢献する例です:
- 日常業務でAnt Design Proを使う
- [issues](http://github.com/ant-design/ant-design-pro/issues)でバグ報告や質問を投稿する
- [pull requests](http://github.com/ant-design/ant-design-pro/pulls)でコード改善を提案する

50
README.md

@ -1,4 +1,4 @@
Language: 🇺🇸 | [🇨🇳](./README.zh-CN.md) | [🇷🇺](./README.ru-RU.md) | [🇹🇷](./README.tr-TR.md) | [🇯🇵](./README.ja-JP.md) | [🇫🇷](./README.fr-FR.md) | [🇵🇹](./README.pt-BR.md) | [🇸🇦](./README.ar-DZ.md) | [🇪🇸](./README.es-ES.md)
Language: 🇺🇸 | [🇨🇳](./README.zh-CN.md)
<h1 align="center">Ant Design Pro</h1>
@ -74,36 +74,48 @@ An out-of-box UI solution for enterprise applications as a React boilerplate.
## Usage
### Use bash
### Get Started
We provide pro-cli to quickly initialize scaffolding.
Clone or download this repository to your local machine:
```bash
# use npm
npm i @ant-design/pro-cli -g
pro create myapp
git clone --depth=1 https://github.com/ant-design/ant-design-pro.git myapp
cd myapp
```
Choose the pro template. Simple is the basic template, which only provides the basic content of the framework operation. Complete contains all blocks, which is not suitable for secondary development as a basic template.
### Installation
```shell
? 🚀 Full or a simple scaffold? (Use arrow keys)
❯ simple
complete
```bash
npm install
```
### Development
```bash
# Start development server (full version by default)
npm start
```
Initialized Git repository:
### Simplify to Simple Version
```shell
$ git init myapp
This project includes all blocks by default. If you need a minimal version, run:
```bash
npm run simple
```
Install dependencies:
This will:
- Remove extra page directories (dashboard, form, list/*, profile, result, exception, account, etc.)
- Remove extra mock files
- Replace routes with simple version
- Remove extra dependencies from package.json
**Note**: This operation is irreversible and will permanently delete files.
```shell
$ cd myapp && tyarn
// or
$ cd myapp && npm install
### Build
```bash
npm run build
```
## Browsers support

125
README.pt-BR.md

@ -1,125 +0,0 @@
# Ant Design Pro
Idioma: 🇺🇸 | [🇨🇳](./README.zh-CN.md) | [🇷🇺](./README.ru-RU.md) | [🇹🇷](./README.tr-TR.md) | [🇯🇵](./README.ja-JP.md) | [🇫🇷](./README.fr-FR.md) | [🇧🇷](./README.pt-BR.md) | [🇩🇿](./README.ar-DZ.md) | [🇪🇸](./README.es-ES.md)
<h1 align="center">Ant Design Pro</h1>
<div align="center">
Uma solução de UI pronta para uso para aplicações empresariais baseada em React.
[![CI](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml)
[![Preview Deploy](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml)
[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/)
[![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev)
[![](https://badgen.net/badge/icon/Ant%20Design?icon=https://gw.alipayobjects.com/zos/antfincdn/Pp4WPgVDB3/KDpgvguMpGfqaHPjicRK.svg&label)](https://ant.design/)
<img width="1718" height="1191" alt="visualização do tema claro" src="https://github.com/user-attachments/assets/74ad0b4a-e086-4955-8edd-9f2cff31aee8" />
<img width="1718" height="1191" alt="visualização do tema escuro" src="https://github.com/user-attachments/assets/d4bcb7c1-42c7-4c0f-b130-1193a931f9f7" />
</div>
- Visualizar: http://preview.pro.ant.design
- Página inicial: http://pro.ant.design
- Documentação: http://pro.ant.design/docs/getting-started
- Registro de alterações: http://pro.ant.design/docs/changelog
- FAQ: http://pro.ant.design/docs/faq
## Funcionalidades
- :bulb: **TypeScript**: Uma linguagem para aplicações JavaScript em larga escala
- :scroll: **Blocos**: Construa páginas com modelos de blocos
- :gem: **Design elegante**: Segue a [especificação do Ant Design](http://ant.design/)
- :triangular_ruler: **Modelos comuns**: Modelos típicos para aplicações empresariais
- :rocket: **Desenvolvimento de ponta**: Stack mais recente de React/umi/dva/antd
- :iphone: **Responsivo**: Projetado para diferentes tamanhos de tela
- :art: **Temas**: Tema personalizável com configuração simples
- :globe_with_meridians: **Internacionalização**: Solução i18n integrada
- :gear: **Boas práticas**: Workflow sólido para manter seu código saudável
- :1234: **Desenvolvimento mock**: Solução de mock fácil de usar
- :white_check_mark: **Teste de UI**: Segurança com testes unitários e e2e
## Modelos
```
- Painel
- Analítico
- Monitoramento
- Espaço de trabalho
- Formulário
- Formulário básico
- Formulário em etapas
- Formulário avançado
- Lista
- Tabela padrão
- Lista padrão
- Lista de cartões
- Lista de busca (Projeto/Aplicações/Artigo)
- Perfil
- Perfil simples
- Perfil avançado
- Conta
- Central da conta
- Configurações da conta
- Resultado
- Sucesso
- Falha
- Exceção
- 403
- 404
- 500
- Usuário
- Login
- Registro
- Resultado do registro
```
## Uso
### Usando bash
Fornecemos o pro-cli para inicializar rapidamente o projeto.
```bash
# usar npm
npm i @ant-design/pro-cli -g
pro create myapp
```
Escolha o modelo pro. Simple é o modelo básico, que fornece apenas o conteúdo essencial para o funcionamento do framework. Complete contém todos os blocos, não sendo adequado como modelo base para desenvolvimento secundário.
```shell
? 🚀 Projeto completo ou um esqueleto simples? (Use as setas)
➥ simple
complete
```
Inicialize o repositório Git:
```shell
$ git init myapp
```
Instale as dependências:
```shell
$ cd myapp && tyarn
// ou
$ cd myapp && npm install
```
## Navegadores suportados
Navegadores modernos.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --- | --- | --- | --- | --- |
| Edge | últimas 2 versões | últimas 2 versões | últimas 2 versões | últimas 2 versões |
## Contribuindo
Qualquer tipo de contribuição é bem-vinda. Aqui estão alguns exemplos de como você pode contribuir para este projeto:
- Use o Ant Design Pro no seu trabalho diário.
- Envie [issues](http://github.com/ant-design/ant-design-pro/issues) para relatar bugs ou fazer perguntas.
- Proponha [pull requests](http://github.com/ant-design/ant-design-pro/pulls) para melhorar nosso código.

125
README.ru-RU.md

@ -1,125 +0,0 @@
# Ant Design Pro
Язык: 🇺🇸 | [🇨🇳](./README.zh-CN.md) | [🇷🇺](./README.ru-RU.md) | [🇹🇷](./README.tr-TR.md) | [🇯🇵](./README.ja-JP.md) | [🇫🇷](./README.fr-FR.md) | [🇧🇷](./README.pt-BR.md) | [🇩🇿](./README.ar-DZ.md) | [🇪🇸](./README.es-ES.md)
<h1 align="center">Ant Design Pro</h1>
<div align="center">
Готовое решение UI для корпоративных приложений на базе React.
[![CI](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml)
[![Preview Deploy](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml)
[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/)
[![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev)
[![](https://badgen.net/badge/icon/Ant%20Design?icon=https://gw.alipayobjects.com/zos/antfincdn/Pp4WPgVDB3/KDpgvguMpGfqaHPjicRK.svg&label)](https://ant.design/)
<img width="1718" height="1191" alt="светлая тема" src="https://github.com/user-attachments/assets/74ad0b4a-e086-4955-8edd-9f2cff31aee8" />
<img width="1718" height="1191" alt="тёмная тема" src="https://github.com/user-attachments/assets/d4bcb7c1-42c7-4c0f-b130-1193a931f9f7" />
</div>
- Превью: http://preview.pro.ant.design
- Главная страница: http://pro.ant.design
- Документация: http://pro.ant.design/docs/getting-started
- Список изменений: http://pro.ant.design/docs/changelog
- FAQ: http://pro.ant.design/docs/faq
## Возможности
- :bulb: **TypeScript**: Язык для масштабируемых JavaScript-приложений
- :scroll: **Блоки**: Построение страниц с помощью шаблонов блоков
- :gem: **Элегантный дизайн**: Следует [спецификации Ant Design](http://ant.design/)
- :triangular_ruler: **Типовые шаблоны**: Типовые шаблоны для корпоративных приложений
- :rocket: **Современный стек**: Самые новые технологии React/umi/dva/antd
- :iphone: **Адаптивность**: Поддержка разных размеров экранов
- :art: **Темизация**: Кастомизация темы через простую конфигурацию
- :globe_with_meridians: **Интернационализация**: Встроенное решение i18n
- :gear: **Лучшие практики**: Надёжный workflow для поддержания качества кода
- :1234: **Мок-разработка**: Удобное решение для разработки с мок-данными
- :white_check_mark: **UI-тесты**: Безопасность с помощью unit и e2e тестов
## Шаблоны
```
- Дашборд
- Аналитика
- Мониторинг
- Рабочее пространство
- Форма
- Базовая форма
- Многошаговая форма
- Продвинутая форма
- Список
- Стандартная таблица
- Стандартный список
- Список карточек
- Поисковый список (Проект/Приложения/Статья)
- Профиль
- Простой профиль
- Продвинутый профиль
- Аккаунт
- Центр аккаунта
- Настройки аккаунта
- Результат
- Успех
- Ошибка
- Исключения
- 403
- 404
- 500
- Пользователь
- Вход
- Регистрация
- Результат регистрации
```
## Использование
### Использование bash
Мы предоставляем pro-cli для быстрой инициализации проекта.
```bash
# использовать npm
npm i @ant-design/pro-cli -g
pro create myapp
```
Выберите шаблон pro. Simple — это базовый шаблон, который содержит только необходимый минимум для работы фреймворка. Complete включает все блоки и не подходит для вторичной разработки как базовый шаблон.
```shell
? 🚀 Полный или простой шаблон? (Используйте стрелки)
➥ simple
complete
```
Инициализация репозитория Git:
```shell
$ git init myapp
```
Установка зависимостей:
```shell
$ cd myapp && tyarn
// или
$ cd myapp && npm install
```
## Поддержка браузеров
Современные браузеры.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --- | --- | --- | --- | --- |
| Edge | последние 2 версии | последние 2 версии | последние 2 версии | последние 2 версии |
## Вклад
Любой вклад приветствуется. Вот несколько способов, как вы можете помочь проекту:
- Используйте Ant Design Pro в своей повседневной работе.
- Оставляйте [issues](http://github.com/ant-design/ant-design-pro/issues) для сообщений об ошибках или вопросов.
- Предлагайте [pull requests](http://github.com/ant-design/ant-design-pro/pulls) для улучшения кода.

125
README.tr-TR.md

@ -1,125 +0,0 @@
# Ant Design Pro
Dil: 🇺🇸 | [🇨🇳](./README.zh-CN.md) | [🇷🇺](./README.ru-RU.md) | [🇹🇷](./README.tr-TR.md) | [🇯🇵](./README.ja-JP.md) | [🇫🇷](./README.fr-FR.md) | [🇧🇷](./README.pt-BR.md) | [🇩🇿](./README.ar-DZ.md) | [🇪🇸](./README.es-ES.md)
<h1 align="center">Ant Design Pro</h1>
<div align="center">
React tabanlı kurumsal uygulamalar için kutudan çıkan bir UI çözümü.
[![CI](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml)
[![Preview Deploy](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml/badge.svg)](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml)
[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/)
[![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev)
[![](https://badgen.net/badge/icon/Ant%20Design?icon=https://gw.alipayobjects.com/zos/antfincdn/Pp4WPgVDB3/KDpgvguMpGfqaHPjicRK.svg&label)](https://ant.design/)
<img width="1718" height="1191" alt="açık tema önizlemesi" src="https://github.com/user-attachments/assets/74ad0b4a-e086-4955-8edd-9f2cff31aee8" />
<img width="1718" height="1191" alt="koyu tema önizlemesi" src="https://github.com/user-attachments/assets/d4bcb7c1-42c7-4c0f-b130-1193a931f9f7" />
</div>
- Önizleme: http://preview.pro.ant.design
- Ana Sayfa: http://pro.ant.design
- Dokümantasyon: http://pro.ant.design/docs/getting-started
- Değişiklik Günlüğü: http://pro.ant.design/docs/changelog
- SSS: http://pro.ant.design/docs/faq
## Özellikler
- :bulb: **TypeScript**: Büyük ölçekli JavaScript uygulamaları için bir dil
- :scroll: **Bloklar**: Blok şablonlarıyla sayfa oluşturun
- :gem: **Şık Tasarım**: [Ant Design spesifikasyonuna](http://ant.design/) uygun
- :triangular_ruler: **Yaygın Şablonlar**: Kurumsal uygulamalar için tipik şablonlar
- :rocket: **En Yeni Geliştirme**: React/umi/dva/antd'nin en yeni geliştirme yığını
- :iphone: **Duyarlı**: Farklı ekran boyutları için tasarlandı
- :art: **Tema**: Basit yapılandırmayla özelleştirilebilir tema
- :globe_with_meridians: **Uluslararasılaştırma**: Dahili i18n çözümü
- :gear: **En İyi Uygulamalar**: Kodunuzu sağlıklı tutmak için sağlam iş akışı
- :1234: **Mock geliştirme**: Kullanımı kolay mock geliştirme çözümü
- :white_check_mark: **UI Testi**: Birim ve e2e testleriyle güvenli geliştirme
## Şablonlar
```
- Gösterge Paneli
- Analitik
- İzleme
- Çalışma Alanı
- Form
- Temel Form
- Adım Adım Form
- Gelişmiş Form
- Liste
- Standart Tablo
- Standart Liste
- Kart Listesi
- Arama Listesi (Proje/Uygulamalar/Makale)
- Profil
- Basit Profil
- Gelişmiş Profil
- Hesap
- Hesap Merkezi
- Hesap Ayarları
- Sonuç
- Başarılı
- Başarısız
- İstisna
- 403
- 404
- 500
- Kullanıcı
- Giriş
- Kayıt Ol
- Kayıt Sonucu
```
## Kullanım
### Bash kullanımı
Projeyi hızlıca başlatmak için pro-cli sağlıyoruz.
```bash
# npm kullan
npm i @ant-design/pro-cli -g
pro create myapp
```
Pro şablonunu seçin. Simple, yalnızca temel framework içeriğini sağlayan temel şablondur. Complete, tüm blokları içerir ve ikincil geliştirme için temel şablon olarak uygun değildir.
```shell
? 🚀 Tam veya basit bir iskelet mi? (Ok tuşlarını kullanın)
➥ simple
complete
```
Git deposunu başlatın:
```shell
$ git init myapp
```
Bağımlılıkları yükleyin:
```shell
$ cd myapp && tyarn
// veya
$ cd myapp && npm install
```
## Desteklenen Tarayıcılar
Modern tarayıcılar.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --- | --- | --- | --- | --- |
| Edge | son 2 sürüm | son 2 sürüm | son 2 sürüm | son 2 sürüm |
## Katkı
Her türlü katkı memnuniyetle karşılanır. Bu projeye katkıda bulunmanın bazı yolları şunlardır:
- Ant Design Pro'yu günlük işinizde kullanın.
- Hataları bildirmek veya soru sormak için [issues](http://github.com/ant-design/ant-design-pro/issues) gönderin.
- Kodumuzu geliştirmek için [pull requests](http://github.com/ant-design/ant-design-pro/pulls) önerin.

50
README.zh-CN.md

@ -1,4 +1,4 @@
Language : [🇺🇸](./README.md) | 🇨🇳 | [🇷🇺](./README.ru-RU.md) | [🇹🇷](./README.tr-TR.md) | [🇯🇵](./README.ja-JP.md) | [🇫🇷](./README.fr-FR.md) | [🇵🇹](./README.pt-BR.md) | [🇸🇦](./README.ar-DZ.md)
Language : [🇺🇸](./README.md) | 🇨🇳
<h1 align="center">Ant Design Pro</h1>
@ -77,28 +77,48 @@ Language : [🇺🇸](./README.md) | 🇨🇳 | [🇷🇺](./README.ru-RU.md) |
## 使用
我们提供了 pro-cli 来快速的初始化脚手架。
### 开始使用
克隆或下载本项目到本地:
```bash
git clone --depth=1 https://github.com/ant-design/ant-design-pro.git myapp
cd myapp
```
### 安装依赖
```bash
npm install
```
### 开发
```bash
# 使用 npm
npm i @ant-design/pro-cli -g
pro create myapp
# 启动开发服务器(默认为完整版)
npm start
```
选择 pro 的模板,simple 是基础模板,只提供了框架运行的基本内容,complete 包含所有区块,不太适合当基础模板来进行二次开发。
### 精简为简单版本
```shell
? 🚀 要全量的还是一个简单的脚手架? (Use arrow keys)
❯ simple
complete
本项目默认包含所有区块。如果你需要一个最小化的版本,运行:
```bash
npm run simple
```
安装依赖:
这将会:
- 删除多余的页面目录(dashboard、form、list/*、profile、result、exception、account 等)
- 删除多余的 mock 文件
- 替换路由为简单版本
- 从 package.json 中移除多余的依赖
```shell
$ cd myapp && tyarn
// 或
$ cd myapp && npm install
**注意**:此操作不可逆,将永久删除文件。
### 构建
```bash
npm run build
```
更多信息请参考 [使用文档](http://pro.ant.design/docs/getting-started)。

3
biome.json

@ -27,7 +27,8 @@
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "off"
"noExplicitAny": "off",
"noUnknownAtRules": "off"
},
"correctness": {
"useUniqueElementIds": "off",

3
config/config.ts

@ -143,6 +143,7 @@ export default defineConfig({
// 解决首次加载时白屏的问题
{ src: join(PUBLIC_PATH, 'scripts/loading.js'), async: true },
],
//================ pro 插件配置 =================
presets: ['umi-presets-pro'],
/**
@ -165,6 +166,7 @@ export default defineConfig({
projectName: 'swagger',
},
],
mock: {
include: ['mock/**/*', 'src/pages/**/_mock.ts'],
},
@ -174,4 +176,5 @@ export default defineConfig({
define: {
'process.env.CI': process.env.CI,
},
tailwindcss: {},
});

55
config/routes.simple.ts

@ -0,0 +1,55 @@
/**
* @name
* @description npm run simple 使
*/
export default [
{
path: '/user',
layout: false,
routes: [
{
name: 'login',
path: '/user/login',
component: './user/login',
},
],
},
{
path: '/welcome',
name: 'welcome',
icon: 'smile',
component: './Welcome',
},
{
path: '/admin',
name: 'admin',
icon: 'crown',
access: 'canAdmin',
routes: [
{
path: '/admin',
redirect: '/admin/sub-page',
},
{
path: '/admin/sub-page',
name: 'sub-page',
component: './Admin',
},
],
},
{
name: 'list.table-list',
icon: 'table',
path: '/list',
component: './table-list',
},
{
path: '/',
redirect: '/welcome',
},
{
component: '404',
layout: false,
path: './*',
},
];

242
config/routes.ts

@ -1,4 +1,4 @@
/**
/**
* @name umi
* @description path,component,routes,redirect,wrappers,name,icon
* @param path path 第一种是动态参数 :id *
@ -16,10 +16,31 @@ export default [
layout: false,
routes: [
{
name: 'login',
path: '/user/login',
layout: false,
name: 'login',
component: './user/login',
},
{
path: '/user',
redirect: '/user/login',
},
{
name: 'register-result',
icon: 'smile',
path: '/user/register-result',
component: './user/register-result',
},
{
name: 'register',
icon: 'smile',
path: '/user/register',
component: './user/register',
},
{
component: '404',
path: '/user/*',
},
],
},
{
@ -46,18 +67,225 @@ export default [
],
},
{
name: 'list.table-list',
icon: 'table',
path: '/dashboard',
name: 'dashboard',
icon: 'dashboard',
routes: [
{
path: '/dashboard',
redirect: '/dashboard/analysis',
},
{
name: 'analysis',
icon: 'smile',
path: '/dashboard/analysis',
component: './dashboard/analysis',
},
{
name: 'monitor',
icon: 'smile',
path: '/dashboard/monitor',
component: './dashboard/monitor',
},
{
name: 'workplace',
icon: 'smile',
path: '/dashboard/workplace',
component: './dashboard/workplace',
},
],
},
{
path: '/form',
icon: 'form',
name: 'form',
routes: [
{
path: '/form',
redirect: '/form/basic-form',
},
{
name: 'basic-form',
icon: 'smile',
path: '/form/basic-form',
component: './form/basic-form',
},
{
name: 'step-form',
icon: 'smile',
path: '/form/step-form',
component: './form/step-form',
},
{
name: 'advanced-form',
icon: 'smile',
path: '/form/advanced-form',
component: './form/advanced-form',
},
],
},
{
path: '/list',
component: './table-list',
icon: 'table',
name: 'list',
routes: [
{
path: '/list/search',
name: 'search-list',
component: './list/search',
routes: [
{
path: '/list/search',
redirect: '/list/search/articles',
},
{
name: 'articles',
icon: 'smile',
path: '/list/search/articles',
component: './list/search/articles',
},
{
name: 'projects',
icon: 'smile',
path: '/list/search/projects',
component: './list/search/projects',
},
{
name: 'applications',
icon: 'smile',
path: '/list/search/applications',
component: './list/search/applications',
},
],
},
{
path: '/list',
redirect: '/list/table-list',
},
{
name: 'table-list',
icon: 'smile',
path: '/list/table-list',
component: './table-list',
},
{
name: 'basic-list',
icon: 'smile',
path: '/list/basic-list',
component: './list/basic-list',
},
{
name: 'card-list',
icon: 'smile',
path: '/list/card-list',
component: './list/card-list',
},
],
},
{
path: '/profile',
name: 'profile',
icon: 'profile',
routes: [
{
path: '/profile',
redirect: '/profile/basic',
},
{
name: 'basic',
icon: 'smile',
path: '/profile/basic',
component: './profile/basic',
},
{
name: 'advanced',
icon: 'smile',
path: '/profile/advanced',
component: './profile/advanced',
},
],
},
{
name: 'result',
icon: 'CheckCircleOutlined',
path: '/result',
routes: [
{
path: '/result',
redirect: '/result/success',
},
{
name: 'success',
icon: 'smile',
path: '/result/success',
component: './result/success',
},
{
name: 'fail',
icon: 'smile',
path: '/result/fail',
component: './result/fail',
},
],
},
{
name: 'exception',
icon: 'warning',
path: '/exception',
routes: [
{
path: '/exception',
redirect: '/exception/403',
},
{
name: '403',
icon: 'smile',
path: '/exception/403',
component: './exception/403',
},
{
name: '404',
icon: 'smile',
path: '/exception/404',
component: './exception/404',
},
{
name: '500',
icon: 'smile',
path: '/exception/500',
component: './exception/500',
},
],
},
{
name: 'account',
icon: 'user',
path: '/account',
routes: [
{
path: '/account',
redirect: '/account/center',
},
{
name: 'center',
icon: 'smile',
path: '/account/center',
component: './account/center',
},
{
name: 'settings',
icon: 'smile',
path: '/account/settings',
component: './account/settings',
},
],
},
{
path: '/',
redirect: '/welcome',
redirect: '/dashboard/analysis',
},
{
component: '404',
layout: false,
path: './*',
},
];

2
jest.config.ts

@ -1,4 +1,4 @@
import { configUmiAlias, createConfig } from '@umijs/max/test';
import { configUmiAlias, createConfig } from '@umijs/max/test.js';
export default async (): Promise<any> => {
const config = await configUmiAlias({

210
mock/analysis.mock.ts

@ -0,0 +1,210 @@
import dayjs from 'dayjs';
import type { Request, Response } from 'express';
import type { AnalysisData, DataItem, RadarData } from '../src/pages/dashboard/analysis/data';
// mock data
const visitData: DataItem[] = [];
const beginDay = new Date().getTime();
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
for (let i = 0; i < fakeY.length; i += 1) {
visitData.push({
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: fakeY[i],
});
}
const visitData2 = [];
const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
for (let i = 0; i < fakeY2.length; i += 1) {
visitData2.push({
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: fakeY2[i],
});
}
const salesData = [];
for (let i = 0; i < 12; i += 1) {
salesData.push({
x: `${i + 1}`,
y: Math.floor(Math.random() * 1000) + 200,
});
}
const searchData = [];
for (let i = 0; i < 50; i += 1) {
searchData.push({
index: i + 1,
keyword: `搜索关键词-${i}`,
count: Math.floor(Math.random() * 1000),
range: Math.floor(Math.random() * 100),
status: Math.floor((Math.random() * 10) % 2),
});
}
const salesTypeData = [
{
x: '家用电器',
y: 4544,
},
{
x: '食用酒水',
y: 3321,
},
{
x: '个护健康',
y: 3113,
},
{
x: '服饰箱包',
y: 2341,
},
{
x: '母婴产品',
y: 1231,
},
{
x: '其他',
y: 1231,
},
];
const salesTypeDataOnline = [
{
x: '家用电器',
y: 244,
},
{
x: '食用酒水',
y: 321,
},
{
x: '个护健康',
y: 311,
},
{
x: '服饰箱包',
y: 41,
},
{
x: '母婴产品',
y: 121,
},
{
x: '其他',
y: 111,
},
];
const salesTypeDataOffline = [
{
x: '家用电器',
y: 99,
},
{
x: '食用酒水',
y: 188,
},
{
x: '个护健康',
y: 344,
},
{
x: '服饰箱包',
y: 255,
},
{
x: '其他',
y: 65,
},
];
const offlineData = [];
for (let i = 0; i < 10; i += 1) {
offlineData.push({
name: `Stores ${i}`,
cvr: Math.ceil(Math.random() * 9) / 10,
});
}
const offlineChartData = [];
for (let i = 0; i < 20; i += 1) {
const date = dayjs(new Date().getTime() + 1000 * 60 * 30 * i).format('HH:mm');
offlineChartData.push({
date,
type: '客流量',
value: Math.floor(Math.random() * 100) + 10,
});
offlineChartData.push({
date,
type: '支付笔数',
value: Math.floor(Math.random() * 100) + 10,
});
}
const radarOriginData = [
{
name: '个人',
ref: 10,
koubei: 8,
output: 4,
contribute: 5,
hot: 7,
},
{
name: '团队',
ref: 3,
koubei: 9,
output: 6,
contribute: 3,
hot: 1,
},
{
name: '部门',
ref: 4,
koubei: 1,
output: 6,
contribute: 5,
hot: 7,
},
];
const radarData: RadarData[] = [];
const radarTitleMap = {
ref: '引用',
koubei: '口碑',
output: '产量',
contribute: '贡献',
hot: '热度',
};
radarOriginData.forEach((item) => {
Object.keys(item).forEach((key) => {
if (key !== 'name') {
radarData.push({
name: item.name,
label: radarTitleMap[key as 'ref'],
value: item[key as 'ref'],
});
}
});
});
const getFakeChartData: AnalysisData = {
visitData,
visitData2,
salesData,
searchData,
offlineData,
offlineChartData,
salesTypeData,
salesTypeDataOnline,
salesTypeDataOffline,
radarData,
};
const fakeChartData = (_: Request, res: Response) => {
return res.json({
data: getFakeChartData,
});
};
export default {
'GET /api/fake_analysis_chart_data': fakeChartData,
};

38
mock/listTableList.ts

@ -34,10 +34,7 @@ let tableListDataSource = genList(1, 100);
function getRule(req: Request, res: Response, u: string) {
let realUrl = u;
if (
!realUrl ||
Object.prototype.toString.call(realUrl) !== '[object String]'
) {
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const { current = 1, pageSize = 10 } = req.query;
@ -81,25 +78,21 @@ function getRule(req: Request, res: Response, u: string) {
};
if (Object.keys(filter).length > 0) {
dataSource = dataSource.filter((item) => {
return (Object.keys(filter) as Array<keyof API.RuleListItem>).some(
(key) => {
if (!filter[key]) {
return true;
}
if (filter[key].includes(`${item[key]}`)) {
return true;
}
return false;
},
);
return (Object.keys(filter) as Array<keyof API.RuleListItem>).some((key) => {
if (!filter[key]) {
return true;
}
if (filter[key].includes(`${item[key]}`)) {
return true;
}
return false;
});
});
}
}
if (params.name) {
dataSource = dataSource.filter((data) =>
data?.name?.includes(params.name || ''),
);
dataSource = dataSource.filter((data) => data?.name?.includes(params.name || ''));
}
const result = {
data: dataSource,
@ -114,10 +107,7 @@ function getRule(req: Request, res: Response, u: string) {
function postRule(req: Request, res: Response, u: string, b: Request) {
let realUrl = u;
if (
!realUrl ||
Object.prototype.toString.call(realUrl) !== '[object String]'
) {
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
@ -126,9 +116,7 @@ function postRule(req: Request, res: Response, u: string, b: Request) {
switch (method) {
case 'delete':
tableListDataSource = tableListDataSource.filter(
(item) => key.indexOf(item.key) === -1,
);
tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1);
break;
case 'post':
(() => {

6
mock/notices.ts

@ -85,8 +85,7 @@ const getNotices = (_req: Request, res: Response) => {
{
id: '000000010',
title: '第三方紧急代码变更',
description:
'冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '马上到期',
status: 'urgent',
type: 'event',
@ -102,8 +101,7 @@ const getNotices = (_req: Request, res: Response) => {
{
id: '000000012',
title: 'ABCD 版本发布',
description:
'冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '进行中',
status: 'processing',
type: 'event',

63
mock/requestRecord.mock.js

@ -2,8 +2,7 @@ module.exports = {
'GET /api/currentUser': {
data: {
name: 'Serati Ma',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
@ -34,8 +33,7 @@ module.exports = {
key: 99,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 99',
owner: '曲丽丽',
desc: '这是一段描述',
@ -49,8 +47,7 @@ module.exports = {
key: 98,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 98',
owner: '曲丽丽',
desc: '这是一段描述',
@ -64,8 +61,7 @@ module.exports = {
key: 97,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 97',
owner: '曲丽丽',
desc: '这是一段描述',
@ -79,8 +75,7 @@ module.exports = {
key: 96,
disabled: true,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 96',
owner: '曲丽丽',
desc: '这是一段描述',
@ -94,8 +89,7 @@ module.exports = {
key: 95,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 95',
owner: '曲丽丽',
desc: '这是一段描述',
@ -109,8 +103,7 @@ module.exports = {
key: 94,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 94',
owner: '曲丽丽',
desc: '这是一段描述',
@ -124,8 +117,7 @@ module.exports = {
key: 93,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 93',
owner: '曲丽丽',
desc: '这是一段描述',
@ -139,8 +131,7 @@ module.exports = {
key: 92,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 92',
owner: '曲丽丽',
desc: '这是一段描述',
@ -154,8 +145,7 @@ module.exports = {
key: 91,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 91',
owner: '曲丽丽',
desc: '这是一段描述',
@ -169,8 +159,7 @@ module.exports = {
key: 90,
disabled: true,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 90',
owner: '曲丽丽',
desc: '这是一段描述',
@ -184,8 +173,7 @@ module.exports = {
key: 89,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 89',
owner: '曲丽丽',
desc: '这是一段描述',
@ -199,8 +187,7 @@ module.exports = {
key: 88,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 88',
owner: '曲丽丽',
desc: '这是一段描述',
@ -214,8 +201,7 @@ module.exports = {
key: 87,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 87',
owner: '曲丽丽',
desc: '这是一段描述',
@ -229,8 +215,7 @@ module.exports = {
key: 86,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 86',
owner: '曲丽丽',
desc: '这是一段描述',
@ -244,8 +229,7 @@ module.exports = {
key: 85,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 85',
owner: '曲丽丽',
desc: '这是一段描述',
@ -259,8 +243,7 @@ module.exports = {
key: 84,
disabled: true,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 84',
owner: '曲丽丽',
desc: '这是一段描述',
@ -274,8 +257,7 @@ module.exports = {
key: 83,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 83',
owner: '曲丽丽',
desc: '这是一段描述',
@ -289,8 +271,7 @@ module.exports = {
key: 82,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 82',
owner: '曲丽丽',
desc: '这是一段描述',
@ -304,8 +285,7 @@ module.exports = {
key: 81,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 81',
owner: '曲丽丽',
desc: '这是一段描述',
@ -319,8 +299,7 @@ module.exports = {
key: 80,
disabled: false,
href: 'https://ant.design',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 80',
owner: '曲丽丽',
desc: '这是一段描述',

6
mock/user.ts

@ -20,8 +20,7 @@ const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env;
* current user access if is '', user need login
* pro
*/
let access =
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ? 'admin' : '';
let access = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ? 'admin' : '';
const getAccess = () => {
return access;
@ -46,8 +45,7 @@ export default {
success: true,
data: {
name: 'Serati Ma',
avatar:
'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',

418
mock/workplace.mock.ts

@ -0,0 +1,418 @@
import dayjs from 'dayjs';
import type { Request, Response } from 'express';
import type { DataItem, OfflineDataType } from '../src/pages/dashboard/workplace/data.d';
export type SearchDataType = {
index: number;
keyword: string;
count: number;
range: number;
status: number;
};
// mock data
const visitData: DataItem[] = [];
const beginDay = new Date().getTime();
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
for (let i = 0; i < fakeY.length; i += 1) {
visitData.push({
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: fakeY[i],
});
}
const visitData2: DataItem[] = [];
const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
for (let i = 0; i < fakeY2.length; i += 1) {
visitData2.push({
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: fakeY2[i],
});
}
const salesData: DataItem[] = [];
for (let i = 0; i < 12; i += 1) {
salesData.push({
x: `${i + 1}`,
y: Math.floor(Math.random() * 1000) + 200,
});
}
const searchData: SearchDataType[] = [];
for (let i = 0; i < 50; i += 1) {
searchData.push({
index: i + 1,
keyword: `搜索关键词-${i}`,
count: Math.floor(Math.random() * 1000),
range: Math.floor(Math.random() * 100),
status: Math.floor((Math.random() * 10) % 2),
});
}
const salesTypeData = [
{
x: '家用电器',
y: 4544,
},
{
x: '食用酒水',
y: 3321,
},
{
x: '个护健康',
y: 3113,
},
{
x: '服饰箱包',
y: 2341,
},
{
x: '母婴产品',
y: 1231,
},
{
x: '其他',
y: 1231,
},
];
const salesTypeDataOnline = [
{
x: '家用电器',
y: 244,
},
{
x: '食用酒水',
y: 321,
},
{
x: '个护健康',
y: 311,
},
{
x: '服饰箱包',
y: 41,
},
{
x: '母婴产品',
y: 121,
},
{
x: '其他',
y: 111,
},
];
const salesTypeDataOffline = [
{
x: '家用电器',
y: 99,
},
{
x: '食用酒水',
y: 188,
},
{
x: '个护健康',
y: 344,
},
{
x: '服饰箱包',
y: 255,
},
{
x: '其他',
y: 65,
},
];
const offlineData: OfflineDataType[] = [];
for (let i = 0; i < 10; i += 1) {
offlineData.push({
name: `Stores ${i}`,
cvr: Math.ceil(Math.random() * 9) / 10,
});
}
const offlineChartData: DataItem[] = [];
for (let i = 0; i < 20; i += 1) {
offlineChartData.push({
x: new Date().getTime() + 1000 * 60 * 30 * i,
y1: Math.floor(Math.random() * 100) + 10,
y2: Math.floor(Math.random() * 100) + 10,
});
}
const titles = [
'Alipay',
'Angular',
'Ant Design',
'Ant Design Pro',
'Bootstrap',
'React',
'Vue',
'Webpack',
];
const avatars = [
'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
];
const avatars2 = [
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png',
'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png',
'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png',
'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png',
'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png',
'https://gw.alipayobjects.com/zos/rmsportal/psOgztMplJMGpVEqfcgF.png',
'https://gw.alipayobjects.com/zos/rmsportal/ZpBqSxLxVEXfcUNoPKrz.png',
'https://gw.alipayobjects.com/zos/rmsportal/laiEnJdGHVOhJrUShBaJ.png',
'https://gw.alipayobjects.com/zos/rmsportal/UrQsqscbKEpNuJcvBZBu.png',
];
const getNotice = (_: Request, res: Response) => {
res.json({
data: [
{
id: 'xxx1',
title: titles[0],
logo: avatars[0],
description: '那是一种内在的东西,他们到达不了,也无法触及的',
updatedAt: new Date(),
member: '科学搬砖组',
href: '',
memberLink: '',
},
{
id: 'xxx2',
title: titles[1],
logo: avatars[1],
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
updatedAt: new Date('2017-07-24'),
member: '全组都是吴彦祖',
href: '',
memberLink: '',
},
{
id: 'xxx3',
title: titles[2],
logo: avatars[2],
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
updatedAt: new Date(),
member: '中二少女团',
href: '',
memberLink: '',
},
{
id: 'xxx4',
title: titles[3],
logo: avatars[3],
description: '那时候我只会想自己想要什么,从不想自己拥有什么',
updatedAt: new Date('2017-07-23'),
member: '程序员日常',
href: '',
memberLink: '',
},
{
id: 'xxx5',
title: titles[4],
logo: avatars[4],
description: '凛冬将至',
updatedAt: new Date('2017-07-23'),
member: '高逼格设计天团',
href: '',
memberLink: '',
},
{
id: 'xxx6',
title: titles[5],
logo: avatars[5],
description: '生命就像一盒巧克力,结果往往出人意料',
updatedAt: new Date('2017-07-23'),
member: '骗你来学计算机',
href: '',
memberLink: '',
},
],
});
};
const getActivities = (_: Request, res: Response) => {
res.json({
data: [
{
id: 'trend-1',
updatedAt: new Date(),
user: {
name: '曲丽丽',
avatar: avatars2[0],
},
group: {
name: '高逼格设计天团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-2',
updatedAt: new Date(),
user: {
name: '付小小',
avatar: avatars2[1],
},
group: {
name: '高逼格设计天团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-3',
updatedAt: new Date(),
user: {
name: '林东东',
avatar: avatars2[2],
},
group: {
name: '中二少女团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-4',
updatedAt: new Date(),
user: {
name: '周星星',
avatar: avatars2[4],
},
project: {
name: '5 月日常迭代',
link: 'http://github.com/',
},
template: '将 @{project} 更新至已发布状态',
},
{
id: 'trend-5',
updatedAt: new Date(),
user: {
name: '朱偏右',
avatar: avatars2[3],
},
project: {
name: '工程效能',
link: 'http://github.com/',
},
comment: {
name: '留言',
link: 'http://github.com/',
},
template: '在 @{project} 发布了 @{comment}',
},
{
id: 'trend-6',
updatedAt: new Date(),
user: {
name: '乐哥',
avatar: avatars2[5],
},
group: {
name: '程序员日常',
link: 'http://github.com/',
},
project: {
name: '品牌迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
],
});
};
const radarOriginData = [
{
name: '个人',
ref: 10,
koubei: 8,
output: 4,
contribute: 5,
hot: 7,
},
{
name: '团队',
ref: 3,
koubei: 9,
output: 6,
contribute: 3,
hot: 1,
},
{
name: '部门',
ref: 4,
koubei: 1,
output: 6,
contribute: 5,
hot: 7,
},
];
const radarData: any[] = [];
const radarTitleMap = {
ref: '引用',
koubei: '口碑',
output: '产量',
contribute: '贡献',
hot: '热度',
};
radarOriginData.forEach((item) => {
Object.keys(item).forEach((key) => {
if (key !== 'name') {
radarData.push({
name: item.name,
label: radarTitleMap[key as 'ref'],
value: item[key as 'ref'],
});
}
});
});
const getChartData = (_: Request, res: Response) => {
res.json({
data: {
visitData,
visitData2,
salesData,
searchData,
offlineData,
offlineChartData,
salesTypeData,
salesTypeDataOnline,
salesTypeDataOffline,
radarData,
},
});
};
export default {
'GET /api/project/notice': getNotice,
'GET /api/activities': getActivities,
'GET /api/fake_workplace_chart_data': getChartData,
};

18
package.json

@ -10,17 +10,19 @@
"deploy": "npm run build && npm run gh-pages",
"dev": "npm run start:dev",
"gh-pages": "gh-pages -d dist",
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
"i18n-remove": "max i18n-remove --locale=zh-CN --write",
"postinstall": "max setup",
"jest": "jest",
"lint": "npm run biome:lint && npm run tsc",
"lint-staged": "lint-staged",
"biome:lint": "npx @biomejs/biome lint",
"biome": "biome check --write",
"openapi": "max openapi",
"prepare": "husky",
"preview": "npm run build && max preview --port 8000",
"record": "cross-env NODE_ENV=development UMI_ENV=test max record --scene=login",
"serve": "umi-serve",
"simple": "node scripts/simple.js",
"start": "cross-env UMI_ENV=dev max dev",
"start:dev": "cross-env UMI_ENV=dev MOCK=none max dev",
"start:no-mock": "cross-env MOCK=none max dev",
@ -36,22 +38,29 @@
],
"dependencies": {
"@ant-design/icons": "^6.1.0",
"@ant-design/plots": "^2.6.0",
"@ant-design/pro-components": "3.1.2-0",
"@antv/l7": "^2.22.7",
"@antv/l7-react": "^2.4.3",
"@rc-component/util": "^1.9.0",
"antd": "^6.2.2",
"antd-style": "^4.1.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.19",
"numeral": "^2.0.6",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@ant-design/pro-cli": "^3.3.0",
"@biomejs/biome": "^2.1.1",
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.3.0",
"@types/express": "^5.0.3",
"@types/jest": "^30.0.0",
"@types/node": "^25.0.3",
"@types/numeral": "^2.0.5",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@types/react-helmet": "^6.1.11",
@ -65,12 +74,11 @@
"jest-environment-jsdom": "^30.0.5",
"lint-staged": "^16.1.2",
"mockjs": "^1.1.0",
"tailwindcss": "^3",
"ts-node": "^10.9.2",
"typescript": "^5.6.3",
"umi-presets-pro": "^2.0.3",
"umi-serve": "^1.9.11",
"@biomejs/biome": "^2.1.1",
"@types/node": "^25.0.3"
"umi-serve": "^1.9.11"
},
"engines": {
"node": ">=20.0.0"

2
public/scripts/loading.js

@ -2,7 +2,7 @@
* loading 占位
* 解决首次加载时白屏的问题
*/
(function () {
(() => {
const _root = document.querySelector('#root');
if (_root && _root.innerHTML === '') {
_root.innerHTML = `

165
scripts/simple.js

@ -0,0 +1,165 @@
/**
* 精简脚本 - 将完整版精简为简单版
* 执行 npm run simple 运行此脚本
*
* 此操作不可逆会删除以下内容
* - 页面目录dashboard, form, list/basic-list, list/card-list, list/search, profile, result, exception, account, user/register, user/register-result
* - Mock 文件analysis.mock.ts, workplace.mock.ts
* - 替换路由配置为简单版
*/
const fs = require('node:fs');
const path = require('node:path');
// 需要删除的页面目录
const pageDirsToDelete = [
'src/pages/dashboard',
'src/pages/form',
'src/pages/list/basic-list',
'src/pages/list/card-list',
'src/pages/list/search',
'src/pages/profile',
'src/pages/result',
'src/pages/exception',
'src/pages/account',
'src/pages/user/register',
'src/pages/user/register-result',
];
// 需要删除的 mock 文件
const mockFilesToDelete = ['mock/analysis.mock.ts', 'mock/workplace.mock.ts'];
// 需要从 package.json 移除的依赖
const depsToRemove = [
'@ant-design/plots',
'@antv/l7-react',
'@antv/l7',
'numeral',
];
const devDepsToRemove = ['@types/numeral'];
// 递归删除目录
function deleteDir(dirPath) {
if (fs.existsSync(dirPath)) {
fs.rmSync(dirPath, { recursive: true, force: true });
console.log(`✓ 已删除目录: ${dirPath}`);
} else {
console.log(`- 目录不存在,跳过: ${dirPath}`);
}
}
// 删除文件
function deleteFile(filePath) {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
console.log(`✓ 已删除文件: ${filePath}`);
} else {
console.log(`- 文件不存在,跳过: ${filePath}`);
}
}
// 替换路由配置
function replaceRoutes() {
const simpleRoutesPath = 'config/routes.simple.ts';
const routesPath = 'config/routes.ts';
if (fs.existsSync(simpleRoutesPath)) {
// 读取简单版路由
const simpleRoutes = fs.readFileSync(simpleRoutesPath, 'utf-8');
// 写入到 routes.ts
fs.writeFileSync(routesPath, simpleRoutes);
console.log(`✓ 已替换路由配置: ${routesPath}`);
// 删除简单版路由备份文件
fs.unlinkSync(simpleRoutesPath);
console.log(`✓ 已删除备份文件: ${simpleRoutesPath}`);
} else {
console.log(`- 简单版路由配置不存在,跳过: ${simpleRoutesPath}`);
}
}
// 更新 package.json
function updatePackageJson() {
const pkgPath = 'package.json';
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
let modified = false;
// 移除 dependencies
if (pkg.dependencies) {
for (const dep of depsToRemove) {
if (pkg.dependencies[dep]) {
delete pkg.dependencies[dep];
console.log(`✓ 已移除依赖: ${dep}`);
modified = true;
}
}
}
// 移除 devDependencies
if (pkg.devDependencies) {
for (const dep of devDepsToRemove) {
if (pkg.devDependencies[dep]) {
delete pkg.devDependencies[dep];
console.log(`✓ 已移除开发依赖: ${dep}`);
modified = true;
}
}
}
// 移除 simple 脚本
if (pkg.scripts?.simple) {
delete pkg.scripts.simple;
console.log('✓ 已移除 simple 脚本');
modified = true;
}
if (modified) {
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
console.log('✓ 已更新 package.json');
} else {
console.log('- package.json 无需更新');
}
}
// 主函数
function main() {
console.log('========================================');
console.log(' 开始执行精简脚本');
console.log('========================================\n');
console.log('>>> 删除页面目录...');
for (const dir of pageDirsToDelete) {
deleteDir(dir);
}
console.log('\n>>> 删除 mock 文件...');
for (const file of mockFilesToDelete) {
deleteFile(file);
}
console.log('\n>>> 替换路由配置...');
replaceRoutes();
console.log('\n>>> 更新 package.json...');
updatePackageJson();
// 删除自身
console.log('\n>>> 清理精简脚本...');
fs.unlinkSync(__filename);
console.log('✓ 已删除 scripts/simple.js');
// 尝试删除 scripts 目录(如果为空)
const scriptsDir = path.dirname(__filename);
if (fs.readdirSync(scriptsDir).length === 0) {
fs.rmdirSync(scriptsDir);
console.log('✓ 已删除空的 scripts 目录');
}
console.log('\n========================================');
console.log(' 精简完成!');
console.log(' 请运行 npm install 更新依赖');
console.log('========================================');
}
main();

3
src/locales/bn-BD.ts

@ -12,9 +12,6 @@ export default {
'layout.user.link.privacy': 'গোপনীয়তা',
'layout.user.link.terms': 'শর্তাদি',
'app.preview.down.block': 'আপনার স্থানীয় প্রকল্পে এই পৃষ্ঠাটি ডাউনলোড করুন',
'app.welcome.link.fetch-blocks': 'সমস্ত ব্লক পান',
'app.welcome.link.block-list':
'`block` ডেভেলপমেন্ট এর উপর ভিত্তি করে দ্রুত স্ট্যান্ডার্ড, পৃষ্ঠাসমূহ তৈরি করুন।',
...globalHeader,
...menu,
...settingDrawer,

3
src/locales/en-US.ts

@ -12,9 +12,6 @@ export default {
'layout.user.link.privacy': 'Privacy',
'layout.user.link.terms': 'Terms',
'app.preview.down.block': 'Download this page to your local project',
'app.welcome.link.fetch-blocks': 'Get all block',
'app.welcome.link.block-list':
'Quickly build standard, pages based on `block` development',
...globalHeader,
...menu,
...settingDrawer,

3
src/locales/fa-IR.ts

@ -12,9 +12,6 @@ export default {
'layout.user.link.privacy': 'حریم خصوصی',
'layout.user.link.terms': 'مقررات',
'app.preview.down.block': 'این صفحه را در پروژه محلی خود بارگیری کنید',
'app.welcome.link.fetch-blocks': 'دریافت تمام بلوک',
'app.welcome.link.block-list':
'به سرعت صفحات استاندارد مبتنی بر توسعه "بلوک" را بسازید',
...globalHeader,
...menu,
...settingDrawer,

3
src/locales/id-ID.ts

@ -12,9 +12,6 @@ export default {
'layout.user.link.privacy': 'Privasi',
'layout.user.link.terms': 'Ketentuan',
'app.preview.down.block': 'Unduh halaman ini dalam projek lokal anda',
'app.welcome.link.fetch-blocks': 'Dapatkan semua blok',
'app.welcome.link.block-list':
'Buat standar dengan cepat, halaman-halaman berdasarkan pengembangan `block`',
...globalHeader,
...menu,
...settingDrawer,

2
src/locales/ja-JP.ts

@ -13,8 +13,6 @@ export default {
'layout.user.link.terms': '利用規約',
'app.preview.down.block':
'このページをローカルプロジェクトにダウンロードしてください',
'app.welcome.link.fetch-blocks': '',
'app.welcome.link.block-list': '',
...globalHeader,
...menu,
...settingDrawer,

2
src/locales/zh-CN.ts

@ -12,8 +12,6 @@ export default {
'layout.user.link.privacy': '隐私',
'layout.user.link.terms': '条款',
'app.preview.down.block': '下载此页面到本地项目',
'app.welcome.link.fetch-blocks': '获取全部区块',
'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面',
...pages,
...globalHeader,
...menu,

69
src/pages/account/center/Center.style.ts

@ -0,0 +1,69 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
avatarHolder: {
marginBottom: '24px',
textAlign: 'center',
'& > img': { width: '104px', height: '104px', marginBottom: '20px' },
},
name: {
marginBottom: '4px',
color: token.colorTextHeading,
fontWeight: '500',
fontSize: '20px',
lineHeight: '28px',
},
detail: {
p: {
position: 'relative',
marginBottom: '8px',
paddingLeft: '26px',
'&:last-child': {
marginBottom: '0',
},
},
i: {
position: 'absolute',
top: '4px',
left: '0',
width: '14px',
height: '14px',
},
},
tagsTitle: {
marginBottom: '12px',
color: token.colorTextHeading,
fontWeight: '500',
},
teamTitle: {
marginBottom: '12px',
color: token.colorTextHeading,
fontWeight: '500',
},
tags: {
'.ant-tag': { marginBottom: '8px' },
},
team: {
'.ant-avatar': { marginRight: '12px' },
a: {
display: 'block',
marginBottom: '24px',
overflow: 'hidden',
color: token.colorText,
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
wordBreak: 'break-all',
transition: 'color 0.3s',
'&:hover': {
color: token.colorPrimary,
},
},
},
tabsCard: {
'.ant-card-head': { padding: '0 16px' },
},
};
});
export default useStyles;

249
src/pages/account/center/_mock.ts

@ -0,0 +1,249 @@
import type { Request, Response } from 'express';
import type { ListItemDataType } from './data.d';
const titles = [
'Alipay',
'Angular',
'Ant Design',
'Ant Design Pro',
'Bootstrap',
'React',
'Vue',
'Webpack',
];
const avatars = [
'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
];
const covers = [
'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png',
'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png',
'https://gw.alipayobjects.com/zos/rmsportal/iXjVmWVHbCJAyqvDxdtx.png',
'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
];
const desc = [
'那是一种内在的东西, 他们到达不了,也无法触及的',
'希望是一个好东西,也许是最好的,好东西是不会消亡的',
'生命就像一盒巧克力,结果往往出人意料',
'城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
'那时候我只会想自己想要什么,从不想自己拥有什么',
];
const user = [
'付小小',
'曲丽丽',
'林东东',
'周星星',
'吴加好',
'朱偏右',
'鱼酱',
'乐哥',
'谭小仪',
'仲尼',
];
// 当前用户信息
const currentUseDetail = {
name: 'Serati Ma',
avatar:
'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
tags: [
{
key: '0',
label: '很有想法的',
},
{
key: '1',
label: '专注设计',
},
{
key: '2',
label: '辣~',
},
{
key: '3',
label: '大长腿',
},
{
key: '4',
label: '川妹子',
},
{
key: '5',
label: '海纳百川',
},
],
notice: [
{
id: 'xxx1',
title: titles[0],
logo: avatars[0],
description: '那是一种内在的东西,他们到达不了,也无法触及的',
updatedAt: new Date(),
member: '科学搬砖组',
href: '',
memberLink: '',
},
{
id: 'xxx2',
title: titles[1],
logo: avatars[1],
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
updatedAt: new Date('2017-07-24'),
member: '全组都是吴彦祖',
href: '',
memberLink: '',
},
{
id: 'xxx3',
title: titles[2],
logo: avatars[2],
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
updatedAt: new Date(),
member: '中二少女团',
href: '',
memberLink: '',
},
{
id: 'xxx4',
title: titles[3],
logo: avatars[3],
description: '那时候我只会想自己想要什么,从不想自己拥有什么',
updatedAt: new Date('2017-07-23'),
member: '程序员日常',
href: '',
memberLink: '',
},
{
id: 'xxx5',
title: titles[4],
logo: avatars[4],
description: '凛冬将至',
updatedAt: new Date('2017-07-23'),
member: '高逼格设计天团',
href: '',
memberLink: '',
},
{
id: 'xxx6',
title: titles[5],
logo: avatars[5],
description: '生命就像一盒巧克力,结果往往出人意料',
updatedAt: new Date('2017-07-23'),
member: '骗你来学计算机',
href: '',
memberLink: '',
},
],
notifyCount: 12,
unreadCount: 11,
country: 'China',
geographic: {
province: {
label: '浙江省',
key: '330000',
},
city: {
label: '杭州市',
key: '330100',
},
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
};
function fakeList(count: number): ListItemDataType[] {
const list = [];
for (let i = 0; i < count; i += 1) {
list.push({
id: `fake-list-${i}`,
owner: user[i % 10],
title: titles[i % 8],
avatar: avatars[i % 8],
cover:
parseInt(`${i / 4}`, 10) % 2 === 0
? covers[i % 4]
: covers[3 - (i % 4)],
status: ['active', 'exception', 'normal'][i % 3] as
| 'normal'
| 'exception'
| 'active'
| 'success',
percent: Math.ceil(Math.random() * 50) + 50,
logo: avatars[i % 8],
href: 'https://ant.design',
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 2 * i).getTime(),
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2 * i).getTime(),
subDescription: desc[i % 5],
description:
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
activeUser: Math.ceil(Math.random() * 100000) + 100000,
newUser: Math.ceil(Math.random() * 1000) + 1000,
star: Math.ceil(Math.random() * 100) + 100,
like: Math.ceil(Math.random() * 100) + 100,
message: Math.ceil(Math.random() * 10) + 10,
content:
'段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
members: [
{
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
name: '曲丽丽',
id: 'member1',
},
{
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
name: '王昭君',
id: 'member2',
},
{
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
name: '董娜娜',
id: 'member3',
},
],
});
}
return list;
}
function getFakeList(req: Request, res: Response) {
const params = req.query as any;
const count = Number(params.count) * 1 || 5;
const result = fakeList(count);
return res.json({
data: {
list: result,
},
});
}
// 获取用户信息
function getCurrentUser(_req: Request, res: Response) {
return res.json({
data: currentUseDetail,
});
}
export default {
'GET /api/fake_list_Detail': getFakeList,
// 支持值为 Object 和 Array
'GET /api/currentUserDetail': getCurrentUser,
};

43
src/pages/account/center/components/Applications/index.style.ts

@ -0,0 +1,43 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
filterCardList: {
marginBottom: '-24px',
'.ant-card-meta-content': { marginTop: '0' },
'.ant-card-meta-avatar': { fontSize: '0' },
'.ant-list .ant-list-item-content-single': { maxWidth: '100%' },
},
cardInfo: {
marginTop: '16px',
marginLeft: '40px',
zoom: '1',
'&::before, &::after': { display: 'table', content: "' '" },
'&::after': {
clear: 'both',
height: '0',
fontSize: '0',
visibility: 'hidden',
},
'& > div': {
position: 'relative',
float: 'left',
width: '50%',
textAlign: 'left',
p: {
margin: '0',
fontSize: '24px',
lineHeight: '32px',
},
'p:first-child': {
marginBottom: '4px',
color: token.colorTextSecondary,
fontSize: '12px',
lineHeight: '20px',
},
},
},
};
});
export default useStyles;

136
src/pages/account/center/components/Applications/index.tsx

@ -0,0 +1,136 @@
import {
DownloadOutlined,
EditOutlined,
EllipsisOutlined,
ShareAltOutlined,
} from '@ant-design/icons';
import { useRequest } from '@umijs/max';
import { Avatar, Card, Dropdown, List, Tooltip } from 'antd';
import numeral from 'numeral';
import React from 'react';
import type { ListItemDataType } from '../../data.d';
import { queryFakeList } from '../../service';
import useStyles from './index.style';
export function formatWan(val: number) {
const v = val * 1;
if (!v || Number.isNaN(v)) return '';
let result: React.ReactNode = val;
if (val > 10000) {
result = (
<span>
{Math.floor(val / 10000)}
<span
style={{
position: 'relative',
top: -2,
fontSize: 14,
fontStyle: 'normal',
marginLeft: 2,
}}
>
</span>
</span>
);
}
return result;
}
const CardInfo: React.FC<{
activeUser: React.ReactNode;
newUser: React.ReactNode;
}> = ({ activeUser, newUser }) => {
const { styles: stylesApplications } = useStyles();
return (
<div className={stylesApplications.cardInfo}>
<div>
<p></p>
<p>{activeUser}</p>
</div>
<div>
<p></p>
<p>{newUser}</p>
</div>
</div>
);
};
const Applications: React.FC = () => {
const { styles: stylesApplications } = useStyles();
// 获取tab列表数据
const { data: listData } = useRequest(() => {
return queryFakeList({
count: 30,
});
});
return (
<List<ListItemDataType>
rowKey="id"
className={stylesApplications.filterCardList}
grid={{
gutter: 24,
xxl: 3,
xl: 2,
lg: 2,
md: 2,
sm: 2,
xs: 1,
}}
dataSource={listData?.list || []}
renderItem={(item) => (
<List.Item key={item.id}>
<Card
hoverable
styles={{
body: {
paddingBottom: 20,
},
}}
actions={[
<Tooltip key="download" title="下载">
<DownloadOutlined />
</Tooltip>,
<Tooltip title="编辑" key="edit">
<EditOutlined />
</Tooltip>,
<Tooltip title="分享" key="share">
<ShareAltOutlined />
</Tooltip>,
<Dropdown
menu={{
items: [
{
key: '1',
title: '1st menu item',
},
{
key: '2',
title: '2nd menu item',
},
],
}}
key="ellipsis"
>
<EllipsisOutlined />
</Dropdown>,
]}
>
<Card.Meta
avatar={<Avatar size="small" src={item.avatar} />}
title={item.title}
/>
<div>
<CardInfo
activeUser={formatWan(item.activeUser)}
newUser={numeral(item.newUser).format('0,0')}
/>
</div>
</Card>
</List.Item>
)}
/>
);
};
export default Applications;

31
src/pages/account/center/components/ArticleListContent/index.style.ts

@ -0,0 +1,31 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
description: {
maxWidth: '720px',
lineHeight: '22px',
},
extra: {
marginTop: '16px',
color: token.colorTextSecondary,
lineHeight: '22px',
display: 'flex',
gap: '8px',
alignItems: 'center',
'& > em': {
color: token.colorTextDisabled,
fontStyle: 'normal',
},
[`@media screen and (max-width: ${token.screenXS}px)`]: {
'& > em': {
display: 'block',
marginTop: '8px',
marginLeft: '0',
},
},
},
};
});
export default useStyles;

29
src/pages/account/center/components/ArticleListContent/index.tsx

@ -0,0 +1,29 @@
import { Avatar } from 'antd';
import dayjs from 'dayjs';
import React from 'react';
import useStyles from './index.style';
export type ApplicationsProps = {
data: {
content?: string;
updatedAt?: any;
avatar?: string;
owner?: string;
href?: string;
};
};
const ArticleListContent: React.FC<ApplicationsProps> = ({
data: { content, updatedAt, avatar, owner, href },
}) => {
const { styles } = useStyles();
return (
<div>
<div className={styles.description}>{content}</div>
<div className={styles.extra}>
<Avatar src={avatar} size="small" />
<a href={href}>{owner}</a> <a href={href}>{href}</a>
<em>{dayjs(updatedAt).format('YYYY-MM-DD HH:mm')}</em>
</div>
</div>
);
};
export default ArticleListContent;

14
src/pages/account/center/components/Articles/index.style.ts

@ -0,0 +1,14 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
articleList: {
'.ant-list-item:first-child': { paddingTop: '0' },
},
listItemMetaTitle: {
color: token.colorTextHeading,
},
};
});
export default useStyles;

71
src/pages/account/center/components/Articles/index.tsx

@ -0,0 +1,71 @@
import { LikeOutlined, MessageFilled, StarTwoTone } from '@ant-design/icons';
import { useRequest } from '@umijs/max';
import { List, Tag } from 'antd';
import React from 'react';
import type { ListItemDataType } from '../../data.d';
import { queryFakeList } from '../../service';
import ArticleListContent from '../ArticleListContent';
import useStyles from './index.style';
const IconText: React.FC<{
icon: React.ReactNode;
text: React.ReactNode;
}> = ({ icon, text }) => (
<span>
{icon} {text}
</span>
);
const Articles: React.FC = () => {
const { styles } = useStyles();
// 获取tab列表数据
const { data: listData } = useRequest(() => {
return queryFakeList({
count: 30,
});
});
return (
<List<ListItemDataType>
size="large"
className={styles.articleList}
rowKey="id"
itemLayout="vertical"
dataSource={listData?.list || []}
style={{
margin: '0 -24px',
}}
renderItem={(item) => (
<List.Item
key={item.id}
actions={[
<IconText key="star" icon={<StarTwoTone />} text={item.star} />,
<IconText key="like" icon={<LikeOutlined />} text={item.like} />,
<IconText
key="message"
icon={<MessageFilled />}
text={item.message}
/>,
]}
>
<List.Item.Meta
title={
<a className={styles.listItemMetaTitle} href={item.href}>
{item.title}
</a>
}
description={
<span>
<Tag>Ant Design</Tag>
<Tag></Tag>
<Tag></Tag>
</span>
}
/>
<ArticleListContent data={item} />
</List.Item>
)}
/>
);
};
export default Articles;

41
src/pages/account/center/components/AvatarList/index.style.ts

@ -0,0 +1,41 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
avatarList: {
display: 'inline-block',
ul: { display: 'inline-block', marginLeft: '8px', fontSize: '0' },
},
avatarItem: {
display: 'inline-block',
width: token.controlHeight,
height: token.controlHeight,
marginLeft: '-8px',
fontSize: token.fontSize,
'.ant-avatar': { border: `1px solid ${token.colorBorder}` },
},
avatarItemLarge: {
width: token.controlHeightLG,
height: token.controlHeightLG,
},
avatarItemSmall: {
width: token.controlHeightSM,
height: token.controlHeightSM,
},
avatarItemMini: {
width: '20px',
height: '20px',
'.ant-avatar': {
width: '20px',
height: '20px',
lineHeight: '20px',
'.ant-avatar-string': {
fontSize: '12px',
lineHeight: '18px',
},
},
},
};
});
export default useStyles;

89
src/pages/account/center/components/AvatarList/index.tsx

@ -0,0 +1,89 @@
import { Avatar, Tooltip } from 'antd';
import classNames from 'classnames';
import React from 'react';
import useStyles from './index.style';
export declare type SizeType = number | 'small' | 'default' | 'large';
export type AvatarItemProps = {
tips: React.ReactNode;
src: string;
size?: SizeType;
style?: React.CSSProperties;
onClick?: () => void;
};
export type AvatarListProps = {
Item?: React.ReactElement<AvatarItemProps>;
size?: SizeType;
maxLength?: number;
excessItemsStyle?: React.CSSProperties;
style?: React.CSSProperties;
children:
| React.ReactElement<AvatarItemProps>
| React.ReactElement<AvatarItemProps>[];
};
const avatarSizeToClassName = (styles: any, size?: SizeType | 'mini') =>
classNames(styles.avatarItem, {
[styles.avatarItemLarge]: size === 'large',
[styles.avatarItemSmall]: size === 'small',
[styles.avatarItemMini]: size === 'mini',
});
const Item: React.FC<AvatarItemProps> = ({
src,
size,
tips,
onClick = () => {},
}) => {
const { styles } = useStyles();
const cls = avatarSizeToClassName(styles, size);
return (
<li className={cls} onClick={onClick}>
{tips ? (
<Tooltip title={tips}>
<Avatar
src={src}
size={size}
style={{
cursor: 'pointer',
}}
/>
</Tooltip>
) : (
<Avatar src={src} size={size} />
)}
</li>
);
};
const AvatarList: React.FC<AvatarListProps> & {
Item: typeof Item;
} = ({ children, size, maxLength = 5, excessItemsStyle, ...other }) => {
const { styles } = useStyles();
const numOfChildren = React.Children.count(children);
const numToShow = maxLength >= numOfChildren ? numOfChildren : maxLength;
const childrenArray = React.Children.toArray(
children,
) as React.ReactElement<AvatarItemProps>[];
const childrenWithProps = childrenArray.slice(0, numToShow).map((child) =>
React.cloneElement(child, {
size,
}),
);
if (numToShow < numOfChildren) {
const cls = avatarSizeToClassName(styles, size);
childrenWithProps.push(
<li key="exceed" className={cls}>
<Avatar
size={size}
style={excessItemsStyle}
>{`+${numOfChildren - maxLength}`}</Avatar>
</li>,
);
}
return (
<div {...other} className={styles.avatarList}>
<ul> {childrenWithProps} </ul>
</div>
);
};
AvatarList.Item = Item;
export default AvatarList;

49
src/pages/account/center/components/Projects/index.style.ts

@ -0,0 +1,49 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
card: {
'.ant-card-meta-title': {
marginBottom: '4px',
'& > a': {
display: 'inline-block',
maxWidth: '100%',
color: token.colorTextHeading,
},
},
'.ant-card-meta-description': {
height: '44px',
overflow: 'hidden',
lineHeight: '22px',
},
'&:hover': {
'.ant-card-meta-title > a': {
color: token.colorPrimary,
},
},
},
cardItemContent: {
display: 'flex',
height: '20px',
marginTop: '16px',
marginBottom: '-4px',
lineHeight: '20px',
'& > span': {
flex: '1',
color: token.colorTextSecondary,
fontSize: '12px',
},
},
avatarList: {
flex: '0 1 auto',
},
cardList: {
marginTop: '24px',
},
coverCardList: {
'.ant-list .ant-list-item-content-single': { maxWidth: '100%' },
},
};
});
export default useStyles;

65
src/pages/account/center/components/Projects/index.tsx

@ -0,0 +1,65 @@
import { useRequest } from '@umijs/max';
import { Card, List } from 'antd';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import React from 'react';
import type { ListItemDataType } from '../../data.d';
import { queryFakeList } from '../../service';
import AvatarList from '../AvatarList';
import useStyles from './index.style';
dayjs.extend(relativeTime);
const Projects: React.FC = () => {
const { styles } = useStyles();
// 获取tab列表数据
const { data: listData } = useRequest(() => {
return queryFakeList({
count: 30,
});
});
return (
<List<ListItemDataType>
className={styles.coverCardList}
rowKey="id"
grid={{
gutter: 24,
xxl: 3,
xl: 2,
lg: 2,
md: 2,
sm: 2,
xs: 1,
}}
dataSource={listData?.list || []}
renderItem={(item) => (
<List.Item>
<Card
className={styles.card}
hoverable
cover={<img alt={item.title} src={item.cover} />}
>
<Card.Meta
title={<a>{item.title}</a>}
description={item.subDescription}
/>
<div className={styles.cardItemContent}>
<span>{dayjs(item.updatedAt).fromNow()}</span>
<div className={styles.avatarList}>
<AvatarList size="small">
{item.members.map((member) => (
<AvatarList.Item
key={`${item.id}-avatar-${member.id}`}
src={member.avatar}
tips={member.name}
/>
))}
</AvatarList>
</div>
</div>
</Card>
</List.Item>
)}
/>
);
};
export default Projects;

75
src/pages/account/center/data.d.ts

@ -0,0 +1,75 @@
export type tabKeyType = 'articles' | 'applications' | 'projects';
export interface TagType {
key: string;
label: string;
}
export type GeographicType = {
province: {
label: string;
key: string;
};
city: {
label: string;
key: string;
};
};
export type NoticeType = {
id: string;
title: string;
logo: string;
description: string;
updatedAt: string;
member: string;
href: string;
memberLink: string;
};
export type CurrentUser = {
name: string;
avatar: string;
userid: string;
notice: NoticeType[];
email: string;
signature: string;
title: string;
group: string;
tags: TagType[];
notifyCount: number;
unreadCount: number;
country: string;
geographic: GeographicType;
address: string;
phone: string;
};
export type Member = {
avatar: string;
name: string;
id: string;
};
export type ListItemDataType = {
id: string;
owner: string;
title: string;
avatar: string;
cover: string;
status: 'normal' | 'exception' | 'active' | 'success';
percent: number;
logo: string;
href: string;
body?: any;
updatedAt: number;
createdAt: number;
subDescription: string;
description: string;
activeUser: number;
newUser: number;
star: number;
like: number;
message: number;
content: string;
members: Member[];
};

278
src/pages/account/center/index.tsx

@ -0,0 +1,278 @@
import {
ClusterOutlined,
ContactsOutlined,
HomeOutlined,
PlusOutlined,
} from '@ant-design/icons';
import { GridContent } from '@ant-design/pro-components';
import { useRequest } from '@umijs/max';
import {
Avatar,
Card,
Col,
Divider,
Input,
type InputRef,
Row,
Tag,
} from 'antd';
import React, { useRef, useState } from 'react';
import useStyles from './Center.style';
import Applications from './components/Applications';
import Articles from './components/Articles';
import Projects from './components/Projects';
import type { CurrentUser, TagType, tabKeyType } from './data.d';
import { queryCurrent } from './service';
const operationTabList = [
{
key: 'articles',
tab: (
<span>
{' '}
<span
style={{
fontSize: 14,
}}
>
(8)
</span>
</span>
),
},
{
key: 'applications',
tab: (
<span>
{' '}
<span
style={{
fontSize: 14,
}}
>
(8)
</span>
</span>
),
},
{
key: 'projects',
tab: (
<span>
{' '}
<span
style={{
fontSize: 14,
}}
>
(8)
</span>
</span>
),
},
];
const TagList: React.FC<{
tags: CurrentUser['tags'];
}> = ({ tags }) => {
const { styles } = useStyles();
const ref = useRef<InputRef | null>(null);
const [newTags, setNewTags] = useState<TagType[]>([]);
const [inputVisible, setInputVisible] = useState<boolean>(false);
const [inputValue, setInputValue] = useState<string>('');
const showInput = () => {
setInputVisible(true);
if (ref.current) {
// eslint-disable-next-line no-unused-expressions
ref.current?.focus();
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
const handleInputConfirm = () => {
let tempsTags = [...newTags];
if (
inputValue &&
tempsTags.filter((tag) => tag.label === inputValue).length === 0
) {
tempsTags = [
...tempsTags,
{
key: `new-${tempsTags.length}`,
label: inputValue,
},
];
}
setNewTags(tempsTags);
setInputVisible(false);
setInputValue('');
};
return (
<div className={styles.tags}>
<div className={styles.tagsTitle}></div>
{(tags || []).concat(newTags).map((item) => (
<Tag key={item.key}>{item.label}</Tag>
))}
{inputVisible && (
<Input
ref={ref}
size="small"
style={{
width: 78,
}}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
/>
)}
{!inputVisible && (
<Tag
onClick={showInput}
style={{
borderStyle: 'dashed',
}}
>
<PlusOutlined />
</Tag>
)}
</div>
);
};
const Center: React.FC = () => {
const { styles } = useStyles();
const [tabKey, setTabKey] = useState<tabKeyType>('articles');
// 获取用户信息
const { data: currentUser, loading } = useRequest(() => {
return queryCurrent();
});
// 渲染用户信息
const renderUserInfo = ({
title,
group,
geographic,
}: Partial<CurrentUser>) => {
return (
<div className={styles.detail}>
<p>
<ContactsOutlined
style={{
marginRight: 8,
}}
/>
{title}
</p>
<p>
<ClusterOutlined
style={{
marginRight: 8,
}}
/>
{group}
</p>
<p>
<HomeOutlined
style={{
marginRight: 8,
}}
/>
{
(
geographic || {
province: {
label: '',
},
}
).province.label
}
{
(
geographic || {
city: {
label: '',
},
}
).city.label
}
</p>
</div>
);
};
// 渲染tab切换
const renderChildrenByTabKey = (tabValue: tabKeyType) => {
if (tabValue === 'projects') {
return <Projects />;
}
if (tabValue === 'applications') {
return <Applications />;
}
if (tabValue === 'articles') {
return <Articles />;
}
return null;
};
return (
<GridContent>
<Row gutter={24}>
<Col lg={7} md={24}>
<Card
variant="borderless"
style={{
marginBottom: 24,
}}
loading={loading}
>
{!loading && currentUser && (
<>
<div className={styles.avatarHolder}>
<img alt="" src={currentUser.avatar} />
<div className={styles.name}>{currentUser.name}</div>
<div>{currentUser?.signature}</div>
</div>
{renderUserInfo(currentUser)}
<Divider dashed />
<TagList tags={currentUser.tags || []} />
<Divider
style={{
marginTop: 16,
}}
dashed
/>
<div className={styles.team}>
<div className={styles.teamTitle}></div>
<Row gutter={36}>
{currentUser.notice?.map((item) => (
<Col key={item.id} lg={24} xl={12}>
<a href={item.href}>
<Avatar size="small" src={item.logo} />
{item.member}
</a>
</Col>
))}
</Row>
</div>
</>
)}
</Card>
</Col>
<Col lg={17} md={24}>
<Card
className={styles.tabsCard}
variant="borderless"
tabList={operationTabList}
activeTabKey={tabKey}
onTabChange={(_tabKey: string) => {
setTabKey(_tabKey as tabKeyType);
}}
>
{renderChildrenByTabKey(tabKey)}
</Card>
</Col>
</Row>
</GridContent>
);
};
export default Center;

14
src/pages/account/center/service.ts

@ -0,0 +1,14 @@
import { request } from '@umijs/max';
import type { CurrentUser, ListItemDataType } from './data.d';
export async function queryCurrent(): Promise<{ data: CurrentUser }> {
return request('/api/currentUserDetail');
}
export async function queryFakeList(params: {
count: number;
}): Promise<{ data: { list: ListItemDataType[] } }> {
return request('/api/fake_list_Detail', {
params,
});
}

80
src/pages/account/settings/_mock.ts

@ -0,0 +1,80 @@
import type { Request, Response } from 'express';
const city = require('./geographic/city.json');
const province = require('./geographic/province.json');
function getProvince(_: Request, res: Response) {
return res.json({
data: province,
});
}
function getCity(req: Request, res: Response) {
const province = req.params.province;
return res.json({
data: city[province as keyof typeof city],
});
}
function getCurrentUse(_req: Request, res: Response) {
return res.json({
data: {
name: 'Serati Ma',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
tags: [
{
key: '0',
label: '很有想法的',
},
{
key: '1',
label: '专注设计',
},
{
key: '2',
label: '辣~',
},
{
key: '3',
label: '大长腿',
},
{
key: '4',
label: '川妹子',
},
{
key: '5',
label: '海纳百川',
},
],
notifyCount: 12,
unreadCount: 11,
country: 'China',
geographic: {
province: {
label: '浙江省',
key: '330000',
},
city: {
label: '杭州市',
key: '330100',
},
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
},
});
}
// 代码中会兼容本地 service mock 以及部署站点的静态数据
export default {
// 支持值为 Object 和 Array
'GET /api/accountSettingCurrentUser': getCurrentUse,
'GET /api/geographic/province': getProvince,
'GET /api/geographic/city/:province': getCity,
};

239
src/pages/account/settings/components/base.tsx

@ -0,0 +1,239 @@
import { UploadOutlined } from '@ant-design/icons';
import {
ProForm,
ProFormDependency,
ProFormFieldSet,
ProFormSelect,
ProFormText,
ProFormTextArea,
} from '@ant-design/pro-components';
import { useRequest } from '@umijs/max';
import { Button, Input, message, Upload } from 'antd';
import React from 'react';
import { queryCity, queryCurrent, queryProvince } from '../service';
import useStyles from './index.style';
const validatorPhone = (
_rule: any,
value: string[],
callback: (message?: string) => void,
) => {
if (!value[0]) {
callback('Please input your area code!');
}
if (!value[1]) {
callback('Please input your phone number!');
}
callback();
};
const BaseView: React.FC = () => {
const { styles } = useStyles();
const { data: currentUser, loading } = useRequest(() => {
return queryCurrent();
});
const getAvatarURL = () => {
if (currentUser) {
if (currentUser.avatar) {
return currentUser.avatar;
}
const url =
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png';
return url;
}
return '';
};
const handleFinish = async () => {
message.success('更新基本信息成功');
};
return (
<div className={styles.baseView}>
{loading ? null : (
<>
<div className={styles.left}>
<ProForm
layout="vertical"
onFinish={handleFinish}
submitter={{
searchConfig: {
submitText: '更新基本信息',
},
render: (_, dom) => dom[1],
}}
initialValues={{
...currentUser,
phone: currentUser?.phone?.split('-'),
}}
requiredMark={false}
>
<ProFormText
width="md"
name="email"
label="邮箱"
rules={[
{
required: true,
message: '请输入您的邮箱!',
},
]}
/>
<ProFormText
width="md"
name="name"
label="昵称"
rules={[
{
required: true,
message: '请输入您的昵称!',
},
]}
/>
<ProFormTextArea
name="profile"
label="个人简介"
rules={[
{
required: true,
message: '请输入个人简介!',
},
]}
placeholder="个人简介"
/>
<ProFormSelect
width="sm"
name="country"
label="国家/地区"
rules={[
{
required: true,
message: '请输入您的国家或地区!',
},
]}
options={[
{
label: '中国',
value: 'China',
},
]}
/>
<ProForm.Group title="所在省市" size={8}>
<ProFormSelect
rules={[
{
required: true,
message: '请输入您的所在省!',
},
]}
width="sm"
fieldProps={{
labelInValue: true,
}}
name="province"
request={async () => {
return queryProvince().then(({ data }) => {
return data.map((item) => {
return {
label: item.name,
value: item.id,
};
});
});
}}
/>
<ProFormDependency name={['province']}>
{({ province }) => {
return (
<ProFormSelect
params={{
key: province?.value,
}}
name="city"
width="sm"
rules={[
{
required: true,
message: '请输入您的所在城市!',
},
]}
disabled={!province}
request={async () => {
if (!province?.key) {
return [];
}
return queryCity(province.key || '').then(
({ data }) => {
return data.map((item) => {
return {
label: item.name,
value: item.id,
};
});
},
);
}}
/>
);
}}
</ProFormDependency>
</ProForm.Group>
<ProFormText
width="md"
name="address"
label="街道地址"
rules={[
{
required: true,
message: '请输入您的街道地址!',
},
]}
/>
<ProFormFieldSet
name="phone"
label="联系电话"
rules={[
{
required: true,
message: '请输入您的联系电话!',
},
{
validator: validatorPhone,
},
]}
>
<Input className={styles.area_code} />
<Input className={styles.phone_number} />
</ProFormFieldSet>
</ProForm>
</div>
<div className={styles.right}>
<AvatarView avatar={getAvatarURL()} />
</div>
</>
)}
</div>
);
};
export default BaseView;
const AvatarView = ({ avatar }: { avatar: string }) => {
const { styles } = useStyles();
return (
<>
<div className={styles.avatar_title}></div>
<div className={styles.avatar}>
<img src={avatar} alt="avatar" />
</div>
<Upload showUploadList={false}>
<div className={styles.button_view}>
<Button>
<UploadOutlined />
</Button>
</div>
</Upload>
</>
);
};

48
src/pages/account/settings/components/binding.tsx

@ -0,0 +1,48 @@
import {
AlipayOutlined,
DingdingOutlined,
TaobaoOutlined,
} from '@ant-design/icons';
import { List } from 'antd';
import React from 'react';
const BindingView: React.FC = () => {
const getData = () => [
{
title: '绑定淘宝',
description: '当前未绑定淘宝账号',
actions: [<a key="Bind"></a>],
avatar: <TaobaoOutlined className="taobao" />,
},
{
title: '绑定支付宝',
description: '当前未绑定支付宝账号',
actions: [<a key="Bind"></a>],
avatar: <AlipayOutlined className="alipay" />,
},
{
title: '绑定钉钉',
description: '当前未绑定钉钉账号',
actions: [<a key="Bind"></a>],
avatar: <DingdingOutlined className="dingding" />,
},
];
return (
<List
itemLayout="horizontal"
dataSource={getData()}
renderItem={(item) => (
<List.Item actions={item.actions}>
<List.Item.Meta
avatar={item.avatar}
title={item.title}
description={item.description}
/>
</List.Item>
)}
/>
);
};
export default BindingView;

60
src/pages/account/settings/components/index.style.ts

@ -0,0 +1,60 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
baseView: {
display: 'flex',
paddingTop: '12px',
'.ant-legacy-form-item .ant-legacy-form-item-control-wrapper': {
width: '100%',
},
[`@media screen and (max-width: ${token.screenXL}px)`]: {
flexDirection: 'column-reverse',
},
},
left: {
minWidth: '224px',
maxWidth: '448px',
},
right: {
flex: '1',
paddingLeft: '104px',
[`@media screen and (max-width: ${token.screenXL}px)`]: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
maxWidth: '448px',
padding: '20px',
},
},
avatar_title: {
height: '22px',
marginBottom: '8px',
color: token.colorTextHeading,
fontSize: token.fontSize,
lineHeight: '22px',
[`@media screen and (max-width: ${token.screenXL}px)`]: {
display: 'none',
},
},
avatar: {
width: '144px',
height: '144px',
marginBottom: '12px',
overflow: 'hidden',
img: { width: '100%' },
},
button_view: {
width: '144px',
textAlign: 'center',
},
area_code: {
width: '72px',
},
phone_number: {
width: '214px',
},
};
});
export default useStyles;

44
src/pages/account/settings/components/notification.tsx

@ -0,0 +1,44 @@
import { List, Switch } from 'antd';
import React from 'react';
type Unpacked<T> = T extends (infer U)[] ? U : T;
const NotificationView: React.FC = () => {
const getData = () => {
const Action = (
<Switch checkedChildren="开" unCheckedChildren="关" defaultChecked />
);
return [
{
title: '用户消息',
description: '其他用户的消息将以站内信的形式通知',
actions: [Action],
},
{
title: '系统消息',
description: '系统消息将以站内信的形式通知',
actions: [Action],
},
{
title: '待办任务',
description: '待办任务将以站内信的形式通知',
actions: [Action],
},
];
};
const data = getData();
return (
<List<Unpacked<typeof data>>
itemLayout="horizontal"
dataSource={data}
renderItem={(item) => (
<List.Item actions={item.actions}>
<List.Item.Meta title={item.title} description={item.description} />
</List.Item>
)}
/>
);
};
export default NotificationView;

60
src/pages/account/settings/components/security.tsx

@ -0,0 +1,60 @@
import { List } from 'antd';
import React from 'react';
type Unpacked<T> = T extends (infer U)[] ? U : T;
const passwordStrength = {
strong: <span className="strong"></span>,
medium: <span className="medium"></span>,
weak: <span className="weak"> Weak</span>,
};
const SecurityView: React.FC = () => {
const getData = () => [
{
title: '账户密码',
description: (
<>
{passwordStrength.strong}
</>
),
actions: [<a key="Modify"></a>],
},
{
title: '密保手机',
description: `已绑定手机:138****8293`,
actions: [<a key="Modify"></a>],
},
{
title: '密保问题',
description: '未设置密保问题,密保问题可有效保护账户安全',
actions: [<a key="Set"></a>],
},
{
title: '备用邮箱',
description: `已绑定邮箱:ant***sign.com`,
actions: [<a key="Modify"></a>],
},
{
title: 'MFA 设备',
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
actions: [<a key="bind"></a>],
},
];
const data = getData();
return (
<List<Unpacked<typeof data>>
itemLayout="horizontal"
dataSource={data}
renderItem={(item) => (
<List.Item actions={item.actions}>
<List.Item.Meta title={item.title} description={item.description} />
</List.Item>
)}
/>
);
};
export default SecurityView;

43
src/pages/account/settings/data.d.ts

@ -0,0 +1,43 @@
export type TagType = {
key: string;
label: string;
};
export type GeographicItemType = {
name: string;
id: string;
};
export type GeographicType = {
province: GeographicItemType;
city: GeographicItemType;
};
export type NoticeType = {
id: string;
title: string;
logo: string;
description: string;
updatedAt: string;
member: string;
href: string;
memberLink: string;
};
export type CurrentUser = {
name: string;
avatar: string;
userid: string;
notice: NoticeType[];
email: string;
signature: string;
title: string;
group: string;
tags: TagType[];
notifyCount: number;
unreadCount: number;
country: string;
geographic: GeographicType;
address: string;
phone: string;
};

1784
src/pages/account/settings/geographic/city.json

File diff suppressed because it is too large

138
src/pages/account/settings/geographic/province.json

@ -0,0 +1,138 @@
[
{
"name": "北京市",
"id": "110000"
},
{
"name": "天津市",
"id": "120000"
},
{
"name": "河北省",
"id": "130000"
},
{
"name": "山西省",
"id": "140000"
},
{
"name": "内蒙古自治区",
"id": "150000"
},
{
"name": "辽宁省",
"id": "210000"
},
{
"name": "吉林省",
"id": "220000"
},
{
"name": "黑龙江省",
"id": "230000"
},
{
"name": "上海市",
"id": "310000"
},
{
"name": "江苏省",
"id": "320000"
},
{
"name": "浙江省",
"id": "330000"
},
{
"name": "安徽省",
"id": "340000"
},
{
"name": "福建省",
"id": "350000"
},
{
"name": "江西省",
"id": "360000"
},
{
"name": "山东省",
"id": "370000"
},
{
"name": "河南省",
"id": "410000"
},
{
"name": "湖北省",
"id": "420000"
},
{
"name": "湖南省",
"id": "430000"
},
{
"name": "广东省",
"id": "440000"
},
{
"name": "广西壮族自治区",
"id": "450000"
},
{
"name": "海南省",
"id": "460000"
},
{
"name": "重庆市",
"id": "500000"
},
{
"name": "四川省",
"id": "510000"
},
{
"name": "贵州省",
"id": "520000"
},
{
"name": "云南省",
"id": "530000"
},
{
"name": "西藏自治区",
"id": "540000"
},
{
"name": "陕西省",
"id": "610000"
},
{
"name": "甘肃省",
"id": "620000"
},
{
"name": "青海省",
"id": "630000"
},
{
"name": "宁夏回族自治区",
"id": "640000"
},
{
"name": "新疆维吾尔自治区",
"id": "650000"
},
{
"name": "台湾省",
"id": "710000"
},
{
"name": "香港特别行政区",
"id": "810000"
},
{
"name": "澳门特别行政区",
"id": "820000"
}
]

108
src/pages/account/settings/index.tsx

@ -0,0 +1,108 @@
import { GridContent } from '@ant-design/pro-components';
import { Menu } from 'antd';
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import BaseView from './components/base';
import BindingView from './components/binding';
import NotificationView from './components/notification';
import SecurityView from './components/security';
import useStyles from './style.style';
type SettingsStateKeys = 'base' | 'security' | 'binding' | 'notification';
type SettingsState = {
mode: 'inline' | 'horizontal';
selectKey: SettingsStateKeys;
};
const Settings: React.FC = () => {
const { styles } = useStyles();
const menuMap: Record<string, React.ReactNode> = {
base: '基本设置',
security: '安全设置',
binding: '账号绑定',
notification: '新消息通知',
};
const [initConfig, setInitConfig] = useState<SettingsState>({
mode: 'inline',
selectKey: 'base',
});
const dom = useRef<HTMLDivElement>(null);
const resize = useCallback(() => {
requestAnimationFrame(() => {
if (!dom.current) {
return;
}
let mode: 'inline' | 'horizontal' = 'inline';
const { offsetWidth } = dom.current;
if (dom.current.offsetWidth < 641 && offsetWidth > 400) {
mode = 'horizontal';
}
if (window.innerWidth < 768 && offsetWidth > 400) {
mode = 'horizontal';
}
setInitConfig((prev) => ({
...prev,
mode: mode as SettingsState['mode'],
}));
});
}, []);
useLayoutEffect(() => {
window.addEventListener('resize', resize);
resize();
return () => {
window.removeEventListener('resize', resize);
};
}, [resize]);
const getMenu = () => {
return Object.keys(menuMap).map((item) => ({
key: item,
label: menuMap[item],
}));
};
const renderChildren = () => {
const { selectKey } = initConfig;
switch (selectKey) {
case 'base':
return <BaseView />;
case 'security':
return <SecurityView />;
case 'binding':
return <BindingView />;
case 'notification':
return <NotificationView />;
default:
return null;
}
};
return (
<GridContent>
<div
className={styles.main}
ref={(ref) => {
if (ref) {
dom.current = ref;
}
}}
>
<div className={styles.leftMenu}>
<Menu
mode={initConfig.mode}
selectedKeys={[initConfig.selectKey]}
onClick={({ key }) => {
setInitConfig({
...initConfig,
selectKey: key as SettingsStateKeys,
});
}}
items={getMenu()}
/>
</div>
<div className={styles.right}>
<div className={styles.title}>{menuMap[initConfig.selectKey]}</div>
{renderChildren()}
</div>
</div>
</GridContent>
);
};
export default Settings;

20
src/pages/account/settings/service.ts

@ -0,0 +1,20 @@
import { request } from '@umijs/max';
import type { CurrentUser, GeographicItemType } from './data';
export async function queryCurrent(): Promise<{ data: CurrentUser }> {
return request('/api/accountSettingCurrentUser');
}
export async function queryProvince(): Promise<{ data: GeographicItemType[] }> {
return request('/api/geographic/province');
}
export async function queryCity(
province: string,
): Promise<{ data: GeographicItemType[] }> {
return request(`/api/geographic/city/${province}`);
}
export async function query() {
return request('/api/users');
}

74
src/pages/account/settings/style.style.ts

@ -0,0 +1,74 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
main: {
display: 'flex',
width: '100%',
height: '100%',
paddingTop: '16px',
paddingBottom: '16px',
backgroundColor: token.colorBgContainer,
'.ant-list-split .ant-list-item:last-child': {
borderBottom: `1px solid ${token.colorSplit}`,
},
'.ant-list-item': { paddingTop: '14px', paddingBottom: '14px' },
[`@media screen and (max-width: ${token.screenMD}px)`]: {
flexDirection: 'column',
},
},
leftMenu: {
width: '224px',
borderRight: `${token.lineWidth}px solid ${token.colorSplit}`,
'.ant-menu-inline': { border: 'none' },
'.ant-menu-horizontal': { fontWeight: 'bold' },
[`@media screen and (max-width: ${token.screenMD}px)`]: {
width: '100%',
border: 'none',
},
},
right: {
flex: '1',
padding: '8px 40px',
[`@media screen and (max-width: ${token.screenMD}px)`]: {
padding: '40px',
},
},
title: {
marginBottom: '12px',
color: token.colorTextHeading,
fontWeight: '500',
fontSize: '20px',
lineHeight: '28px',
},
taobao: {
display: 'block',
color: '#ff4000',
fontSize: '48px',
lineHeight: '48px',
borderRadius: token.borderRadius,
},
dingding: {
margin: '2px',
padding: '6px',
color: '#fff',
fontSize: '32px',
lineHeight: '32px',
backgroundColor: '#2eabff',
borderRadius: token.borderRadius,
},
alipay: {
color: '#2eabff',
fontSize: '48px',
lineHeight: '48px',
borderRadius: token.borderRadius,
},
':global': {
'font.strong': { color: token.colorSuccess },
'font.medium': { color: token.colorWarning },
'font.weak': { color: token.colorError },
},
};
});
export default useStyles;

210
src/pages/dashboard/analysis/_mock.ts

@ -0,0 +1,210 @@
import dayjs from 'dayjs';
import type { Request, Response } from 'express';
import type { AnalysisData, DataItem, RadarData } from './data.d';
// mock data
const visitData: DataItem[] = [];
const beginDay = Date.now();
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
for (let i = 0; i < fakeY.length; i += 1) {
visitData.push({
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: fakeY[i],
});
}
const visitData2 = [];
const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
for (let i = 0; i < fakeY2.length; i += 1) {
visitData2.push({
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: fakeY2[i],
});
}
const salesData = [];
for (let i = 0; i < 12; i += 1) {
salesData.push({
x: `${i + 1}`,
y: Math.floor(Math.random() * 1000) + 200,
});
}
const searchData = [];
for (let i = 0; i < 50; i += 1) {
searchData.push({
index: i + 1,
keyword: `搜索关键词-${i}`,
count: Math.floor(Math.random() * 1000),
range: Math.floor(Math.random() * 100),
status: Math.floor((Math.random() * 10) % 2),
});
}
const salesTypeData = [
{
x: '家用电器',
y: 4544,
},
{
x: '食用酒水',
y: 3321,
},
{
x: '个护健康',
y: 3113,
},
{
x: '服饰箱包',
y: 2341,
},
{
x: '母婴产品',
y: 1231,
},
{
x: '其他',
y: 1231,
},
];
const salesTypeDataOnline = [
{
x: '家用电器',
y: 244,
},
{
x: '食用酒水',
y: 321,
},
{
x: '个护健康',
y: 311,
},
{
x: '服饰箱包',
y: 41,
},
{
x: '母婴产品',
y: 121,
},
{
x: '其他',
y: 111,
},
];
const salesTypeDataOffline = [
{
x: '家用电器',
y: 99,
},
{
x: '食用酒水',
y: 188,
},
{
x: '个护健康',
y: 344,
},
{
x: '服饰箱包',
y: 255,
},
{
x: '其他',
y: 65,
},
];
const offlineData = [];
for (let i = 0; i < 10; i += 1) {
offlineData.push({
name: `Stores ${i}`,
cvr: Math.ceil(Math.random() * 9) / 10,
});
}
const offlineChartData = [];
for (let i = 0; i < 20; i += 1) {
const date = dayjs(Date.now() + 1000 * 60 * 30 * i).format('HH:mm');
offlineChartData.push({
date,
type: '客流量',
value: Math.floor(Math.random() * 100) + 10,
});
offlineChartData.push({
date,
type: '支付笔数',
value: Math.floor(Math.random() * 100) + 10,
});
}
const radarOriginData = [
{
name: '个人',
ref: 10,
koubei: 8,
output: 4,
contribute: 5,
hot: 7,
},
{
name: '团队',
ref: 3,
koubei: 9,
output: 6,
contribute: 3,
hot: 1,
},
{
name: '部门',
ref: 4,
koubei: 1,
output: 6,
contribute: 5,
hot: 7,
},
];
const radarData: RadarData[] = [];
const radarTitleMap = {
ref: '引用',
koubei: '口碑',
output: '产量',
contribute: '贡献',
hot: '热度',
};
radarOriginData.forEach((item) => {
Object.keys(item).forEach((key) => {
if (key !== 'name') {
radarData.push({
name: item.name,
label: radarTitleMap[key as 'ref'],
value: item[key as 'ref'],
});
}
});
});
const getFakeChartData: AnalysisData = {
visitData,
visitData2,
salesData,
searchData,
offlineData,
offlineChartData,
salesTypeData,
salesTypeDataOnline,
salesTypeDataOffline,
radarData,
};
const fakeChartData = (_: Request, res: Response) => {
return res.json({
data: getFakeChartData,
});
};
export default {
'GET /api/fake_analysis_chart_data': fakeChartData,
};

75
src/pages/dashboard/analysis/components/Charts/ChartCard/index.less

@ -0,0 +1,75 @@
@import '~antd/es/style/themes/default.less';
.chartCard {
position: relative;
.chartTop {
position: relative;
width: 100%;
overflow: hidden;
}
.chartTopMargin {
margin-bottom: 12px;
}
.chartTopHasMargin {
margin-bottom: 20px;
}
.metaWrap {
float: left;
}
.avatar {
position: relative;
top: 4px;
float: left;
margin-right: 20px;
img {
border-radius: 100%;
}
}
.meta {
height: 22px;
color: @text-color-secondary;
font-size: @font-size-base;
line-height: 22px;
}
.action {
position: absolute;
top: 4px;
right: 0;
line-height: 1;
cursor: pointer;
}
.total {
height: 38px;
margin-top: 4px;
margin-bottom: 0;
overflow: hidden;
color: @heading-color;
font-size: 30px;
line-height: 38px;
white-space: nowrap;
text-overflow: ellipsis;
word-break: break-all;
}
.content {
position: relative;
width: 100%;
margin-bottom: 12px;
}
.contentFixed {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
}
.footer {
margin-top: 8px;
padding-top: 9px;
border-top: 1px solid @border-color-split;
& > * {
position: relative;
}
}
.footerMargin {
margin-top: 20px;
}
}

77
src/pages/dashboard/analysis/components/Charts/ChartCard/index.style.ts

@ -0,0 +1,77 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
chartCard: {
position: 'relative',
},
chartTop: {
position: 'relative',
width: '100%',
overflow: 'hidden',
},
chartTopMargin: {
marginBottom: '12px',
},
chartTopHasMargin: {
marginBottom: '20px',
},
metaWrap: {
float: 'left',
},
avatar: {
position: 'relative',
top: '4px',
float: 'left',
marginRight: '20px',
img: { borderRadius: '100%' },
},
meta: {
height: '22px',
color: token.colorTextSecondary,
fontSize: token.fontSize,
lineHeight: '22px',
},
action: {
position: 'absolute',
top: '4px',
right: '0',
lineHeight: '1',
cursor: 'pointer',
},
total: {
height: '38px',
marginTop: '4px',
marginBottom: '0',
overflow: 'hidden',
color: token.colorTextHeading,
fontSize: '30px',
lineHeight: '38px',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
wordBreak: 'break-all',
},
content: {
position: 'relative',
width: '100%',
marginBottom: '12px',
},
contentFixed: {
position: 'absolute',
bottom: '0',
left: '0',
width: '100%',
},
footer: {
marginTop: '8px',
paddingTop: '9px',
borderTop: `1px solid ${token.colorSplit}`,
'& > *': { position: 'relative' },
},
footerMargin: {
marginTop: '20px',
},
};
});
export default useStyles;

110
src/pages/dashboard/analysis/components/Charts/ChartCard/index.tsx

@ -0,0 +1,110 @@
import omit from '@rc-component/util/es/omit';
import { Card } from 'antd';
import type { CardProps } from 'antd/es/card';
import classNames from 'classnames';
import React from 'react';
import useStyles from './index.style';
type totalType = () => React.ReactNode;
export type ChartCardProps = {
title: React.ReactNode;
action?: React.ReactNode;
total?: React.ReactNode | number | (() => React.ReactNode | number);
footer?: React.ReactNode;
contentHeight?: number;
avatar?: React.ReactNode;
style?: React.CSSProperties;
} & CardProps;
const ChartCard: React.FC<ChartCardProps> = (props) => {
const { styles } = useStyles();
const renderTotal = (total?: number | totalType | React.ReactNode) => {
if (!total && total !== 0) {
return null;
}
let totalDom: React.ReactNode | null = null;
switch (typeof total) {
case 'undefined':
totalDom = null;
break;
case 'function':
totalDom = <div className={styles.total}>{total()}</div>;
break;
default:
totalDom = <div className={styles.total}>{total}</div>;
}
return totalDom;
};
const renderContent = () => {
const {
contentHeight,
title,
avatar,
action,
total,
footer,
children,
loading,
} = props;
if (loading) {
return false;
}
return (
<div className={styles.chartCard}>
<div
className={classNames(styles.chartTop, {
[styles.chartTopMargin]: !children && !footer,
})}
>
<div className={styles.avatar}>{avatar}</div>
<div className={styles.metaWrap}>
<div className={styles.meta}>
<span>{title}</span>
<span className={styles.action}>{action}</span>
</div>
{renderTotal(total)}
</div>
</div>
{children && (
<div
className={styles.content}
style={{
height: contentHeight || 'auto',
}}
>
<div className={contentHeight ? styles.contentFixed : undefined}>
{children}
</div>
</div>
)}
{footer && (
<div
className={classNames(styles.footer, {
[styles.footerMargin]: !children,
})}
>
{footer}
</div>
)}
</div>
);
};
const { loading = false, ...rest } = props;
const cardProps = omit(rest, ['total', 'contentHeight', 'action']);
return (
<Card
loading={loading}
styles={{
body: {
padding: '20px 24px 8px 24px',
},
}}
{...cardProps}
>
{renderContent()}
</Card>
);
};
export default ChartCard;

17
src/pages/dashboard/analysis/components/Charts/Field/index.less

@ -0,0 +1,17 @@
@import '~antd/es/style/themes/default.less';
.field {
margin: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.label,
.number {
font-size: @font-size-base;
line-height: 22px;
}
.number {
margin-left: 8px;
color: @heading-color;
}
}

22
src/pages/dashboard/analysis/components/Charts/Field/index.style.ts

@ -0,0 +1,22 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
field: {
margin: '0',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
},
label: {
fontSize: token.fontSize,
lineHeight: '22px',
},
number: {
marginLeft: '8px',
color: token.colorTextHeading,
},
};
});
export default useStyles;

17
src/pages/dashboard/analysis/components/Charts/Field/index.tsx

@ -0,0 +1,17 @@
import React from 'react';
import useStyles from './index.style';
export type FieldProps = {
label: React.ReactNode;
value: React.ReactNode;
style?: React.CSSProperties;
};
const Field: React.FC<FieldProps> = ({ label, value, ...rest }) => {
const { styles } = useStyles();
return (
<div className={styles.field} {...rest}>
<span className={styles.label}>{label}</span>
<span className={styles.number}>{value}</span>
</div>
);
};
export default Field;

19
src/pages/dashboard/analysis/components/Charts/index.less

@ -0,0 +1,19 @@
.miniChart {
position: relative;
width: 100%;
.chartContent {
position: absolute;
bottom: -28px;
width: 100%;
> div {
margin: 0 -5px;
overflow: hidden;
}
}
.chartLoading {
position: absolute;
top: 16px;
left: 50%;
margin-left: -7px;
}
}

23
src/pages/dashboard/analysis/components/Charts/index.style.ts

@ -0,0 +1,23 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(() => {
return {
miniChart: {
position: 'relative',
width: '100%',
},
chartContent: {
position: 'absolute',
bottom: '-28px',
width: '100%',
'> div': { margin: '0 -5px', overflow: 'hidden' },
},
chartLoading: {
position: 'absolute',
top: '16px',
left: '50%',
marginLeft: '-7px',
},
};
});
export default useStyles;

13
src/pages/dashboard/analysis/components/Charts/index.tsx

@ -0,0 +1,13 @@
import numeral from 'numeral';
import ChartCard from './ChartCard';
import Field from './Field';
const yuan = (val: number | string) => `¥ ${numeral(val).format('0,0')}`;
const Charts = {
yuan,
ChartCard,
Field,
};
export { Charts as default, yuan, ChartCard, Field };

168
src/pages/dashboard/analysis/components/IntroduceRow.tsx

@ -0,0 +1,168 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { Area, Column } from '@ant-design/plots';
import { Col, Progress, Row, Tooltip } from 'antd';
import numeral from 'numeral';
import type { DataItem } from '../data.d';
import useStyles from '../style.style';
import Yuan from '../utils/Yuan';
import { ChartCard, Field } from './Charts';
import Trend from './Trend';
const topColResponsiveProps = {
xs: 24,
sm: 12,
md: 12,
lg: 12,
xl: 6,
style: {
marginBottom: 24,
},
};
const IntroduceRow = ({
loading,
visitData,
}: {
loading: boolean;
visitData: DataItem[];
}) => {
const { styles } = useStyles();
return (
<Row gutter={24}>
<Col {...topColResponsiveProps}>
<ChartCard
variant="borderless"
title="总销售额"
action={
<Tooltip title="指标说明">
<InfoCircleOutlined />
</Tooltip>
}
loading={loading}
total={() => <Yuan>126560</Yuan>}
footer={
<Field
label="日销售额"
value={`${numeral(12423).format('0,0')}`}
/>
}
contentHeight={46}
>
<Trend
flag="up"
style={{
marginRight: 16,
}}
>
<span className={styles.trendText}>12%</span>
</Trend>
<Trend flag="down">
<span className={styles.trendText}>11%</span>
</Trend>
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
variant="borderless"
loading={loading}
title="访问量"
action={
<Tooltip title="指标说明">
<InfoCircleOutlined />
</Tooltip>
}
total={numeral(8846).format('0,0')}
footer={
<Field label="日访问量" value={numeral(1234).format('0,0')} />
}
contentHeight={46}
>
<Area
xField="x"
yField="y"
shapeField="smooth"
height={46}
axis={false}
style={{
fill: 'linear-gradient(-90deg, white 0%, #975FE4 100%)',
fillOpacity: 0.6,
width: '100%',
}}
padding={-20}
data={visitData}
/>
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
variant="borderless"
loading={loading}
title="支付笔数"
action={
<Tooltip title="指标说明">
<InfoCircleOutlined />
</Tooltip>
}
total={numeral(6560).format('0,0')}
footer={<Field label="转化率" value="60%" />}
contentHeight={46}
>
<Column
xField="x"
yField="y"
padding={-20}
axis={false}
height={46}
data={visitData}
scale={{ x: { paddingInner: 0.4 } }}
/>
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
loading={loading}
variant="borderless"
title="运营活动效果"
action={
<Tooltip title="指标说明">
<InfoCircleOutlined />
</Tooltip>
}
total="78%"
footer={
<div
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
>
<Trend
flag="up"
style={{
marginRight: 16,
}}
>
<span className={styles.trendText}>12%</span>
</Trend>
<Trend flag="down">
<span className={styles.trendText}>11%</span>
</Trend>
</div>
}
contentHeight={46}
>
<Progress
percent={78}
strokeColor={{ from: '#108ee9', to: '#87d068' }}
status="active"
/>
</ChartCard>
</Col>
</Row>
);
};
export default IntroduceRow;

68
src/pages/dashboard/analysis/components/NumberInfo/index.less

@ -0,0 +1,68 @@
@import '~antd/es/style/themes/default.less';
.numberInfo {
.suffix {
margin-left: 4px;
color: @text-color;
font-size: 16px;
font-style: normal;
}
.numberInfoTitle {
margin-bottom: 16px;
color: @text-color;
font-size: @font-size-lg;
transition: all 0.3s;
}
.numberInfoSubTitle {
height: 22px;
overflow: hidden;
color: @text-color-secondary;
font-size: @font-size-base;
line-height: 22px;
white-space: nowrap;
text-overflow: ellipsis;
word-break: break-all;
}
.numberInfoValue {
margin-top: 4px;
overflow: hidden;
font-size: 0;
white-space: nowrap;
text-overflow: ellipsis;
word-break: break-all;
& > span {
display: inline-block;
height: 32px;
margin-right: 32px;
color: @heading-color;
font-size: 24px;
line-height: 32px;
}
.subTotal {
margin-right: 0;
color: @text-color-secondary;
font-size: @font-size-lg;
vertical-align: top;
.anticon {
margin-left: 4px;
font-size: 12px;
transform: scale(0.82);
}
:global {
.anticon-caret-up {
color: @red-6;
}
.anticon-caret-down {
color: @green-6;
}
}
}
}
}
.numberInfolight {
.numberInfoValue {
& > span {
color: @text-color;
}
}
}

56
src/pages/dashboard/analysis/components/NumberInfo/index.style.ts

@ -0,0 +1,56 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
suffix: {
marginLeft: '4px',
color: token.colorText,
fontSize: '16px',
fontStyle: 'normal',
},
numberInfoTitle: {
marginBottom: '16px',
color: token.colorText,
fontSize: token.fontSizeLG,
transition: 'all 0.3s',
},
numberInfoSubTitle: {
height: '22px',
overflow: 'hidden',
color: token.colorTextSecondary,
fontSize: token.fontSize,
lineHeight: '22px',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
wordBreak: 'break-all',
},
numberInfoValue: {
marginTop: '4px',
overflow: 'hidden',
fontSize: '0',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
wordBreak: 'break-all',
'& > span': { color: token.colorText },
},
subTotal: {
marginRight: '0',
color: token.colorTextSecondary,
fontSize: token.fontSizeLG,
verticalAlign: 'top',
},
anticon: {
marginLeft: '4px',
fontSize: '12px',
transform: 'scale(0.82)',
},
'anticon-caret-up': {
color: token['red-6'],
},
'anticon-caret-down': {
color: token['green-6'],
},
};
});
export default useStyles;

79
src/pages/dashboard/analysis/components/NumberInfo/index.tsx

@ -0,0 +1,79 @@
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import React from 'react';
import useStyles from './index.style';
export type NumberInfoProps = {
title?: React.ReactNode | string;
subTitle?: React.ReactNode | string;
total?: React.ReactNode | string;
status?: 'up' | 'down';
theme?: string;
gap?: number;
subTotal?: number;
suffix?: string;
style?: React.CSSProperties;
};
const NumberInfo: React.FC<NumberInfoProps> = ({
theme,
title,
subTitle,
total,
subTotal,
status,
suffix,
gap,
...rest
}) => {
const { styles } = useStyles();
return (
<div
className={classNames({
[styles[`numberInfo${theme}` as keyof typeof styles]]: !!theme,
})}
{...rest}
>
{title && (
<div
className={styles.numberInfoTitle}
title={typeof title === 'string' ? title : ''}
>
{title}
</div>
)}
{subTitle && (
<div
className={styles.numberInfoSubTitle}
title={typeof subTitle === 'string' ? subTitle : ''}
>
{subTitle}
</div>
)}
<div
className={styles.numberInfoValue}
style={
gap
? {
marginTop: gap,
}
: {}
}
>
<span>
{total}
{suffix && <em className={styles.suffix}>{suffix}</em>}
</span>
{(status || subTotal) && (
<span className={styles.subTotal}>
{subTotal}
{status && status === 'up' ? (
<CaretUpOutlined />
) : (
<CaretDownOutlined />
)}
</span>
)}
</div>
</div>
);
};
export default NumberInfo;

110
src/pages/dashboard/analysis/components/OfflineData.tsx

@ -0,0 +1,110 @@
import { Line, Tiny } from '@ant-design/plots';
import { Card, Col, Row, Tabs } from 'antd';
import type { DataItem, OfflineDataType } from '../data.d';
import useStyles from '../style.style';
import NumberInfo from './NumberInfo';
const CustomTab = ({
data,
currentTabKey: currentKey,
}: {
data: OfflineDataType;
currentTabKey: string;
}) => (
<Row
gutter={8}
style={{
width: 138,
margin: '8px 0',
}}
>
<Col span={12}>
<NumberInfo
title={data.name}
subTitle="转化率"
gap={2}
total={`${data.cvr * 100}%`}
theme={currentKey !== data.name ? 'light' : undefined}
/>
</Col>
<Col
span={12}
style={{
paddingTop: 36,
}}
>
<Tiny.Ring
height={60}
width={60}
percent={data.cvr}
color={['#E8EEF4', '#5FABF4']}
/>
</Col>
</Row>
);
const OfflineData = ({
activeKey,
loading,
offlineData,
offlineChartData,
handleTabChange,
}: {
activeKey: string;
loading: boolean;
offlineData: OfflineDataType[];
offlineChartData: DataItem[];
handleTabChange: (activeKey: string) => void;
}) => {
const { styles } = useStyles();
return (
<Card
loading={loading}
className={styles.offlineCard}
variant="borderless"
style={{
marginTop: 32,
}}
>
<Tabs
activeKey={activeKey}
onChange={handleTabChange}
items={offlineData.map((shop) => ({
key: shop.name,
label: <CustomTab data={shop} currentTabKey={activeKey} />,
children: (
<div
style={{
padding: '0 24px',
}}
>
<Line
height={400}
data={offlineChartData}
xField="date"
yField="value"
colorField="type"
slider={{ x: true }}
axis={{
x: { title: false },
y: {
title: false,
gridLineDash: null,
gridStroke: '#ccc',
gridStrokeOpacity: 1,
},
}}
legend={{
color: {
layout: { justifyContent: 'center' },
},
}}
/>
</div>
),
}))}
/>
</Card>
);
};
export default OfflineData;

9
src/pages/dashboard/analysis/components/PageLoading/index.tsx

@ -0,0 +1,9 @@
import { Spin } from 'antd';
// loading components from code split
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
export default () => (
<div style={{ paddingTop: 100, textAlign: 'center' }}>
<Spin size="large" />
</div>
);

67
src/pages/dashboard/analysis/components/ProportionSales.tsx

@ -0,0 +1,67 @@
import { Pie } from '@ant-design/plots';
import { Card, Segmented, Typography } from 'antd';
import numeral from 'numeral';
import React from 'react';
import type { DataItem } from '../data.d';
import useStyles from '../style.style';
const { Text } = Typography;
const ProportionSales = ({
dropdownGroup,
salesType,
loading,
salesPieData,
handleChangeSalesType,
}: {
loading: boolean;
dropdownGroup: React.ReactNode;
salesType: 'all' | 'online' | 'stores';
salesPieData: DataItem[];
handleChangeSalesType?: (value: 'all' | 'online' | 'stores') => void;
}) => {
const { styles } = useStyles();
return (
<Card
loading={loading}
className={styles.salesCard}
variant="borderless"
title="销售额类别占比"
style={{
height: '100%',
}}
extra={
<div className={styles.salesCardExtra}>
{dropdownGroup}
<Segmented
className={styles.salesTypeRadio}
value={salesType}
onChange={handleChangeSalesType}
options={[
{ label: '全部渠道', value: 'all' },
{ label: '线上', value: 'online' },
{ label: '门店', value: 'stores' },
]}
size="middle"
/>
</div>
}
>
<Text></Text>
<Pie
height={340}
radius={0.8}
innerRadius={0.5}
angleField="y"
colorField="x"
data={salesPieData as any}
legend={false}
label={{
position: 'spider',
text: (item: { x: number; y: number }) =>
`${item.x}: ${numeral(item.y).format('0,0')}`,
}}
/>
</Card>
);
};
export default ProportionSales;

225
src/pages/dashboard/analysis/components/SalesCard.tsx

@ -0,0 +1,225 @@
import { Column } from '@ant-design/plots';
import { Button, Card, Col, DatePicker, Row, Tabs } from 'antd';
import type { RangePickerProps } from 'antd/es/date-picker';
import numeral from 'numeral';
import type { DataItem } from '../data.d';
import useStyles from '../style.style';
export type TimeType = 'today' | 'week' | 'month' | 'year';
const { RangePicker } = DatePicker;
const rankingListData: {
title: string;
total: number;
}[] = [];
for (let i = 0; i < 7; i += 1) {
rankingListData.push({
title: `工专路 ${i} 号店`,
total: 323234,
});
}
const SalesCard = ({
rangePickerValue,
salesData,
isActive,
handleRangePickerChange,
loading,
selectDate,
}: {
rangePickerValue: RangePickerProps['value'];
isActive: (key: TimeType) => string;
salesData: DataItem[];
loading: boolean;
handleRangePickerChange: RangePickerProps['onChange'];
selectDate: (key: TimeType) => void;
}) => {
const { styles } = useStyles();
return (
<Card
loading={loading}
variant="borderless"
styles={{
body: {
padding: loading ? 24 : 0,
},
}}
>
<Tabs
className={styles.salesCard}
tabBarExtraContent={
<div className={styles.salesExtraWrap}>
<div className={styles.salesExtra}>
<Button
type="text"
className={isActive('today')}
onClick={() => selectDate('today')}
>
</Button>
<Button
type="text"
className={isActive('week')}
onClick={() => selectDate('week')}
>
</Button>
<Button
type="text"
className={isActive('month')}
onClick={() => selectDate('month')}
>
</Button>
<Button
type="text"
className={isActive('year')}
onClick={() => selectDate('year')}
>
</Button>
</div>
<RangePicker
value={rangePickerValue}
onChange={handleRangePickerChange}
variant="filled"
style={{
width: 256,
}}
/>
</div>
}
size="large"
tabBarStyle={{
marginBottom: 24,
}}
items={[
{
key: 'sales',
label: '销售额',
children: (
<Row>
<Col xl={16} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesBar}>
<Column
height={300}
data={salesData}
xField="x"
yField="y"
paddingBottom={12}
axis={{
x: {
title: false,
},
y: {
title: false,
gridLineDash: null,
gridStroke: '#ccc',
},
}}
scale={{
x: { paddingInner: 0.4 },
}}
tooltip={{
name: '销售量',
channel: 'y',
}}
/>
</div>
</Col>
<Col xl={8} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesRank}>
<h4 className={styles.rankingTitle}></h4>
<ul className={styles.rankingList}>
{rankingListData.map((item, i) => (
<li key={item.title}>
<span
className={`${styles.rankingItemNumber} ${
i < 3 ? styles.rankingItemNumberActive : ''
}`}
>
{i + 1}
</span>
<span
className={styles.rankingItemTitle}
title={item.title}
>
{item.title}
</span>
<span>{numeral(item.total).format('0,0')}</span>
</li>
))}
</ul>
</div>
</Col>
</Row>
),
},
{
key: 'views',
label: '访问量',
children: (
<Row>
<Col xl={16} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesBar}>
<Column
height={300}
data={salesData}
xField="x"
yField="y"
paddingBottom={12}
axis={{
x: {
title: false,
},
y: {
title: false,
},
}}
scale={{
x: { paddingInner: 0.4 },
}}
tooltip={{
name: '访问量',
channel: 'y',
}}
/>
</div>
</Col>
<Col xl={8} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesRank}>
<h4 className={styles.rankingTitle}>访</h4>
<ul className={styles.rankingList}>
{rankingListData.map((item, i) => (
<li key={item.title}>
<span
className={`${
i < 3
? styles.rankingItemNumberActive
: styles.rankingItemNumber
}`}
>
{i + 1}
</span>
<span
className={styles.rankingItemTitle}
title={item.title}
>
{item.title}
</span>
<span>{numeral(item.total).format('0,0')}</span>
</li>
))}
</ul>
</div>
</Col>
</Row>
),
},
]}
/>
</Card>
);
};
export default SalesCard;

181
src/pages/dashboard/analysis/components/TopSearch.tsx

@ -0,0 +1,181 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { Area } from '@ant-design/plots';
import { Card, Col, Row, Table, Tooltip } from 'antd';
import numeral from 'numeral';
import React from 'react';
import type { DataItem } from '../data.d';
import NumberInfo from './NumberInfo';
import Trend from './Trend';
const TopSearch = ({
loading,
visitData2,
searchData,
dropdownGroup,
}: {
loading: boolean;
visitData2: DataItem[];
dropdownGroup: React.ReactNode;
searchData: DataItem[];
}) => {
const columns = [
{
title: '排名',
dataIndex: 'index',
key: 'index',
},
{
title: '搜索关键词',
dataIndex: 'keyword',
key: 'keyword',
render: (text: React.ReactNode) => <a href="/">{text}</a>,
},
{
title: '用户数',
dataIndex: 'count',
key: 'count',
sorter: (
a: {
count: number;
},
b: {
count: number;
},
) => a.count - b.count,
},
{
title: '周涨幅',
dataIndex: 'range',
key: 'range',
sorter: (
a: {
range: number;
},
b: {
range: number;
},
) => a.range - b.range,
render: (
text: React.ReactNode,
record: {
status: number;
},
) => (
<Trend flag={record.status === 1 ? 'down' : 'up'}>
<span
style={{
marginRight: 4,
}}
>
{text}%
</span>
</Trend>
),
},
];
return (
<Card
loading={loading}
variant="borderless"
title="线上热门搜索"
extra={dropdownGroup}
style={{
height: '100%',
}}
>
<Row gutter={68}>
<Col
sm={12}
xs={24}
style={{
marginBottom: 24,
}}
>
<NumberInfo
subTitle={
<span>
<Tooltip title="指标说明">
<InfoCircleOutlined
style={{
marginLeft: 8,
}}
/>
</Tooltip>
</span>
}
gap={8}
total={numeral(12321).format('0,0')}
status="up"
subTotal={17.1}
/>
<Area
xField="x"
yField="y"
shapeField="smooth"
height={45}
axis={false}
padding={-12}
style={{
fill: 'linear-gradient(-90deg, white 0%, #6294FA 100%)',
fillOpacity: 0.4,
}}
data={visitData2}
/>
</Col>
<Col
sm={12}
xs={24}
style={{
marginBottom: 24,
}}
>
<NumberInfo
subTitle={
<span>
<Tooltip title="指标说明">
<InfoCircleOutlined
style={{
marginLeft: 8,
}}
/>
</Tooltip>
</span>
}
total={2.7}
status="down"
subTotal={26.2}
gap={8}
/>
<Area
xField="x"
yField="y"
shapeField="smooth"
height={45}
padding={-12}
style={{
fill: 'linear-gradient(-90deg, white 0%, #6294FA 100%)',
fillOpacity: 0.4,
}}
data={visitData2}
axis={false}
/>
</Col>
</Row>
<Table<any>
rowKey={(record) => record.index}
size="small"
columns={columns}
dataSource={searchData}
pagination={{
style: {
marginBottom: 0,
},
pageSize: 5,
}}
/>
</Card>
);
};
export default TopSearch;

37
src/pages/dashboard/analysis/components/Trend/index.less

@ -0,0 +1,37 @@
@import '~antd/es/style/themes/default.less';
.trendItem {
display: inline-block;
font-size: @font-size-base;
line-height: 22px;
.up,
.down {
position: relative;
top: 1px;
margin-left: 4px;
span {
font-size: 12px;
transform: scale(0.83);
}
}
.up {
color: @red-6;
}
.down {
top: -1px;
color: @green-6;
}
&.trendItemGrey .up,
&.trendItemGrey .down {
color: @text-color;
}
&.reverseColor .up {
color: @green-6;
}
&.reverseColor .down {
color: @red-6;
}
}

32
src/pages/dashboard/analysis/components/Trend/index.style.ts

@ -0,0 +1,32 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
trendItem: {
display: 'inline-block',
fontSize: token.fontSize,
lineHeight: '22px',
},
up: {
color: token['red-6'],
},
down: {
top: '-1px',
color: token['green-6'],
},
trendItemGrey: {
up: {
color: token.colorText,
},
down: {
color: token.colorText,
},
},
reverseColor: {
up: { color: token['green-6'] },
down: { color: token['red-6'] },
},
};
});
export default useStyles;

47
src/pages/dashboard/analysis/components/Trend/index.tsx

@ -0,0 +1,47 @@
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import React from 'react';
import useStyles from './index.style';
export type TrendProps = {
colorful?: boolean;
flag: 'up' | 'down';
style?: React.CSSProperties;
reverseColor?: boolean;
className?: string;
children?: React.ReactNode;
};
const Trend: React.FC<TrendProps> = ({
colorful = true,
reverseColor = false,
flag,
children,
className,
...rest
}) => {
const { styles } = useStyles();
const classString = classNames(
styles.trendItem,
{
[styles.trendItemGrey]: !colorful,
[styles.reverseColor]: reverseColor && colorful,
},
className,
);
return (
<div
{...rest}
className={classString}
title={typeof children === 'string' ? children : ''}
>
<span>{children}</span>
{flag && (
<span className={styles[flag]}>
{flag === 'up' ? <CaretUpOutlined /> : <CaretDownOutlined />}
</span>
)}
</div>
);
};
export default Trend;

45
src/pages/dashboard/analysis/data.d.ts

@ -0,0 +1,45 @@
export interface DataItem {
[field: string]: string | number | number[] | null | undefined;
}
export interface VisitDataType {
x: string;
y: number;
}
export type SearchDataType = {
index: number;
keyword: string;
count: number;
range: number;
status: number;
};
export type OfflineDataType = {
name: string;
cvr: number;
};
export interface OfflineChartData {
date: number;
type: number;
value: number;
}
export type RadarData = {
name: string;
label: string;
value: number;
};
export interface AnalysisData {
visitData: DataItem[];
visitData2: DataItem[];
salesData: DataItem[];
searchData: DataItem[];
offlineData: OfflineDataType[];
offlineChartData: DataItem[];
salesTypeData: DataItem[];
salesTypeDataOnline: DataItem[];
salesTypeDataOffline: DataItem[];
radarData: RadarData[];
}

157
src/pages/dashboard/analysis/index.tsx

@ -0,0 +1,157 @@
import { EllipsisOutlined } from '@ant-design/icons';
import { GridContent } from '@ant-design/pro-components';
import { useRequest } from '@umijs/max';
import { Col, Dropdown, Row } from 'antd';
import type { RangePickerProps } from 'antd/es/date-picker';
import type { Dayjs } from 'dayjs';
import type { FC } from 'react';
import { Suspense, useState } from 'react';
import IntroduceRow from './components/IntroduceRow';
import OfflineData from './components/OfflineData';
import PageLoading from './components/PageLoading';
import ProportionSales from './components/ProportionSales';
import type { TimeType } from './components/SalesCard';
import SalesCard from './components/SalesCard';
import TopSearch from './components/TopSearch';
import type { AnalysisData } from './data.d';
import { fakeChartData } from './service';
import useStyles from './style.style';
import { getTimeDistance } from './utils/utils';
type RangePickerValue = RangePickerProps['value'];
type AnalysisProps = {
dashboardAndanalysis: AnalysisData;
loading: boolean;
};
type SalesType = 'all' | 'online' | 'stores';
const Analysis: FC<AnalysisProps> = () => {
const { styles } = useStyles();
const [salesType, setSalesType] = useState<SalesType>('all');
const [currentTabKey, setCurrentTabKey] = useState<string>('');
const [rangePickerValue, setRangePickerValue] = useState<RangePickerValue>(
getTimeDistance('year'),
);
const { loading, data } = useRequest(fakeChartData);
const selectDate = (type: TimeType) => {
setRangePickerValue(getTimeDistance(type));
};
const handleRangePickerChange = (value: RangePickerValue) => {
setRangePickerValue(value);
};
const isActive = (type: TimeType) => {
if (!rangePickerValue) {
return '';
}
const value = getTimeDistance(type);
if (!value) {
return '';
}
if (!rangePickerValue[0] || !rangePickerValue[1]) {
return '';
}
if (
rangePickerValue[0].isSame(value[0] as Dayjs, 'day') &&
rangePickerValue[1].isSame(value[1] as Dayjs, 'day')
) {
return styles.currentDate;
}
return '';
};
let salesPieData: any;
if (salesType === 'all') {
salesPieData = data?.salesTypeData;
} else {
salesPieData =
salesType === 'online'
? data?.salesTypeDataOnline
: data?.salesTypeDataOffline;
}
const dropdownGroup = (
<span className={styles.iconGroup}>
<Dropdown
menu={{
items: [
{
key: '1',
label: '操作一',
},
{
key: '2',
label: '操作二',
},
],
}}
placement="bottomRight"
>
<EllipsisOutlined />
</Dropdown>
</span>
);
const handleChangeSalesType = (value: SalesType) => {
setSalesType(value);
};
const handleTabChange = (key: string) => {
setCurrentTabKey(key);
};
const activeKey = currentTabKey || data?.offlineData[0]?.name || '';
return (
<GridContent>
<Suspense fallback={<PageLoading />}>
<IntroduceRow loading={loading} visitData={data?.visitData || []} />
</Suspense>
<Suspense fallback={null}>
<SalesCard
rangePickerValue={rangePickerValue}
salesData={data?.salesData || []}
isActive={isActive}
handleRangePickerChange={handleRangePickerChange}
loading={loading}
selectDate={selectDate}
/>
</Suspense>
<Row
gutter={24}
style={{
marginTop: 24,
}}
>
<Col xl={12} lg={24} md={24} sm={24} xs={24}>
<Suspense fallback={null}>
<TopSearch
loading={loading}
visitData2={data?.visitData2 || []}
searchData={data?.searchData || []}
dropdownGroup={dropdownGroup}
/>
</Suspense>
</Col>
<Col xl={12} lg={24} md={24} sm={24} xs={24}>
<Suspense fallback={null}>
<ProportionSales
dropdownGroup={dropdownGroup}
salesType={salesType}
loading={loading}
salesPieData={salesPieData || []}
handleChangeSalesType={handleChangeSalesType}
/>
</Suspense>
</Col>
</Row>
<Suspense fallback={null}>
<OfflineData
activeKey={activeKey}
loading={loading}
offlineData={data?.offlineData || []}
offlineChartData={data?.offlineChartData || []}
handleTabChange={handleTabChange}
/>
</Suspense>
</GridContent>
);
};
export default Analysis;

6
src/pages/dashboard/analysis/service.ts

@ -0,0 +1,6 @@
import { request } from '@umijs/max';
import type { AnalysisData } from './data';
export async function fakeChartData(): Promise<{ data: AnalysisData }> {
return request('/api/fake_analysis_chart_data');
}

189
src/pages/dashboard/analysis/style.less

@ -0,0 +1,189 @@
@import '~antd/es/style/themes/default.less';
.iconGroup {
span.anticon {
margin-left: 16px;
color: @text-color-secondary;
cursor: pointer;
transition: color 0.32s;
&:hover {
color: @text-color;
}
}
}
.rankingList {
margin: 25px 0 0;
padding: 0;
list-style: none;
li {
display: flex;
align-items: center;
margin-top: 16px;
zoom: 1;
&::before,
&::after {
display: table;
content: ' ';
}
&::after {
clear: both;
height: 0;
font-size: 0;
visibility: hidden;
}
span {
color: @text-color;
font-size: 14px;
line-height: 22px;
}
.rankingItemNumber {
display: inline-block;
width: 20px;
height: 20px;
margin-top: 1.5px;
margin-right: 16px;
font-weight: 600;
font-size: 12px;
line-height: 20px;
text-align: center;
background-color: @tag-default-bg;
border-radius: 20px;
&.active {
color: #fff;
background-color: #314659;
}
}
.rankingItemTitle {
flex: 1;
margin-right: 8px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.salesExtra {
display: inline-block;
margin-right: 24px;
a {
margin-left: 24px;
color: @text-color;
&:hover {
color: @primary-color;
}
&.currentDate {
color: @primary-color;
}
}
}
.salesCard {
.salesBar {
padding: 0 0 32px 32px;
}
.salesRank {
padding: 0 32px 32px 72px;
}
:global {
.ant-tabs-bar,
.ant-tabs-nav-wrap {
padding-left: 16px;
.ant-tabs-nav .ant-tabs-tab {
padding-top: 16px;
padding-bottom: 14px;
line-height: 24px;
}
}
.ant-tabs-extra-content {
padding-right: 24px;
line-height: 55px;
}
.ant-card-head {
position: relative;
}
.ant-card-head-title {
align-items: normal;
}
}
}
.salesCardExtra {
height: inherit;
}
.salesTypeRadio {
position: absolute;
right: 54px;
bottom: 12px;
}
.offlineCard {
:global {
.ant-tabs-ink-bar {
bottom: auto;
}
.ant-tabs-bar {
border-bottom: none;
}
.ant-tabs-nav-container-scrolling {
padding-right: 40px;
padding-left: 40px;
}
.ant-tabs-tab-prev-icon::before {
position: relative;
left: 6px;
}
.ant-tabs-tab-next-icon::before {
position: relative;
right: 6px;
}
.ant-tabs-tab-active h4 {
color: @primary-color;
}
}
}
.trendText {
margin-left: 8px;
color: @heading-color;
}
@media screen and (max-width: @screen-lg) {
.salesExtra {
display: none;
}
.rankingList {
li {
span:first-child {
margin-right: 8px;
}
}
}
}
@media screen and (max-width: @screen-md) {
.rankingTitle {
margin-top: 16px;
}
.salesCard .salesBar {
padding: 16px;
}
}
@media screen and (max-width: @screen-sm) {
.salesExtraWrap {
display: none;
}
.salesCard {
:global {
.ant-tabs-content {
padding-top: 30px;
}
}
}
}

160
src/pages/dashboard/analysis/style.style.ts

@ -0,0 +1,160 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
iconGroup: {
'span.anticon': {
marginLeft: '16px',
color: token.colorTextSecondary,
cursor: 'pointer',
transition: 'color 0.32s',
'&:hover': {
color: token.colorText,
},
},
},
rankingList: {
margin: '25px 0 0',
padding: '0',
listStyle: 'none',
li: {
display: 'flex',
alignItems: 'center',
marginTop: '16px',
zoom: '1',
'&::before, &::after': {
display: 'table',
content: "' '",
},
'&::after': {
clear: 'both',
height: '0',
fontSize: '0',
visibility: 'hidden',
},
},
[`@media screen and (max-width: ${token.screenLG}px)`]: {
li: {
'span:first-child': { marginRight: '8px' },
},
},
},
rankingItemNumber: {
display: 'inline-block',
width: '20px',
height: '20px',
marginTop: '1.5px',
marginRight: '16px',
fontWeight: '600',
fontSize: '12px',
lineHeight: '20px',
textAlign: 'center',
borderRadius: '20px',
backgroundColor: token.colorBgContainerDisabled,
},
rankingItemTitle: {
flex: '1',
marginRight: '8px',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
},
rankingItemNumberActive: {
display: 'inline-block',
width: '20px',
height: '20px',
marginTop: '1.5px',
marginRight: '16px',
fontWeight: '600',
fontSize: '12px',
lineHeight: '20px',
textAlign: 'center',
borderRadius: '20px',
color: '#fff',
backgroundColor: token.colorBgSpotlight,
},
salesExtra: {
display: 'inline-block',
marginRight: '24px',
a: {
marginLeft: '24px',
color: token.colorText,
'&:hover': {
color: token.colorPrimary,
},
},
[`@media screen and (max-width: ${token.screenLG}px)`]: {
display: 'none',
},
},
currentDate: {
color: token.colorPrimary,
fontWeight: 'bold',
},
salesBar: {
padding: '0 0 32px 32px',
[`@media screen and (max-width: ${token.screenMD}px)`]: {
padding: '16px',
},
},
salesRank: {
padding: '0 32px 32px 72px',
},
salesCard: {
'.ant-tabs-bar, .ant-tabs-nav-wrap': {
paddingLeft: '16px',
'.ant-tabs-nav .ant-tabs-tab': {
paddingTop: '16px',
paddingBottom: '14px',
lineHeight: '24px',
},
},
'.ant-tabs-extra-content': { paddingRight: '24px', lineHeight: '55px' },
'.ant-card-head': { position: 'relative' },
'.ant-card-head-title': { alignItems: 'normal' },
[`@media screen and (max-width: ${token.screenMD}px)`]: {
padding: '16px',
},
[`@media screen and (max-width: ${token.screenSM}px)`]: {
'.ant-tabs-content': {
paddingTop: '30px',
},
},
},
salesCardExtra: {
height: 'inherit',
},
salesTypeRadio: {
position: 'absolute',
right: '54px',
bottom: '12px',
},
offlineCard: {
'.ant-tabs-ink-bar': { bottom: 'auto' },
'.ant-tabs-bar': { borderBottom: 'none' },
'.ant-tabs-nav-container-scrolling': {
paddingRight: '40px',
paddingLeft: '40px',
},
'.ant-tabs-tab-prev-icon::before': { position: 'relative', left: '6px' },
'.ant-tabs-tab-next-icon::before': { position: 'relative', right: '6px' },
'.ant-tabs-tab-active h4': { color: token.colorPrimary },
},
trendText: {
marginLeft: '8px',
color: token.colorTextHeading,
},
rankingTitle: {
[`@media screen and (max-width: ${token.screenMD}px)`]: {
marginTop: '16px',
},
},
salesExtraWrap: {
[`@media screen and (max-width: ${token.screenSM}px)`]: {
display: 'none',
},
},
};
});
export default useStyles;

17
src/pages/dashboard/analysis/utils/Yuan.tsx

@ -0,0 +1,17 @@
import { useEffect, useRef } from 'react';
import { yuan } from '../components/Charts';
/** 减少使用 dangerouslySetInnerHTML */
const Yuan: React.FC<{ children: string | number }> = ({ children }) => {
const spanRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (spanRef.current) {
spanRef.current.innerHTML = yuan(children);
}
}, [children]);
return <span ref={spanRef} />;
};
export default Yuan;

57
src/pages/dashboard/analysis/utils/utils.ts

@ -0,0 +1,57 @@
import type { RangePickerProps } from 'antd/es/date-picker';
import dayjs from 'dayjs';
type RangePickerValue = RangePickerProps['value'];
export function fixedZero(val: number) {
return val * 1 < 10 ? `0${val}` : val;
}
export function getTimeDistance(
type: 'today' | 'week' | 'month' | 'year',
): RangePickerValue {
const now = new Date();
const oneDay = 1000 * 60 * 60 * 24;
if (type === 'today') {
now.setHours(0);
now.setMinutes(0);
now.setSeconds(0);
return [dayjs(now), dayjs(now.getTime() + (oneDay - 1000))];
}
if (type === 'week') {
let day = now.getDay();
now.setHours(0);
now.setMinutes(0);
now.setSeconds(0);
if (day === 0) {
day = 6;
} else {
day -= 1;
}
const beginTime = now.getTime() - day * oneDay;
return [dayjs(beginTime), dayjs(beginTime + (7 * oneDay - 1000))];
}
const year = now.getFullYear();
if (type === 'month') {
const month = now.getMonth();
const nextDate = dayjs(now).add(1, 'months');
const nextYear = nextDate.year();
const nextMonth = nextDate.month();
return [
dayjs(`${year}-${fixedZero(month + 1)}-01 00:00:00`),
dayjs(
dayjs(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).valueOf() -
1000,
),
];
}
return [dayjs(`${year}-01-01 00:00:00`), dayjs(`${year}-12-31 23:59:59`)];
}

14
src/pages/dashboard/monitor/_mock.ts

@ -0,0 +1,14 @@
import type { Request, Response } from 'express';
import mockjs from 'mockjs';
const getTags = (_: Request, res: Response) => {
return res.json({
data: mockjs.mock({
'list|100': [{ name: '@city', 'value|1-100': 150, 'type|0-2': 1 }],
}),
});
};
export default {
'GET /api/tags': getTags,
};

51
src/pages/dashboard/monitor/components/ActiveChart/index.less

@ -0,0 +1,51 @@
.activeChart {
position: relative;
}
.activeChartGrid {
p {
position: absolute;
top: 80px;
}
p:last-child {
top: 115px;
}
}
.activeChartLegend {
position: relative;
height: 20px;
margin-top: 8px;
font-size: 0;
line-height: 20px;
span {
display: inline-block;
width: 33.33%;
font-size: 12px;
text-align: center;
}
span:first-child {
text-align: left;
}
span:last-child {
text-align: right;
}
}
.dashedLine {
position: relative;
top: -70px;
left: -3px;
height: 1px;
.line {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: linear-gradient(to right, transparent 50%, #e9e9e9 50%);
background-size: 6px;
}
}
.dashedLine:last-child {
top: -36px;
}

48
src/pages/dashboard/monitor/components/ActiveChart/index.style.ts

@ -0,0 +1,48 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(() => {
return {
activeChart: {
position: 'relative',
},
activeChartGrid: {
p: { position: 'absolute', top: '80px' },
'p:last-child': { top: '115px' },
},
activeChartLegend: {
position: 'relative',
height: '20px',
marginTop: '8px',
fontSize: '0',
lineHeight: '20px',
span: {
display: 'inline-block',
width: '33.33%',
fontSize: '12px',
textAlign: 'center',
},
'span:first-child': { textAlign: 'left' },
'span:last-child': { textAlign: 'right' },
},
dashedLine: {
position: 'relative',
top: '-70px',
left: '-3px',
height: '1px',
},
line: {
position: 'absolute',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundImage:
'linear-gradient(to right, transparent 50%, #e9e9e9 50%)',
backgroundSize: '6px',
},
'dashedLine:last-child': {
top: '-36px',
},
};
});
export default useStyles;

94
src/pages/dashboard/monitor/components/ActiveChart/index.tsx

@ -0,0 +1,94 @@
import { Area } from '@ant-design/plots';
import { Statistic } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import useStyles from './index.style';
function fixedZero(val: number) {
return val * 1 < 10 ? `0${val}` : val;
}
function getActiveData() {
const activeData = [];
for (let i = 0; i < 24; i += 1) {
activeData.push({
x: `${fixedZero(i)}:00`,
y: Math.floor(Math.random() * 200) + i * 50,
});
}
return activeData;
}
const ActiveChart = () => {
const timerRef = useRef<number | null>(null);
const { styles } = useStyles();
const [activeData, setActiveData] = useState<{ x: string; y: number }[]>([]);
useEffect(() => {
const loopData = () => {
setActiveData(getActiveData());
timerRef.current = window.setTimeout(loopData, 2000);
};
loopData();
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, []);
// Memoize max and median to avoid double sort on every render
const { maxValue, medianValue } = useMemo(() => {
if (!activeData.length) return { maxValue: 0, medianValue: 0 };
const sorted = [...activeData].sort((a, b) => a.y - b.y);
return {
maxValue: sorted[sorted.length - 1]?.y ?? 0,
medianValue: sorted[Math.floor(sorted.length / 2)]?.y ?? 0,
};
}, [activeData]);
return (
<div className={styles.activeChart}>
<Statistic title="目标评估" value="有望达到预期" />
<div
style={{
marginTop: 32,
}}
>
<Area
padding={[0, 0, 0, 0]}
xField="x"
axis={false}
yField="y"
height={84}
style={{
fill: 'linear-gradient(-90deg, white 0%, #6294FA 100%)',
fillOpacity: 0.6,
}}
data={activeData}
/>
</div>
{activeData && (
<div>
<div className={styles.activeChartGrid}>
<p>{maxValue + 200} 亿</p>
<p>{medianValue} 亿</p>
</div>
<div className={styles.dashedLine}>
<div className={styles.line} />
</div>
<div className={styles.dashedLine}>
<div className={styles.line} />
</div>
</div>
)}
{activeData && (
<div className={styles.activeChartLegend}>
<span>00:00</span>
<span>{activeData[Math.floor(activeData.length / 2)]?.x}</span>
<span>{activeData[activeData.length - 1]?.x}</span>
</div>
)}
</div>
);
};
export default ActiveChart;

150
src/pages/dashboard/monitor/components/Map/index.tsx

@ -0,0 +1,150 @@
import { PageLoading } from '@ant-design/pro-components';
import { HeatmapLayer, MapboxScene, PointLayer } from '@antv/l7-react';
import { useEffect, useState } from 'react';
const colors = [
'#eff3ff',
'#c6dbef',
'#9ecae1',
'#6baed6',
'#4292c6',
'#2171b5',
'#084594',
];
export default function MonitorMap() {
const [data, setData] = useState<Record<string, unknown>[] | null>(null);
const [grid, setGrid] = useState<Record<string, unknown>[] | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
async function fetchData() {
const [geoData, gridData] = await Promise.all([
fetch(
'https://gw.alipayobjects.com/os/bmw-prod/c5dba875-b6ea-4e88-b778-66a862906c93.json',
).then((d) => d.json()),
fetch(
'https://gw.alipayobjects.com/os/bmw-prod/8990e8b4-c58e-419b-afb9-8ea3daff2dd1.json',
).then((d) => d.json()),
]);
setData(geoData);
setGrid(gridData);
setLoading(true);
}
fetchData();
}, []);
return loading === false ? (
<PageLoading />
) : (
<MapboxScene
map={{
center: [110.19382669582967, 50.258134],
pitch: 0,
style: 'blank',
zoom: 1,
}}
style={{
position: 'relative',
width: '100%',
height: '452px',
}}
>
{grid && (
<HeatmapLayer
key="1"
source={{
data: grid,
transforms: [
{
type: 'hexagon',
size: 800000,
field: 'capacity',
method: 'sum',
},
],
}}
color={{
values: '#ddd',
}}
shape={{
values: 'hexagon',
}}
style={{
coverage: 0.7,
opacity: 0.8,
}}
/>
)}
{data && [
<PointLayer
key="2"
options={{
autoFit: true,
}}
source={{
data,
}}
scale={{
values: {
color: {
field: 'cum_conf',
type: 'quantile',
},
size: {
field: 'cum_conf',
type: 'log',
},
},
}}
color={{
field: 'cum_conf',
values: colors,
}}
shape={{
values: 'circle',
}}
active={{
option: {
color: '#0c2c84',
},
}}
size={{
field: 'cum_conf',
values: [0, 30],
}}
style={{
opacity: 0.8,
}}
/>,
<PointLayer
key="5"
source={{
data,
}}
color={{
values: '#fff',
}}
shape={{
field: 'Short_Name_ZH',
values: 'text',
}}
filter={{
field: 'cum_conf',
values: (v) => {
return v > 2000;
},
}}
size={{
values: 12,
}}
style={{
opacity: 1,
strokeOpacity: 1,
strokeWidth: 0,
}}
/>,
]}
</MapboxScene>
);
}

5
src/pages/dashboard/monitor/data.d.ts

@ -0,0 +1,5 @@
export type TagType = {
name: string;
value: number;
type: string;
};

203
src/pages/dashboard/monitor/index.tsx

@ -0,0 +1,203 @@
import { Gauge, Liquid, WordCloud } from '@ant-design/plots';
import { GridContent } from '@ant-design/pro-components';
import { useRequest } from '@umijs/max';
import { Card, Col, Progress, Row, Statistic } from 'antd';
import numeral from 'numeral';
import type { FC } from 'react';
import ActiveChart from './components/ActiveChart';
import MonitorMap from './components/Map';
import { queryTags } from './service';
import useStyles from './style.style';
const deadline = Date.now() + 1000 * 60 * 60 * 24 * 2 + 1000 * 30; // Moment is also OK
const Monitor: FC = () => {
const { styles } = useStyles();
const { loading, data } = useRequest(queryTags);
const wordCloudData = (data?.list || []).map((item) => {
return {
id: +Date.now(),
word: item.name,
weight: item.value,
};
});
return (
<GridContent>
<Row gutter={24}>
<Col
xl={18}
lg={24}
md={24}
sm={24}
xs={24}
style={{
marginBottom: 24,
}}
>
<Card title="活动实时交易情况" variant="borderless">
<Row>
<Col md={6} sm={12} xs={24}>
<Statistic
title="今日交易总额"
suffix="元"
value={numeral(124543233).format('0,0')}
/>
</Col>
<Col md={6} sm={12} xs={24}>
<Statistic title="销售目标完成率" value="92%" />
</Col>
<Col md={6} sm={12} xs={24}>
<Statistic.Timer
type="countdown"
title="活动剩余时间"
value={deadline}
format="HH:mm:ss:SSS"
/>
</Col>
<Col md={6} sm={12} xs={24}>
<Statistic
title="每秒交易总额"
suffix="元"
value={numeral(234).format('0,0')}
/>
</Col>
</Row>
<div className={styles.mapChart}>
<MonitorMap />
</div>
</Card>
</Col>
<Col xl={6} lg={24} md={24} sm={24} xs={24}>
<Card
title="活动情况预测"
style={{
marginBottom: 24,
}}
variant="borderless"
>
<ActiveChart />
</Card>
<Card
title="券核效率"
style={{
marginBottom: 24,
}}
styles={{
body: {
textAlign: 'center',
},
}}
variant="borderless"
>
<Gauge
height={180}
data={
{
target: 80,
total: 100,
name: 'score',
thresholds: [20, 40, 60, 80, 100],
} as any
}
padding={-16}
style={{
textContent: () => '优',
}}
meta={{
color: {
range: [
'#6395FA',
'#62DAAB',
'#657798',
'#F7C128',
'#1F8718',
],
},
}}
/>
</Card>
</Col>
</Row>
<Row gutter={24}>
<Col
xl={12}
lg={24}
sm={24}
xs={24}
style={{
marginBottom: 24,
}}
>
<Card title="各品类占比" variant="borderless">
<Row
style={{
padding: '16px 0',
}}
>
<Col span={8}>
<Progress type="dashboard" percent={75} />
</Col>
<Col span={8}>
<Progress type="dashboard" percent={48} />
</Col>
<Col span={8}>
<Progress type="dashboard" percent={33} />
</Col>
</Row>
</Card>
</Col>
<Col
xl={6}
lg={12}
sm={24}
xs={24}
style={{
marginBottom: 24,
}}
>
<Card
title="热门搜索"
loading={loading}
variant="borderless"
styles={{
body: {
overflow: 'hidden',
},
}}
>
<WordCloud
data={wordCloudData}
height={162}
textField="word"
colorField="word"
layout={{ spiral: 'rectangular', fontSize: [10, 20] }}
/>
</Card>
</Col>
<Col
xl={6}
lg={12}
sm={24}
xs={24}
style={{
marginBottom: 24,
}}
>
<Card
title="资源剩余"
styles={{
body: {
textAlign: 'center',
fontSize: 0,
},
}}
variant="borderless"
>
<Liquid height={160} percent={0.35} />
</Card>
</Col>
</Row>
</GridContent>
);
};
export default Monitor;

6
src/pages/dashboard/monitor/service.ts

@ -0,0 +1,6 @@
import { request } from '@umijs/max';
import type { TagType } from './data';
export async function queryTags(): Promise<{ data: { list: TagType[] } }> {
return request('/api/tags');
}

21
src/pages/dashboard/monitor/style.less

@ -0,0 +1,21 @@
@import '~antd/es/style/themes/default.less';
.mapChart {
height: 452px;
padding-top: 24px;
img {
display: inline-block;
max-width: 100%;
max-height: 437px;
}
}
.pieCard :global(.pie-stat) {
font-size: 24px !important;
}
@media screen and (max-width: @screen-lg) {
.mapChart {
height: auto;
}
}

16
src/pages/dashboard/monitor/style.style.ts

@ -0,0 +1,16 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
mapChart: {
height: '452px',
paddingTop: '24px',
img: { display: 'inline-block', maxWidth: '100%', maxHeight: '437px' },
[`@media screen and (max-width: ${token.screenLG}px)`]: {
height: 'auto',
},
},
};
});
export default useStyles;

410
src/pages/dashboard/workplace/_mock.ts

@ -0,0 +1,410 @@
import dayjs from 'dayjs';
import type { Request, Response } from 'express';
import type { DataItem, OfflineDataType, SearchDataType } from './data.d';
// mock data
const visitData: DataItem[] = [];
const beginDay = Date.now();
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
for (let i = 0; i < fakeY.length; i += 1) {
visitData.push({
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: fakeY[i],
});
}
const visitData2: DataItem[] = [];
const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
for (let i = 0; i < fakeY2.length; i += 1) {
visitData2.push({
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: fakeY2[i],
});
}
const salesData: DataItem[] = [];
for (let i = 0; i < 12; i += 1) {
salesData.push({
x: `${i + 1}`,
y: Math.floor(Math.random() * 1000) + 200,
});
}
const searchData: SearchDataType[] = [];
for (let i = 0; i < 50; i += 1) {
searchData.push({
index: i + 1,
keyword: `搜索关键词-${i}`,
count: Math.floor(Math.random() * 1000),
range: Math.floor(Math.random() * 100),
status: Math.floor((Math.random() * 10) % 2),
});
}
const salesTypeData = [
{
x: '家用电器',
y: 4544,
},
{
x: '食用酒水',
y: 3321,
},
{
x: '个护健康',
y: 3113,
},
{
x: '服饰箱包',
y: 2341,
},
{
x: '母婴产品',
y: 1231,
},
{
x: '其他',
y: 1231,
},
];
const salesTypeDataOnline = [
{
x: '家用电器',
y: 244,
},
{
x: '食用酒水',
y: 321,
},
{
x: '个护健康',
y: 311,
},
{
x: '服饰箱包',
y: 41,
},
{
x: '母婴产品',
y: 121,
},
{
x: '其他',
y: 111,
},
];
const salesTypeDataOffline = [
{
x: '家用电器',
y: 99,
},
{
x: '食用酒水',
y: 188,
},
{
x: '个护健康',
y: 344,
},
{
x: '服饰箱包',
y: 255,
},
{
x: '其他',
y: 65,
},
];
const offlineData: OfflineDataType[] = [];
for (let i = 0; i < 10; i += 1) {
offlineData.push({
name: `Stores ${i}`,
cvr: Math.ceil(Math.random() * 9) / 10,
});
}
const offlineChartData: DataItem[] = [];
for (let i = 0; i < 20; i += 1) {
offlineChartData.push({
x: Date.now() + 1000 * 60 * 30 * i,
y1: Math.floor(Math.random() * 100) + 10,
y2: Math.floor(Math.random() * 100) + 10,
});
}
const titles = [
'Alipay',
'Angular',
'Ant Design',
'Ant Design Pro',
'Bootstrap',
'React',
'Vue',
'Webpack',
];
const avatars = [
'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
];
const avatars2 = [
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png',
'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png',
'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png',
'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png',
'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png',
'https://gw.alipayobjects.com/zos/rmsportal/psOgztMplJMGpVEqfcgF.png',
'https://gw.alipayobjects.com/zos/rmsportal/ZpBqSxLxVEXfcUNoPKrz.png',
'https://gw.alipayobjects.com/zos/rmsportal/laiEnJdGHVOhJrUShBaJ.png',
'https://gw.alipayobjects.com/zos/rmsportal/UrQsqscbKEpNuJcvBZBu.png',
];
const getNotice = (_: Request, res: Response) => {
res.json({
data: [
{
id: 'xxx1',
title: titles[0],
logo: avatars[0],
description: '那是一种内在的东西,他们到达不了,也无法触及的',
updatedAt: new Date(),
member: '科学搬砖组',
href: '',
memberLink: '',
},
{
id: 'xxx2',
title: titles[1],
logo: avatars[1],
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
updatedAt: new Date('2017-07-24'),
member: '全组都是吴彦祖',
href: '',
memberLink: '',
},
{
id: 'xxx3',
title: titles[2],
logo: avatars[2],
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
updatedAt: new Date(),
member: '中二少女团',
href: '',
memberLink: '',
},
{
id: 'xxx4',
title: titles[3],
logo: avatars[3],
description: '那时候我只会想自己想要什么,从不想自己拥有什么',
updatedAt: new Date('2017-07-23'),
member: '程序员日常',
href: '',
memberLink: '',
},
{
id: 'xxx5',
title: titles[4],
logo: avatars[4],
description: '凛冬将至',
updatedAt: new Date('2017-07-23'),
member: '高逼格设计天团',
href: '',
memberLink: '',
},
{
id: 'xxx6',
title: titles[5],
logo: avatars[5],
description: '生命就像一盒巧克力,结果往往出人意料',
updatedAt: new Date('2017-07-23'),
member: '骗你来学计算机',
href: '',
memberLink: '',
},
],
});
};
const getActivities = (_: Request, res: Response) => {
res.json({
data: [
{
id: 'trend-1',
updatedAt: new Date(),
user: {
name: '曲丽丽',
avatar: avatars2[0],
},
group: {
name: '高逼格设计天团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-2',
updatedAt: new Date(),
user: {
name: '付小小',
avatar: avatars2[1],
},
group: {
name: '高逼格设计天团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-3',
updatedAt: new Date(),
user: {
name: '林东东',
avatar: avatars2[2],
},
group: {
name: '中二少女团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-4',
updatedAt: new Date(),
user: {
name: '周星星',
avatar: avatars2[4],
},
project: {
name: '5 月日常迭代',
link: 'http://github.com/',
},
template: '将 @{project} 更新至已发布状态',
},
{
id: 'trend-5',
updatedAt: new Date(),
user: {
name: '朱偏右',
avatar: avatars2[3],
},
project: {
name: '工程效能',
link: 'http://github.com/',
},
comment: {
name: '留言',
link: 'http://github.com/',
},
template: '在 @{project} 发布了 @{comment}',
},
{
id: 'trend-6',
updatedAt: new Date(),
user: {
name: '乐哥',
avatar: avatars2[5],
},
group: {
name: '程序员日常',
link: 'http://github.com/',
},
project: {
name: '品牌迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
],
});
};
const radarOriginData = [
{
name: '个人',
ref: 10,
koubei: 8,
output: 4,
contribute: 5,
hot: 7,
},
{
name: '团队',
ref: 3,
koubei: 9,
output: 6,
contribute: 3,
hot: 1,
},
{
name: '部门',
ref: 4,
koubei: 1,
output: 6,
contribute: 5,
hot: 7,
},
];
const radarData: any[] = [];
const radarTitleMap = {
ref: '引用',
koubei: '口碑',
output: '产量',
contribute: '贡献',
hot: '热度',
};
radarOriginData.forEach((item) => {
Object.keys(item).forEach((key) => {
if (key !== 'name') {
radarData.push({
name: item.name,
label: radarTitleMap[key as 'ref'],
value: item[key as 'ref'],
});
}
});
});
const getChartData = (_: Request, res: Response) => {
res.json({
data: {
visitData,
visitData2,
salesData,
searchData,
offlineData,
offlineChartData,
salesTypeData,
salesTypeDataOnline,
salesTypeDataOffline,
radarData,
},
});
};
export default {
'GET /api/project/notice': getNotice,
'GET /api/activities': getActivities,
'GET /api/fake_workplace_chart_data': getChartData,
};

16
src/pages/dashboard/workplace/components/EditableLinkGroup/index.less

@ -0,0 +1,16 @@
@import '~antd/es/style/themes/default.less';
.linkGroup {
padding: 20px 0 8px 24px;
font-size: 0;
& > a {
display: inline-block;
width: 25%;
margin-bottom: 13px;
color: @text-color;
font-size: @font-size-base;
&:hover {
color: @primary-color;
}
}
}

21
src/pages/dashboard/workplace/components/EditableLinkGroup/index.style.ts

@ -0,0 +1,21 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
linkGroup: {
fontSize: '0',
'& > a': {
display: 'inline-block',
width: '25%',
marginBottom: '13px',
color: token.colorText,
fontSize: token.fontSize,
'&:hover': {
color: token.colorPrimary,
},
},
},
};
});
export default useStyles;

38
src/pages/dashboard/workplace/components/EditableLinkGroup/index.tsx

@ -0,0 +1,38 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import React, { createElement } from 'react';
import useStyles from './index.style';
export type EditableLink = {
title: string;
href: string;
id?: string;
};
type EditableLinkGroupProps = {
onAdd: () => void;
links: EditableLink[];
linkElement: any;
};
const EditableLinkGroup: React.FC<EditableLinkGroupProps> = (props) => {
const { styles } = useStyles();
const { links = [], linkElement = 'a', onAdd = () => {} } = props;
return (
<div className={styles.linkGroup}>
{links.map((link) =>
createElement(
linkElement,
{
key: `linkGroup-item-${link.id || link.title}`,
to: link.href,
href: link.href,
},
link.title,
),
)}
<Button size="small" type="primary" ghost onClick={onAdd}>
<PlusOutlined />
</Button>
</div>
);
};
export default EditableLinkGroup;

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

Loading…
Cancel
Save