committed by
GitHub
199 changed files with 16372 additions and 1446 deletions
@ -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. |
|||
|
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml) |
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml) |
|||
[](http://umijs.org/) |
|||
[](https://biomejs.dev) |
|||
[](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) لتحسين الكود الخاص بنا. |
|||
@ -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. |
|||
|
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml) |
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml) |
|||
[](http://umijs.org/) |
|||
[](https://biomejs.dev) |
|||
[](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. |
|||
@ -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. |
|||
|
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml) |
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml) |
|||
[](http://umijs.org/) |
|||
[](https://biomejs.dev) |
|||
[](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. |
|||
@ -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ソリューション。 |
|||
|
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml) |
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml) |
|||
[](http://umijs.org/) |
|||
[](https://biomejs.dev) |
|||
[](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)でコード改善を提案する |
|||
@ -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. |
|||
|
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml) |
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml) |
|||
[](http://umijs.org/) |
|||
[](https://biomejs.dev) |
|||
[](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. |
|||
@ -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. |
|||
|
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml) |
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml) |
|||
[](http://umijs.org/) |
|||
[](https://biomejs.dev) |
|||
[](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) для улучшения кода. |
|||
@ -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ü. |
|||
|
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/ci.yml) |
|||
[](https://github.com/ant-design/ant-design-pro/actions/workflows/preview-deploy.yml) |
|||
[](http://umijs.org/) |
|||
[](https://biomejs.dev) |
|||
[](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. |
|||
@ -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: './*', |
|||
}, |
|||
]; |
|||
@ -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, |
|||
}; |
|||
@ -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, |
|||
}; |
|||
@ -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(); |
|||
@ -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; |
|||
@ -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, |
|||
}; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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[]; |
|||
}; |
|||
@ -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; |
|||
@ -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, |
|||
}); |
|||
} |
|||
@ -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, |
|||
}; |
|||
@ -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> |
|||
</> |
|||
); |
|||
}; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
}; |
|||
File diff suppressed because it is too large
@ -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" |
|||
} |
|||
] |
|||
@ -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; |
|||
@ -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'); |
|||
} |
|||
@ -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; |
|||
@ -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, |
|||
}; |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
@ -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 }; |
|||
@ -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; |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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> |
|||
); |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
@ -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; |
|||
@ -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[]; |
|||
} |
|||
@ -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; |
|||
@ -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'); |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
@ -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; |
|||
@ -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`)]; |
|||
} |
|||
@ -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, |
|||
}; |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
@ -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; |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
export type TagType = { |
|||
name: string; |
|||
value: number; |
|||
type: string; |
|||
}; |
|||
@ -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; |
|||
@ -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'); |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
@ -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, |
|||
}; |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
@ -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…
Reference in new issue